이 글은 'E2E 테스트로 왜 Playwright 선택했는가?'에 이은 4번째 연재물입니다. 이번 연재물부터는 좀 더 실용적인 지식을 전달해 볼까 하는데요. 사실 소개할 기능들은 너무 많은 것 같습니다. 하지만 그중에 가장 중요하다고 생각되는 2가지 기법을 소개하고자 합니다.
목차
- 시작하기 전에 이 2개는 알고 가자!
- Authorization 자동화
- API Mocking
- 마무리
연재물
- 테스트도 종류가 있다.(링크)
- E2E 테스트 도구(tool)들 분류하기.(링크)
- E2E 테스트로 왜 Playwirght 선택했는가?(링크)
- Playwright, Auth 자동화와 API Mocking(링크)
Playwright 빌드 자동화 구축(링크)VScode를 활용한 Playwright(링크)
시작하기 전에 이 2개는 알고 가자!
E2E 테스트를 구축하면서 아래 2가지 사항은 꼭 고려하게 될겁니다.
- Authorization 자동화
- API Mocking
playwright는 어떻게 제공하고 있는지 살펴보겠습니다.
Authorization 자동화
대부분의 서비스는 로그인을 해야만 서비스를 이용할 수 있습니다. 각 테스트를 실행할 때마다 로그인부터 시작하게 되는데, 그러면 매 테스트 실행마다 로그인을 해줘야 합니다. 생명주기를 이용하면 되지라고 생각하겠지만, 매번 로그인을 시도하는 건 테스트 속도를 저하시킵니다.
딱 한 번만 로그인하면, 그 이후에는 로그인된 채로 테스트할 순 없을까요?
playwright는 이런 Authorization 자동화를 위한 기능을 제공해 줍니다. 아래 설명대로 해보세요. 물론 playwright 공식문서(링크)에 이미 다 나와있지만, 좀 더 이해하기 쉽게 설명해 보겠습니다.
테스트 실행 전 실행
playwright의 configure 에는 globalSetup이라는 옵션이 있습니다. 이 옵션에 파일의 경로를 명시해 두면 모든 테스트 실행 전에 단 한 번 이 파일을 실행시킵니다.
playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
globalSetup: require.resolve('./global-setup'),
globalTeardown: require.resolve('./global-teardown'),
});
LocalStorage에 저장
playwright에서는 LocalStorage 효과를 내는 기능을 제공해 줍니다. 바로 storageState입니다. 덕분에 테스트 전에 한번 로그인만 하면 그 이후는 테스트부터는 다시 Authorization 할 필요가 없어집니다. storageState에 이미 명시되어 있기 때문입니다.
멋있죠?! 더 멋진 건 사용법이 꽤 간단합니다. 🤩
storageState 코드 작성
global-setup.ts
import { chromium, FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
const { baseURL, storageState } = config.projects[0].use;
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(baseURL!);
await page.getByLabel('User Name').fill('user');
await page.getByLabel('Password').fill('password');
await page.getByText('Sign in').click();
await page.context().storageState({ path: storageState as string });
await browser.close();
}
export default globalSetup;
storageState configure 설정
playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
globalSetup: require.resolve('./global-setup'),
use: {
baseURL: 'http://localhost:3000/',
storageState: 'tests/auth.json',
},
});
너무 간단하죠?!
이런 점 하나하나가 playwright는 잘되어 있는 것 같습니다. Cypress는 실제 storage에서 데이터를 가져오는 api 기능은 있더라도 이렇게 설정에서 storage와 관련된 편리한 설정은 제공해주지는 않는 것 같습니다. 그렇다는 건 이런 인증 자동화를 직접 다 구현을 해야겠죠? 😖
API Mocking
테스트 코드를 짤 때 유명한 말이 있습니다.
테스트는 Mocking 이 반이다!
들어보신 적 없나요? 사실 제가 한 말입니다... 🤣
테스트를 작성할 때 실제 코드를 대신해서, 가짜 객체나 데이터를 만드는 경우가 있습니다. 이런 작업을 Mocking이라고 표현합니다. 왜 이런 작업이 필요할까요?
- 테스트를 통해 얻고자 하는 목표는 단순하고 분명해야 합니다. 🎯
- 모든 걸 테스트할 수 없습니다. 🖐🏻
- 굳이 실행 시간을 기다릴 필요가 없는 경우가 있습니다. 🕐
프런트 단에서는 Mocking을 하는 대상 중에 하나가 바로 API입니다. 프런트 입장에서는 벡엔드의 응답값만 알면 됩니다. 벡엔드가 내부적으로 오류가 있든 없든 주 관심사가 아닙니다.
'API 널 굳이 테스트하지 않을게, 난 널 믿어!'
그렇기 때문에, 어떠한 환경이든 API에서 기대하는 응답값을 보장하기 위해서 Mocking을 해야 할 때가 있습니다. E2E 테스트 시에도 예외는 없습니다.
Mocking 방법
서론이 길었는데요. 이제 playwright에서 실제로 API를 mocking 하는 방법을 살펴보겠습니다. 공식 문서에 나와있는 코드를 한번 보죠.
await page.route('https://dog.ceo/api/breeds/list/all', async route => {
const json = {
message: { 'test_breed': [] }
};
await route.fulfill({ json });
});
문서를 보면 route()는 첫 번째 인자로 넘어간 'https://dog.ceo/api/breeds/list/all' url로 가는 요청을 중간에서 가로챈다고 합니다. 결국 위 코드가 적용되는 테스트에서는 test_bread이 빈 배열로 응답이 오는 거죠.
😏 참 쉽쥬?
만약 전체 응답값 중에 일부 데이터만 변경하고자 한다면 아래와 같이 route 객체에서. fetch()로 응답값을 받아오고. fulfill()에 함께 넘겨주면 됩니다.
await page.route('https://dog.ceo/api/breeds/list/all', async route => {
const response = await route.fetch();
const json = await response.json();
json.message['big_red_dog'] = [];
await route.fulfill({ response, json });
});
Mocking 응용
이젠 실전에 적용해 보겠습니다. 상황을 정의해 보겠습니다.
- 포스팅 목록 페이지가 있다.
- 각 포스팅은 즐겨찾기 버튼이 있다.
- 즐겨찾기 버튼 클릭 시, postId와 함께 즐겨찾기 API 가 전송된다.
- 이후 로딩 되면서, 즐겨찾기 포스팅이 목록 상단에 보인다.
저는 이런 상황에서 2가지 API를 Mocking 할 겁니다.
- 즐겨찾기 API
- 포스팅 목록 API
import { test, expect } from "@playwright/test";
import { posts, bookmarked } from "./fixtures/posts";
test.describe("포스팅 목록", () => {
test("click 즐겨찾기 버튼", async ({ page }) => {
await page.locator("//button[text()='즐겨찾기']").click();
await page.route("**/api/v1/bookmarks", route => {
route.countinue();
});
await page.route("**/api/v1/posts", async route => {
const response = await route.fetch();
const json = await response.json();
json.message['posts'] = [bookmarked, ...posts];
await route.fulfill({ response, json });
})
const firstPost = await page.locator("tbody tr").first();
expect(firstPost.getByText('개발 스터디')).toBeVisible();
expect(firstPost.getByText('신청하기')).toBeVisible();
});
});
우선 즐겨찾기 API는 서버 쪽에 데이터가 굳이 가지 않도록 흘려 넘겼습니다. 각 테스트는 독립적이어야 하는데요. API를 통해 서버의 DB 데이터를 변경해버리면, 다음 테스트에 영향이 갈 수 있기 때문입니다.
대신, 포스팅 목록을 가져오는 API 를 추가로 mocking 해서, 서버에서 마치 새롭게 변경된 data를 내려준 것처럼 구현했습니다.
이게 무슨 의미가 있겠나 싶으신 분도 있을 것 같습니다.
하지만, 이 작업은 2가지 의미가 있습니다!!
- event 발생 이후 어떤 변화를 테스트하고 싶냐에 따라 의미가 달라질 수 있습니다.
- 테스트를 보고, 시나리오를 이해할 수 있습니다.
마무리
지금까지 playwright에서 Authorization 자동화와 API를 mocking 하는 방법을 살펴봤습니다. 사실 어렵지 않은 개념이지만 테스트에 대한 개념이 익숙하지 않으면 뭘 어떻게 해야 할지 모를 수 있습니다.
하지만 더 쾌적하고 독립적인 테스트 환경을 구축하는데, 소개드린 두 가지는 꼭 필요한 기법들이라고 생각합니다.
특히 mocking은 API 뿐만 아니라, npm 라이브러리도 대상이 됩니다. 물론! E2E에서는 외부 모듈까지 mocking 할 필요는 없어 보입니다.
하지만 우리가 Unit test를 진행할 때, 꼭 한 번씩 하게 되는 게 npm 라이브러리를 mocking 하는 작업입니다. 다음에는 유명한 testing runner인 jest도 살펴볼 건데요. jest에서 mocking을 어떻게 하는지, 정리해보도록 하겠습니다.
https://blog.delpuppo.net/playwright-mock-api
https://playwright.dev/docs/test-global-setup-teardown#configure-globalsetup-and-globalteardown
'Dev.' 카테고리의 다른 글
함수형 코딩 - 방어적 복사 Vs. Copy On Write (0) | 2023.07.31 |
---|---|
함수형 코딩을 읽고 (1~6챕터) (0) | 2023.06.29 |
테스트도 전략이다 (0) | 2023.05.27 |
E2E 테스트로 왜 Playwright 선택했는가? (0) | 2023.05.27 |
E2E 테스트 도구(tool)들 분류하기 (0) | 2023.05.27 |