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

Vọc Vạch Playwright

Playwright fixture – Phần 1: định nghĩa, built-in fixture, custom fixture

1. Giới thiệu

Playwright Fixture là gì?

Playwright Fixture là một cơ chế mạnh mẽ giúp tự động hóa việc setup và teardown các điều kiện cần thiết cho test. Fixtures giúp bạn:

  • Tái sử dụng code setup/teardown giữa các test
  • Quản lý dependency một cách rõ ràng
  • Đảm bảo isolation giữa các test
  • Tối ưu performance bằng cách chia sẻ resource khi phù hợp

Tại sao cần sử dụng Fixture?

Trong testing, chúng ta thường phải:

  • Setup môi trường test (database, authentication, mock data)
  • Cleanup sau khi test chạy xong
  • Đảm bảo mỗi test chạy độc lập

Không có fixture, code sẽ bị duplicate và khó maintain:

// Không dùng fixture - Code bị lặp lại
test('test 1', async ({ page }) => {
  // Setup
  await page.goto('https://example.com');
  await page.fill('#username', 'testuser');
  await page.fill('#password', 'password');
  await page.click('#login');

  // Test logic
  await expect(page.locator('.dashboard')).toBeVisible();

  // Cleanup
  await page.click('#logout');
});

test('test 2', async ({ page }) => {
  // Lặp lại setup giống hệt
  await page.goto('https://example.com');
  await page.fill('#username', 'testuser');
  await page.fill('#password', 'password');
  await page.click('#login');

  // Test logic khác
  await page.click('.settings');

  // Cleanup
  await page.click('#logout');
});

So sánh với các Framework khác

FrameworkSetup/TeardownDependency InjectionScope Control
PlaywrightFixtures✓ Built-in✓ Flexible
JestbeforeEach/afterEach✗ Manual✗ Limited
Mochabefore/after hooks✗ Manual✗ Limited
CypressbeforeEach/afterEach✗ Manual✗ Limited

2. Khái niệm cơ bản về Fixture

Định nghĩa Fixture

Fixture là một function đặc biệt:

  • Chạy trước test để setup môi trường
  • Cung cấp giá trị cho test sử dụng
  • Tự động cleanup sau khi test kết thúc
// Fixture cơ bản
const test = base.extend({
  // Fixture name: userData
  userData: async ({}, use) => {
    // Setup: Tạo test data
    const data = {
      username: 'testuser',
      email: 'test@example.com'
    };

    // Use: Cung cấp data cho test
    await use(data);

    // Teardown: Cleanup (tự động chạy sau test)
    console.log('Cleaning up user data...');
  }
});

Lifecycle của Fixture

Ví dụ chi tiết về lifecycle:

const test = base.extend({
  dbConnection: async ({}, use) => {
    console.log('1. Setup: Connecting to database...');
    const db = await connectToDatabase();

    console.log('2. Providing db connection to test');
    await use(db);

    console.log('3. Teardown: Closing database connection...');
    await db.close();
  }
});

test('database test', async ({ dbConnection }) => {
  console.log('Test running with db connection');
  const users = await dbConnection.query('SELECT * FROM users');
  expect(users).toHaveLength(5);
});

// Output:
// 1. Setup: Connecting to database...
// 2. Providing db connection to test
// Test running with db connection
// 3. Teardown: Closing database connection...

Phân loại Fixture

Built-in Fixtures

Playwright cung cấp sẵn nhiều fixtures hữu ích:

test('using built-in fixtures', async ({ 
  page,           // Browser page
  context,        // Browser context
  browser,        // Browser instance
  browserName,    // 'chromium' | 'firefox' | 'webkit'
  request         // API request context
}) => {
  // Sử dụng trực tiếp
  await page.goto('https://example.com');
});

Custom Fixtures

Fixtures do bạn tự định nghĩa:

const test = base.extend({
  // Custom fixture
  authenticatedPage: async ({ page }, use) => {
    await page.goto('/login');
    await page.fill('#username', 'admin');
    await page.fill('#password', 'secret');
    await page.click('#login-btn');

    await use(page);

    await page.click('#logout');
  }
});

3. Built-in Fixtures có sẵn

Page Fixture

Fixture phổ biến nhất, đại diện cho một browser page:

test('page fixture example', async ({ page }) => {
  // Navigation
  await page.goto('https://playwright.dev');

  // Interactions
  await page.click('text=Get started');
  await page.fill('#search', 'fixtures');

  // Assertions
  await expect(page).toHaveTitle(/Playwright/);
  await expect(page.locator('.results')).toBeVisible();

  // Screenshot
  await page.screenshot({ path: 'example.png' });
});

Context Fixture

Browser context cho phép tạo isolated browser session:

test('context fixture example', async ({ context, page }) => {
  // Set cookies cho toàn bộ context
  await context.addCookies([{
    name: 'session',
    value: 'abc123',
    domain: 'example.com',
    path: '/'
  }]);

  // Set extra HTTP headers
  await context.setExtraHTTPHeaders({
    'X-API-Key': 'my-api-key'
  });

  // Tạo multiple pages (nhiều tab) trong cùng context
  const page1 = await context.newPage();
  const page2 = await context.newPage();

  // Pages share cookies và storage
  await page1.goto('https://example.com');
  await page2.goto('https://example.com'); // Đã có cookie
});

