Design pattern for e2e test
Design pattern for e2e test
When using e2e testing in my work, we faced the issue of a lot of code being duplicated. To solve this problem, we defined a dedicated new design pattern for e2e test.
Folder structure of app
Before start, I would inform you that our company is using two different concept of components: components
and modules
.
project
├── components/
│ ├── Button.ts
│ ├── Menu.ts
│ ├── PasswordInput.ts
│ └── EmailInput.ts
├── modules/
│ ├── Navbar/
│ │ ├── Navbar.vue
│ │ └── Navbar.e2e.model.ts
│ └── SubmitForm/
│ ├── SubmitForm.vue
│ └── SubmitForm.e2e.model.ts
├── e2e/
...
A module is a set of multiple components and it takes in charge of a feature of your app such as navbar
or submit form
. You might noticed that each module has own XXX.e2e.model.ts
file being used in our e2e test.
What is e2e.model
file here?
Model
file is a kind of specification for e2e test. It defines the functions that we gonna test in e2e test, so it's only imported in e2e test file, not in your app. However we decided to leave model file together with module so that we don't need to switching folder between e2e and modules to make a spec of the model of module.
Here is a simple example of navbar model file.
javascript
enum TEST_IDS {
LOGIN_POPUP_TRIGGER = 'LoginPopupTrigger',
LOGOUT_BUTTON = 'LogoutButton',
}
export default {
async openLoginPopup(page) {
await page.click(`[data-test="${TEST_IDS.LOGIN_POPUP_TRIGGER}"]`)
/* ... */
},
async logout(page) {
await page.click(`[data-test="${TEST_IDS.LOGOUT_BUTTON}"]`)
/* ... */
}
}
Folder structure of e2e test
Here is e2e test folder structure of our app. The important thing here is to understand what a model and scenario are.
project
├── e2e/
│ ├── assets/
│ ├── models/
│ │ ├── index.model.ts
│ │ └── navigation.model.ts
│ ├── scenarios/
│ │ ├── user-login.scenario.ts
│ │ └── buy-product.scenario.ts
│ ├── tests/
│ │ └── user-buy-product.e2e.spec.ts
│ ├── types.ts
│ └── utils
assets/
e2e related assets such as uploading image, pdf, etc.
models/
Naming convention: [name].model.ts
As explained above, Model is a specification of module. This folder has index.model.ts
which will import all models under modules
.
javascript
// /e2e/models/index.model.ts
export { default as Navbar } from '@/modules/Navbar/Navbar.e2e.model';
export { default as SubmitForm } from '@/modules/SubmitForm/SubmitForm.e2e.model';
We can also create some useful model for e2e test under models/
folder. For example, our project has Navigation.model.ts
. Highly recommended to create more model if you think some codes are used in many places of e2e test.
javascript
// /e2e/models/Navigation.model.ts
class Navigation {
page: Page;
chatButton: Locator;
constructor(page) {
/* ... */
}
async reload() {
await this.page.reload();
await this.page.waitForTimeout(500);
}
async checkUrlContains(hash: string): boolean {
const pageUrl = this.page.url();
return pageUrl.includes(hash);
}
async endTest() { await this.page.close(); }
async openChat() { await this.chatButton.open(); }
async closeChat() { await this.chatButton.close(); }
async goToLoginPage() { /* ... */ }
}
scenarios/
Naming convention: [name].scenario.ts
Scenario is a file where we write the scenario we want to test. Simply put, a scenario can be thought of as a set of models.
javascript
// /e2e/scenarios/user-login.scenario.ts
import { Navbar, LoginPopup } from "@/e2e/models"
import Navigation from '@e2e/models/Navigation';
export default async (page, { userInfo }) => {
const navigation = new Navigation(page);
await navigation.checkUrlContains('#login')
await Navbar.openLoginPopup(page)
await LoginPopup.login(page, userInfo)
await navigation.endTest()
};
tests/
Naming convention: [name].e2e.spec.ts
The test file refers to a file that defines what we ultimately want to test. There may be one or multiple scenarios in a test file. You can also include model if scenario doesn't cover specific case of your test.
javascript
// @/e2e/tests/user-buy-product.spec.e2e.ts
import UserLoginScenario from '@e2e/scenarios/user-login.scenario';
import UserChooseProductScenario from "@e2e/scenarios/user-choose-product.scenario"
import UserPaymentScenario from "@e2e/scenarios/user-payment.scenario"
import Navigation from '@e2e/models/navigation';
import { getNewUser } from '@e2e/utils/user';
import { test } from '@playwright/test';
test('E2E test - User buy product', async ({ page }) => {
const userInfo = getNewUser();
const nav = new Navigation(page);
await nav.goToLoginPage();
await UserLoginScenario(page, userInfo)
await UserChooseProductScenario(page, userInfo)
await userPaymentScenario(page, userInfo)
await nav.endTest();
});