6. Playwright Fixture
🎈 픽스처 소개
Playwright는 일반적인 테스트 픽스처의 개념을 기반으로 하고 있다. 픽스처는 각 테스트의 환경을 설정하는데 사용되고, 테스트에 필요한 모든 것들을 제공한다. 테스트에 필요하지 않은 것에 대해서는 제공하지 않는다.
또한 픽스처는 테스트간에 격리되기 때문에 테스트내에서 픽스처의 수정이 다른 테스트에 영향을 주는 부분은 고민하지 않아도 된다.
픽스처를 사용하면 일반적인 설정대신 테스트의 의미에 따라 그룹화할 수 있다.
✨ Test Fixture는 테스트를 수행하기 전에 필요한 상태나 환경을 설정하는 것을 의미한다. 이를 통해 일관된 테스트를 할 수 있도록 해준다. 이 개념을 숙지하자!!
🛒 내장 픽스처
Fixture | Type | Description |
page | Page | 테스트 실행을 위한 격리된 페이지. 테스트 시 가장 많이 사용 되는 픽스처이다. (가장 많이 사용되는 픽스처이다.) |
context | BrowserContext | 테스트 실행을 위한 격리된 컨텍스트. 페이지는 컨텍스트에 포함되어 있다. |
browser | Browser | 리소스 최적화를 위해 테스트 간에 브라우저는 공유한다. |
browserName | string | 현재 실행 중인 테스트의 브라우저 명. chromium, firefox, webkit |
request | APIRequestContext | 테스트 실행을 위한 격리된 APIRequestContext 객체 |
아래와 같이 픽스처들은 test에 전달된 콜백함수의 파라미터 객체에 담겨져 있다.
import { test, expect } from '@playwright/test';
test('basic test', async ({ page }) => {
await page.goto('https://playwright.dev');
await expect(page).toHaveTitle(/Playwright/);
});
📯 픽스처 사용 유무에 따른 비교
아래 코드는 Todo 리스트 페이지를 추상화한 클래스이다. 아래 픽스처 사용 유무에 따라 아래 클래스를 어떻게 사용하는지를 비교해볼 예정이다.
Todo 리스트 클래스
import type { Page, Locator } from '@playwright/test';
export class TodoPage {
private readonly input: Locator;
private readonly todoList: Locator;
constructor(public readonly page: Page) {
this.input = this.page.locator('input');
this.todoList = this.page.getByRole('list');
}
async goto() {
await this.page.goto('http://localhost:3000');
}
async addToDo(text: string) {
await this.input.fill(text);
await this.input.press('Enter');
}
async remove(text: string) {
const todo = this.todoList.filter({ hasText: text });
await todo.hover();
await todo.getByLabel('삭제').click();
}
async removeAll() {
while ((await this.todoList.count()) > 0) {
await this.todoList.first().hover();
await this.todoList.getByLabel('삭제').first().click();
}
}
}
픽스처 사용 - ❎
픽스처를 사용하지 않는 다면 beforeEach에서 테스트 전 사전 준비되어야할 공통적인 요소를 정의한다. TodoPage로 정의된 객체를 생성하고 미리 몇개의 아이템을 추가한다.
테스트가 종료 된 후에는 afterEach에서 사용한 리소스를 모두 초기화한다.
import { test, expect } from '@playwright/test'
import { TodoPage } from '../utils/todo';
test.describe('Todo', () => {
let todo;
test.beforeEach(async ({ page }) => {
todo = new TodoPage(page);
await todo.goto();
await todo.addToDo('아이템1');
await todo.addToDo('아이템2');
});
test.afterEach(async () => {
await todo.removeAll();
});
test('should add todo', async () => {
await todo.addToDo('아이템3');
await expect(todo.todoList).toHaveText('아이템3');
});
test('should remove todo', async () => {
await todo.remove('아이템1');
await expect(todo.todoList).not.toHaveText('아이템1');
});
});
✨ beforeEach, afterEach 등에서는 픽스처를 받아올 수 있다. 하지만 describe에 전달 된 콜백함수에서는 픽스처를 받아올 수 없다.
픽스처 사용 - ✅
import { test as base, expect } from '@playwright/test'
import { TodoPage } from '../utils/todo';
const test = base.extend({
todoPage: async ({ page }, use) => {
const todo = new TodoPage(page);
await todo.goto();
await todo.addToDo('아이템1');
await todo.addToDo('아이템2');
await use(todo);
await todo.removeAll();
},
});
test('should add todo', async () => {
await todo.addToDo('아이템3');
await expect(todo.todoList).toHaveText('아이템3');
});
test('should remove todo', async () => {
await todo.remove('아이템1');
await expect(todo.todoList).not.toHaveText('아이템1');
});
현재 위의 예시만 봐서는 코드의 양이 비슷해서 왜 사용해야하는지 알기 힘들 수 있다.
다음은 before/after 훅에 비해 픽스처가 가지는 장점이다.
- 픽스처에서는 setup과 teardown을 하나의 함수내에서 모두 처리 가능하다.
- 위의 코드를 보면 use 함수 호출 전이 setup, use 호출 후가 teardown 이다.
- 픽스처는 한벙 정의 해놓으면 재사용이 가능하다.
- before/after를 사용하면 사용할 각 파일마다 코드를 추가해야 한다.
- 픽스처는 원하는 만큼 만들어 놓고 사용할 수 있다. 픽스처를 사용하는 테스트에 한해서만 셋업을 실행한다.
- before/after는 정의 해놓은 위치에 따라 실제 setup할 필요가 없는 테스트에도 실행하게 된다.
- 적절한 위치에 두면 필요한 테스트에서만 setup을 할 수 있지만 하지만 구조상 그렇게 하기도 힘들때도 있다.
- 픽스처를 사용하면 좀 더 빠른 테스트 실행이 된다.
- before/after는 정의 해놓은 위치에 따라 실제 setup할 필요가 없는 테스트에도 실행하게 된다.
- 픽스처는 복잡한 동작을 제공할 수 있도록 조합이 가능하다.
- 픽스처는 그룹화를 간단하게 정의할 수 있다.
아래와 같이 로그인 처리를 픽스처로 등록해 놓으면 복잡한 동작을 간단하게 사용 가능하다.
const test = base.extend({
loggedInPage: async ({ page }, use) => {
await page.goto('/login');
await page.fill('#username', 'user');
await page.fill('#password', 'pass');
await page.click('#submit');
await use(page);
},
});
✨ before/after 훅이 필요 없는 건 아니다. 너무 작은 범위까지 픽스처를 만들면 비용이 더 들 수 있다. 상황에 따라 선택하도록 하자.
👓 픽스처 만들기
픽스처는 test.extend()를 사용해서 만들 수 있다. 아래와 같이 간단하게 만들 수 있다.
import { test as base } from '@playwright/test';
export const test = base.extend({
todoPage: async ({ page }, use) {
// setup
await use(Fixture);
// teardown
}
});
✨ 픽스처의 이름은 문자 또는 _로 시작해야 한다.
픽스처는 테스트에서 정의 된 이름을 파라미터에서 선언하기만 해도 호출이 된다. 아래와 같이 page를 테스트 내부에서 사용하진 않더라도 아래와 같이 page를 선언만 해도 픽스처가 호출된다.
아래와 같이 작성했을 때 ‘my test’ 테스트가 호출되면 콘솔에 1이 찍히게 된다.
import { test as base } from '@playwright/test'
const test = base.extend({
test: async ({ page }, use) => {
console.log(1);
await use(page);
}
});
test('my test', async ({ test }) => {
});
✨ 즉, test를 선언안하면 픽스처도 호출이 되지 않는다. 이를 통해 before/after 훅 사용보다 빠른 시간을 보장한다.
👔 자동설정
기본적으로는 테스트에서 픽스처를 선언하지 않으면 호출이 되지 않는 시스템이다. 하지만 항상 실행이 되도록 하고 싶은 경우도 있을 것이다. auto 설정을 하면 된다.
아래와 같이 픽스처에 배열 형태로 작성 후 두 번째 아이템에 auto 옵션을 전달하면 된다.
const test = base.extend({
test: [async ({ page }, use) => {
console.log(1);
await use(page);
}, { auto: true }]
});
이 옵션을 활용해서 global before/after 훅처럼 사용이 가능하다.
auto는 모든 테스트에서 실행되고 use호출하면 테스트가 실행되기 때문에 use 호출전이 before, use 호출 완료 되면 after를 구성할 수 있다.
export const test = base.extend<{ forEach: void }>({
forEach: [async ({}, use) => {
// before
await use();
// after
}, { auto: true }]
});
beforeAll/afterAll을 구성하려면 옵션으로 scope: 'worker'를 사용하면 된다.
export const test = base.extend<{ forEach: void }>({
forEach: [async ({}, use) => {
// before
await use();
// after
}, { scope: 'worker', auto: true }]
});
🩳 타임아웃
기본적으로 픽스처는 테스트와 타임아웃을 공유한다. 특정 픽스처에서는 더 많은 시간을 필요로 할 수도 있다. 그런 경우를 위해 픽스처마다 개별 타임아웃을 설정할 수 있다.
const test = base.extend({
slowFixture: [async ({}, use) => {
await use('hello');
}, { timeout: 60000 }]
});
🧣 픽스처 옵션
픽스처의 옵션을 사용하면 정적인 데이터를 구성하기 쉬워진다.
아래 처럼 option: true 설정만 추가하면 된다. 일반 픽스처처럼 접근해서 사용하면 된다.
const test = base.extend({
defaultItem: ['Default Item', { option: true }],
page: async ({ defaultItem }, use) => {
// defaultItem를 사용한 코드.
// defaultItem => Default Item
}
});
playwright.config.ts 에 아래와 같이 구성할 수 도 있다.
import { defineConfig } from '@playwright/test';
import type { MyOptions } from './my-test';
export default defineConfig<MyOptions>({
projects: [
{
name: 'shopping',
use: { defaultItem: 'Buy milk' },
},
{
name: 'wellbeing',
use: { defaultItem: 'Exercise!' },
},
]
});
- projects에 구성하더라도 기본적으로 test에 기본 값은 추가되어 있어야 한다. 안하면 오류 발생!
- 왜냐하면 projects에 구성하는 건 기본값에 오버라이딩 개념이기 때문이다.
- projects에 구성해서 개별 project마다 원하는 값을 설정 가능하다.
- 실행하는 project 마다 다른 값으로 테스트해볼 수 있다.
개별 테스트 파일에서 test.use를 사용해서오버라이딩이 가능하다. 이때 test.use를 어느 위치에 두는지에 따라 스코프가 결정된다. 파일의 전역 위치이면 파일 전체에, describe 내부이면 해당 describe에 존재하는 모든 테스트에 적용된다.
test.use({ defaultItem: '새로운 아이템' });
test('has title', async ({ page, defaultItem }) => {
await page.goto('http://127.0.0.1:5500/index.html');
console.log(defaultItem); // 새로운 아이템
});
배열인 경우 아래와 같이 scope를 사용하라고 되어있는데 사실 다른 점은 모르겠다. 확인이 필요할 것 같다.
const test = base.extend({
defaultItem: [[], { option: true }],
});
// 테스트.spec.ts
test.use({
defaultItem: [['test', 'test1'], { scope: 'test' }],
});
픽스처의 실행 순서는 링크를 참조하자.
Fixtures | Playwright
Introduction
playwright.dev
👜 픽스처 머지
개별 파일로 만들어 둔 픽스처들을 하나로 합칠 수 있다.
import { test as dbTest } from 'database-test';
import { test as apiTest } from 'api-test';
export const test = mergeTests(dbTest, apiTest);
- 파일별로 필요한 픽스처로만 구성해서 개별적으로 사용할 수 있다.
- 다른 모듈에 있는 픽스처도 함께 사용하고 싶은 경우 mergeTests를 통해 한번에 합쳐서 사용도 가능하다.
✨ 1.39.0 버전 이후부터 추가되었다.
🎨 정리
아직까지는 개념이 명확하게 들어오는 건 아니지만 픽스처를 왜 사용해야하는지에 대해 정리해보았다.
- 테스트를 하기 위한 환경을 구성해서 테스트 코드 작성을 용이하게 해준다.
- 정적 테이터 추가, Locator, 복잡한 사전 준비를 미리 셋팅 가능
- 미리 정의된 픽스처를 사용함으로써 일관 된 테스트를 진행할 수 있게 해준다.