Browser Fixture

Quản lý browser instance:

test('browser fixture example', async ({ browser }) => {
  // Tạo multiple contexts
  const userContext = await browser.newContext({
    userAgent: 'Custom User Agent'
  });
  const adminContext = await browser.newContext({
    httpCredentials: {
      username: 'admin',
      password: 'password'
    }
  });

  // Mỗi context độc lập
  const userPage = await userContext.newPage();
  const adminPage = await adminContext.newPage();

  // Cleanup
  await userContext.close();
  await adminContext.close();
});

Request Fixture

Cho API testing:

test('request fixture example', async ({ request }) => {
  // GET request
  const response = await request.get('https://api.example.com/users');
  expect(response.ok()).toBeTruthy();

  const users = await response.json();
  expect(users).toHaveLength(10);

  // POST request
  const newUser = await request.post('https://api.example.com/users', {
    data: {
      name: 'John Doe',
      email: 'john@example.com'
    }
  });

  expect(newUser.status()).toBe(201);
});

Scope của Fixtures

// Test scope (default) - Mới cho mỗi test
test.extend({
  testData: async ({}, use) => {
    const data = generateTestData();
    await use(data);
  }
});

// Worker scope - Shared trong worker
test.extend({
  workerDatabase: [async ({}, use) => {
    const db = await setupDatabase();
    await use(db);
    await db.teardown();
  }, { scope: 'worker' }]
});

// Auto fixture - Chạy tự động
test.extend({
  autoFixture: [async ({}, use) => {
    console.log('This runs automatically');
    await use();
  }, { auto: true }]
});

4. Tạo Custom Fixture

Cú pháp cơ bản với test.extend()

import { test as base } from '@playwright/test';

// Định nghĩa types cho fixtures
type MyFixtures = {
  todoPage: Page;
  testData: {
    username: string;
    password: string;
  };
};

// Extend base test với custom fixtures
const test = base.extend<MyFixtures>({
  // Simple fixture
  testData: async ({}, use) => {
    await use({
      username: 'testuser',
      password: 'Test@123'
    });
  },

  // Fixture with dependencies
  todoPage: async ({ page, testData }, use) => {
    await page.goto('/login');
    await page.fill('#username', testData.username);
    await page.fill('#password', testData.password);
    await page.click('#login');

    await page.goto('/todo');
    await use(page);
  }
});

Setup và Cleanup Logic

const test = base.extend({
  database: async ({}, use) => {
    // Setup
    console.log('Setting up database...');
    const db = new TestDatabase();
    await db.connect();
    await db.seed(); // Insert test data

    // Provide to test
    await use(db);

    // Cleanup - ALWAYS runs, even if test fails
    console.log('Cleaning up database...');
    await db.clean();
    await db.disconnect();
  }
});

test('database operations', async ({ database }) => {
  const user = await database.createUser({
    name: 'John',
    email: 'john@example.com'
  });

  expect(user.id).toBeDefined();
  // Cleanup tự động chạy sau test
});

Dependency Injection giữa các Fixtures

const test = base.extend({
  // Base fixture
  apiClient: async ({}, use) => {
    const client = new APIClient({
      baseURL: 'https://api.example.com'
    });
    await use(client);
  },

  // Fixture phụ thuộc vào apiClient
  authenticatedClient: async ({ apiClient }, use) => {
    const token = await apiClient.login('admin', 'password');
    apiClient.setAuthToken(token);
    await use(apiClient);
  },

  // Fixture phụ thuộc vào nhiều fixtures
  testEnvironment: async ({ page, authenticatedClient }, use) => {
    // Setup UI với authenticated state
    await page.goto('/');
    await page.evaluate((token) => {
      localStorage.setItem('authToken', token);
    }, authenticatedClient.token);

    await use({
      page,
      api: authenticatedClient
    });
  }
});

Fixture với Parameters

// Option 1: Factory function
const test = base.extend({
  createUser: async ({}, use) => {
    const users = [];

    // Factory function
    await use(async (userData) => {
      const user = await api.createUser(userData);
      users.push(user);
      return user;
    });

    // Cleanup all created users
    for (const user of users) {
      await api.deleteUser(user.id);
    }
  }
});

test('create multiple users', async ({ createUser }) => {
  const user1 = await createUser({ name: 'User 1' });
  const user2 = await createUser({ name: 'User 2' });

  expect(user1.id).not.toBe(user2.id);
});

// Option 2: Configuration fixture
const test = base.extend({
  serverURL: ['https://staging.example.com', { option: true }],

  apiClient: async ({ serverURL }, use) => {
    const client = new APIClient({ baseURL: serverURL });
    await use(client);
  }
});

// Override in config or test
test.use({ serverURL: 'https://production.example.com' });

Trả lời