Giới thiệu
- Playwright test được xây dựng dựa trên concept của fixture.
- Test fixture sử dụng để khởi tạo môi trường cho mỗi test. Test fixture là độc lập theo từng test.
Các fixture có sẵn (built-in)
- { page } chính là một fixture có sẵn của Playwright mà bạn thường xuyên dùng.
import { test, expect } from '@playwright/test';
test('basic test', async ({ page }) => {
await page.goto('https://playwright.dev/');
await expect(page).toHaveTitle(/Playwright/);
});
- Dưới đây là một số built-in fixture:
| Fixture | Type | Mô tả |
|---|---|---|
| page | Page | Tạo một page riêng biệt cho test. |
| context | BrowserContext | Tạo một context riêng biệt cho test. Fixture page phía trên cũng cùng context với context này. |
| browser | Browser | Browser được dùng chung giữa các test để tối ưu tài nguyên. |
| browserName | string | Tên browser đang chạy. Có thể là chromium, firefox hay webkit. |
| request | APIRequestContext | Một APIRequestContext instance độc lập. |
Không có fixture
- Không có fixture, thường chúng ta sẽ setup theo kiểu Page Object Model (POM):
- Tạo một cái Page, thêm các hàm cần thiết.
- Trước mỗi test, viết beforeEach, afterEach,…
- VD:
// todo-page.ts
import type { Page, Locator } from '@playwright/test';
export class TodoPage {
private readonly inputBox: Locator;
private readonly todoItems: Locator;
constructor(public readonly page: Page) {
this.inputBox = this.page.locator('input.new-todo');
this.todoItems = this.page.getByTestId('todo-item');
}
async goto() {
await this.page.goto('https://demo.playwright.dev/todomvc/');
}
async addToDo(text: string) {
await this.inputBox.fill(text);
await this.inputBox.press('Enter');
}
async remove(text: string) {
const todo = this.todoItems.filter({ hasText: text });
await todo.hover();
await todo.getByLabel('Delete').click();
}
async removeAll() {
while ((await this.todoItems.count()) > 0) {
await this.todoItems.first().hover();
await this.todoItems.getByLabel('Delete').first().click();
}
}
}
// todo.spec.ts
const { test } = require('@playwright/test');
const { TodoPage } = require('./todo-page');
test.describe('todo tests', () => {
let todoPage;
test.beforeEach(async ({ page }) => {
todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
});
test.afterEach(async () => {
await todoPage.removeAll();
});
test('should add an item', async () => {
await todoPage.addToDo('my item');
// ...
});
test('should remove an item', async () => {
await todoPage.remove('item1');
// ...
});
});
Có fixture
-
Fixture có rất nhiều ưu điểm so với before/after hook:
- Fixture đóng gói phần setup và teardown lại một chỗ, giúp viết dễ dàng hơn.
- Fixture có thể được sử dụng lại ở nhiều test files. Bạn chỉ cần định nghĩa một lần và viết ở tất cả các test.
- Fixture on-demand (tức là theo nhu cầu): bạn có thể định nghĩa rất nhiều fixture khác nhau, nhưng bạn chỉ cần dùng fixture mà bạn cần.
- Fixture có thể sử dụng lẫn nhau (fixture A dùng fixture B).
- Fixture linh hoạt: có thể dùng một tập hợp các fixture khác nhau dựa theo nhu cầu của môi trường, mà không ảnh hưởng tới test khác.
- Fixture có thể dùng ở trong group (describe)
-
Ví dụ, vẫn todo-page.ts như phía trên, nếu ta dùng fixture thì sẽ như sau:
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
// Extend basic test by providing a "todoPage" fixture.
const test = base.extend({
todoPage: async ({ page }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
await use(todoPage);
await todoPage.removeAll();
},
});
test('should add an item', async ({ todoPage }) => {
await todoPage.addToDo('my item');
// ...
});
test('should remove an item', async ({ todoPage }) => {
await todoPage.remove('item1');
// ...
});
Tạo mới một fixture
- Để tạo mới một fixture, bạn sử dụng
test.extend()để tạo mới một test object sẽ sử dụng nó. - Bên dưới là hai fixture
todoPagevàsettingsPagetheo Page Object Model pattern.
// todo-page.ts
import type { Page, Locator } from '@playwright/test';
export class TodoPage {
private readonly inputBox: Locator;
private readonly todoItems: Locator;
constructor(public readonly page: Page) {
this.inputBox = this.page.locator('input.new-todo');
this.todoItems = this.page.getByTestId('todo-item');
}
async goto() {
await this.page.goto('https://demo.playwright.dev/todomvc/');
}
async addToDo(text: string) {
await this.inputBox.fill(text);
await this.inputBox.press('Enter');
}
async remove(text: string) {
const todo = this.todoItems.filter({ hasText: text });
await todo.hover();
await todo.getByLabel('Delete').click();
}
async removeAll() {
while ((await this.todoItems.count()) > 0) {
await this.todoItems.first().hover();
await this.todoItems.getByLabel('Delete').first().click();
}
}
}
// settings-page.ts
import type { Page } from '@playwright/test';
export class SettingsPage {
constructor(public readonly page: Page) {
}
async switchToDarkMode() {
// ...
}
}
- Và đây là test dùng fixture trên
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
import { SettingsPage } from './settings-page';
// Declare the types of your fixtures.
type MyFixtures = {
todoPage: TodoPage;
settingsPage: SettingsPage;
};
// Extend base test by providing "todoPage" and "settingsPage".
// This new "test" can be used in multiple test files, and each of them will get the fixtures.
export const test = base.extend({
todoPage: async ({ page }, use) => {
// Set up the fixture.
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
// Use the fixture value in the test.
await use(todoPage);
// Clean up the fixture.
await todoPage.removeAll();
},
settingsPage: async ({ page }, use) => {
await use(new SettingsPage(page));
},
});
export { expect } from '@playwright/test';
- Lưu ý: những fixture bạn tạo ra thì tên nên đặt bắt đầu bằng chữ cái hoặc _, và tên chỉ bao gồm kí tự chữ, kí tự số và gạch dưới thôi nhé.
Sử dụng fixture
- Như đã nói phía trên, dùng fixture cực kì đơn giản: dùng như argument của test. Fixture cũng dùng được ở hooks và trong fixture khác luôn.
- Trong ví dụ dưới đây là sử dụng 2 fixture:
todoPagevàsettingsPage
import { test, expect } from './my-test';
test.beforeEach(async ({ settingsPage }) => {
await settingsPage.switchToDarkMode();
});
test('basic test', async ({ todoPage, page }) => {
await todoPage.addToDo('something nice');
await expect(page.getByTestId('todo-title')).toContainText(['something nice']);
});
Ghi đè fixture
- Ngoài việc thêm mới fixture, bạn cũng có thể ghi đè các fixture đã có.
- Ví dụ dưới đây ghi đè hành vi của
pagefixture, tự động navigate đếnbaseURL:
import { test as base } from '@playwright/test';
export const test = base.extend({
page: async ({ baseURL, page }, use) => {
await page.goto(baseURL);
await use(page);
},
});
Worker-scoped fixture (fixture theo từng worker)
- Fixture này sẽ chạy riêng cho từng worker.
- Ví dụ test của bạn chạy lên cần run một con backend lên chẳng hạn, thì ở mỗi worker chỉ cần run một lần thôi là được rồi.
- Thử xét ví dụ dưới đây:
- Tạo một cái account fixture.
- Ghi đè page fixture để login vào từng account cho mỗi test.
- Để sinh ra được các account độc lập, chúng ta dùng workerInfo.workerIndex.
- Chú ý cú pháp
{scope: 'worker'}để setting này hoạt động được.
import { test as base } from '@playwright/test';
type Account = {
username: string;
password: string;
};
// Note that we pass worker fixture types as a second template parameter.
export const test = base.extend({
account: [async ({ browser }, use, workerInfo) => {
// Unique username.
const username = 'user' + workerInfo.workerIndex;
const password = 'verysecure';
// Create the account with Playwright.
const page = await browser.newPage();
await page.goto('/signup');
await page.getByLabel('User Name').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByText('Sign up').click();
// Make sure everything is ok.
await expect(page.getByTestId('result')).toHaveText('Success');
// Do not forget to cleanup.
await page.close();
// Use the account value.
await use({ username, password });
}, { scope: 'worker' }],
page: async ({ page, account }, use) => {
// Sign in with our account.
const { username, password } = account;
await page.goto('/signin');
await page.getByLabel('User Name').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByText('Sign in').click();
await expect(page.getByTestId('userinfo')).toHaveText(username);
// Use signed-in page in the test.
await use(page);
},
});
export { expect } from '@playwright/test';
Automatic fixture
- Automatic fixture dùng để cài đặt cho từng test/ worker, kể cả không dùng thì vẫn chạy.
- Để config một fixture là auto, bạn thêm option `{ auto: true }
import * as debug from 'debug';
import * as fs from 'fs';
import { test as base } from '@playwright/test';
export const test = base.extend({
saveLogs: [async ({}, use, testInfo) => {
// Collecting logs during the test.
const logs = [];
debug.log = (...args) => logs.push(args.map(String).join(''));
debug.enable('myserver');
await use();
// After the test we can check whether the test passed or failed.
if (testInfo.status !== testInfo.expectedStatus) {
// outputPath() API guarantees a unique file name.
const logFile = testInfo.outputPath('logs.txt');
await fs.promises.writeFile(logFile, logs.join('\n'), 'utf8');
testInfo.attachments.push({ name: 'logs', contentType: 'text/plain', path: logFile });
}
}, { auto: true }],
});
export { expect } from '@playwright/test';
Fixture timeout
- Mặc định thì thời gian timeout của fixture được tính luôn trong thời gian của test rồi.
- Tuy nhiên, trong một số trường hợp mà fixture chậm, đặc biệt là những cái worker-scoped thì việc tách timeout ra có vẻ sẽ tiện và dễ quản lí hơn.
import { test as base, expect } from '@playwright/test';
const test = base.extend({
slowFixture: [async ({}, use) => {
// ... perform a slow operation ...
await use('hello');
}, { timeout: 60000 }]
});
test('example test', async ({ slowFixture }) => {
// ...
});
Fixture options
- Phần này có vẻ hơi phức tạp, chắc để dịch xong bài param test thì mình quay lại dịch
- TODO
Thứ tự chạy (execution order)
- Mỗi fixture đều có setup và teardown phase rõ ràng, cách nhau bởi
await use()- Tức là: trước use() là setup, còn sau use là teardown.
- Setup được chạy trước test hook, còn teardown chạy sau test hook.
- Fixture dựa vào bộ quy tắc sau để xác định thứ tự thực thi:
- Khi fixture A phụ thuộc vào fixture B: B sẽ chạy setup trước A và teardown sau A.
- Các fixture không phải tự động thì sẽ chạy khi nào được call.
- Test-scoped fixture (tức là fixture thông thường, đi theo từng test) thì sẽ chạy teardown sau khi mỗi test chạy xong. Còn worker-scoped fixture thì sẽ chỉ chạy teardown khi worker shutdown thôi.
- Cùng xét ví dụ sau:
import { test as base } from '@playwright/test';
const test = base.extend({
workerFixture: [async ({ browser }) => {
// workerFixture setup...
await use('workerFixture');
// workerFixture teardown...
}, { scope: 'worker' }],
autoWorkerFixture: [async ({ browser }) => {
// autoWorkerFixture setup...
await use('autoWorkerFixture');
// autoWorkerFixture teardown...
}, { scope: 'worker', auto: true }],
testFixture: [async ({ page, workerFixture }) => {
// testFixture setup...
await use('testFixture');
// testFixture teardown...
}, { scope: 'test' }],
autoTestFixture: [async () => {
// autoTestFixture setup...
await use('autoTestFixture');
// autoTestFixture teardown...
}, { scope: 'test', auto: true }],
unusedFixture: [async ({ page }) => {
// unusedFixture setup...
await use('unusedFixture');
// unusedFixture teardown...
}, { scope: 'test' }],
});
test.beforeAll(async () => { /* ... */ });
test.beforeEach(async ({ page }) => { /* ... */ });
test('first test', async ({ page }) => { /* ... */ });
test('second test', async ({ testFixture }) => { /* ... */ });
test.afterEach(async () => { /* ... */ });
test.afterAll(async () => { /* ... */ });
-
Giả sử test pass hết, không có lỗi gì xảy ra, thế thì thứ tự thực hiện sẽ như sau:
- Worker setup và beforeAll
- Chạy
browsersetup vì nó được required bởiautoWorkerFixture autoWorkerFixturesetup vì nó là automatic worker fixture, sẽ luôn chạy trước tất cả mọi thứ.beforeAll
- Chạy
first testautoTestFixturesetup vì automatic test fixtures luôn chạy trước khi test chạy và trướcbeforeEachhook.pagesetup vì nó được require ởbeforeEachhook.beforeEachfirst testchạyafterEachchạy.pageteardown vì test-scoped fixture sẽ bị teardown ngay sau khi test hoàn thành.autoTestFixtureteardown, vì nó cũng là test-scoped fixture.
second testautoTestFixturesetup vì automatic test fixtures luôn chạy trước khi test chạy và trướcbeforeEachhook.pagesetup vì nó được require ởbeforeEachhook.beforeEachworkerFixturesetup vì nó được require bởitestFixture.testFixturesetupsecond testchạyafterEachchạy.testFixtureteardownpageteardown vì test-scoped fixture sẽ bị teardown ngay sau khi test hoàn thành.autoTestFixtureteardown, vì nó cũng là test-scoped fixture.
afterAllvà worker teardown:afterAllworkerFixtureteardown.autoWorkerFixtureteardown.browserteardown.
- Worker setup và beforeAll
-
Một số điều bạn thấy được dễ dàng:
pagevàautoTestFixtuređược setup và teardown cho mỗi test (test-scoped fixture).unusedFixturesẽ không được gọi, vì không được dùng ở đâu cả.testFixturephụ thuộc vàoworkerFixturevà sẽ gọi tới setup của nó.workerFixturesetup được gọi trước khi chạy test second, nhưng teardown thì chỉ chạy một lần khi worker shutdown (worker-scoped fixture).autoWorkerFixturesetup chạy trướcbeforeAllhook, nhưngautoTestFixturethì không.
Kết hợp fixture từ nhiều modules
- Bạn có thể merge test fixture từ nhiều test files hoặc modules.
// fixtures.ts
import { mergeTests } from '@playwright/test';
import { test as dbTest } from 'database-test-utils';
import { test as a11yTest } from 'a11y-test-utils';
export const test = mergeTests(dbTest, a11yTest);
// test.spec.ts
import { test } from './fixtures';
test('passes', async ({ database, page, a11y }) => {
// use database and a11y fixtures.
});
Trả lời