Học Playwright tiếng Việt, Cộng đồng Playwright cho người Việt

Vọc Vạch Playwright

[Vọc Playwright] Fixture

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 todoPagesettingsPage theo 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: todoPagesettingsPage
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 page fixture, tự động navigate đến baseURL:
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 browser setup vì nó được required bởi autoWorkerFixture
      • autoWorkerFixture setup vì nó là automatic worker fixture, sẽ luôn chạy trước tất cả mọi thứ.
      • beforeAll
    • first test
      • autoTestFixture setup vì automatic test fixtures luôn chạy trước khi test chạy và trước beforeEach hook.
      • page setup vì nó được require ở beforeEach hook.
      • beforeEach
      • first test chạy
      • afterEach chạy.
      • page teardown vì test-scoped fixture sẽ bị teardown ngay sau khi test hoàn thành.
      • autoTestFixture teardown, vì nó cũng là test-scoped fixture.
    • second test
      • autoTestFixture setup vì automatic test fixtures luôn chạy trước khi test chạy và trước beforeEach hook.
      • page setup vì nó được require ở beforeEach hook.
      • beforeEach
      • workerFixture setup vì nó được require bởi testFixture.
      • testFixturesetup
      • second test chạy
      • afterEach chạy.
      • testFixture teardown
      • page teardown vì test-scoped fixture sẽ bị teardown ngay sau khi test hoàn thành.
      • autoTestFixture teardown, vì nó cũng là test-scoped fixture.
    • afterAll và worker teardown:
      • afterAll
      • workerFixture teardown.
      • autoWorkerFixture teardown.
      • browser teardown.
  • Một số điều bạn thấy được dễ dàng:

    • pageautoTestFixture được setup và teardown cho mỗi test (test-scoped fixture).
    • unusedFixture sẽ không được gọi, vì không được dùng ở đâu cả.
    • testFixture phụ thuộc vào workerFixture và sẽ gọi tới setup của nó.
    • workerFixture setup đượ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).
    • autoWorkerFixture setup chạy trước beforeAll hook, nhưng autoTestFixture thì 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