테스트와 테스트 하는 방법에 대해 알아보자.
웹 브라우저 기반의 E2E 테스트 자동화 도구, Playwright 를 알아보자.
E2E 테스트 자동화 도구로 유명한 셀레늄이 있다.
셀레늄 등장 이후에 셀레늄보다 가볍고 Headless Browser를 이용하는 팬덤JS
가 나온다. 팬덤JS는 브라우저 중에서 크로미엄 기반이다. Headless Browser는 브라우저 창을 열지 않고 백그라운드에서 웹 페이지를 렌더링할 수 있는 기능이다.
그러다 Headless Chrome이 나온다. Chrome 자체가 Headless를 직접 지원하게 된다. 그래서 Headlss Chrome를 기반으로 한 Puppeteer
의 등장하게 되고 팬덤JS는 개발을 중단하게 된다.
이후에 Puppeteer 개발자들은 Playwright
를 만들게 된다. Playwright은 Chrome 뿐만 아니라 많은 Browser에서 Headless를 지원한다.
그래서 Puppeteer와 Playwright를 API가 거의 똑같다.
웹 브라우저 기반 E2E 테스트 자동화 도구이다.
Headless Chrome을 기반으로 한 Puppeteer를 계승하면서, 더 많은 웹 브라우저를 지원한다.
npm i -D @playwright/test eslint-plugin-playwright
Playwright은 원래 테스팅 도구가 아니다. 테스트를 지원하지만 정확히는 API를 제공한다. 테스트는 테스트 러너를 제공해줘서 가능하게 한다.
우리는 Playwright을 이용해서 테스트를 할 것이다.
그래서 다음과 같이 설치한다.
npm i -D @playwright/test
원래는 다음과 같이 설치해야 한다.
npm init playwright@latest
하지만 전자가 후자에 대한 의존성을 다 물고 있기 때문에 전자로도 설치가 가능하다.문제가 되는
eslint-plugin
도 같이 설치해야 한다.둘 다 개발 환경에서만 사용한다.
E2E 테스트는 실제 테스트를 하는 것이니, Express 서버를 만들었다면 띄워야 한다.
playwright.config.ts
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
testDir: './tests',
retries: 0,
use: {
channel: 'chrome',
baseURL: 'http://localhost:8080',
headless: !!process.env.CI,
screenshot: 'only-on-failure',
},
};
export default config;
'./tests',
테스트 파일을 넣을 폴더를 지정한다.
channel: 'chrome',
Playwright에서 channel은 사용할 브라우저 엔진을 의미한다.
크로미엄을 안 쓰고 크롬을 쓸 거라고 잡아준다.
baseURL: 'http://localhost:8080',
테스트할 웹 서버의 기본 URL을 설정한다.
headless: !!process.env.CI,
CI 라는 환경 변수의 값의 존재 여부에 따라서 브라우저를 Headless 모드로 실행할지 여부를 결정한다.
CI 환경 변수가 존재하면 Headless 가 ture로 설정된다.
screenshot: 'only-on-failure',
screenshot은 테스트 중에 스크린샷을 언제 저장할지 결정한다.
only-on-failure은 테스트가 실패한 경우에만 스크린샷을 저장하는 설정이다.
CI/CD 환경은 소프트웨어 개발 및 배포를 더욱 효율적이고 빠르게 만들어준다.
CI/CD 를 파이프라인을 통해 코드가 자동으로 빌드되고 테스트 된다. 운영 환경으로 배포되므로 반복적인 수동 프로세스를 줄일 수 있다.
CI (Continuous Integration) 지속적 통합
새로운 코드를 공유 코드 베이스에 빈번하게 통합하는 프로세스이다.
작은 단위의 변경 사항을 주기적으로 공유 코드 베이스에 통합할 수 있다.
자동화 빌드, 테스트, 코드 리뷰 등을 포함한다.
CD (Continuous Deployment) 지속적 배포
통합된 코드가 테스트를 통과하면 자동으로 실제 운영 환경에 배포된다. 수동으로 배포도 가능하다.
GitHub 저장소에서 코드 개발부터 배포까지 전체 CI/CD 프로세스를 자동화하고 관리하기 위한 강력한 도구 중 하나이다.
GitHib 플랫폼의 통합으로 사용자 경험을 향상시키고 개발자들의 생산성을 높여준다.
EsLint 를 잡아준다.
tests/.eslintrc.js
module.exports = {
env: {
jest: false,
},
extends: ['plugin:playwright/playwright-test'],
rules: {
'import/no-extraneous-dependencies': 'off',
},
};
jest: false,
tests 안에서 jest를 쓸 건 아니라서 fasle 로 잡는다.
'import/no-extraneous-dependencies': 'off',
개발 용도로 설치했지만, 테스트 파일 밖에서 사용했을 때 발생하는 ESLint 에러를 잡아준다.
원하는 테스트를 써준다.
tests/home.spec.ts
import { test, expect } from '@playwright/test';
test('Show all products', async ({ page }) => {
await page.goto('/');
await expect(page.getByText('Apple');).toBeVisible();
await expect(page.getByText('Grape');).toBeHidden();
});
name.spec.ts 처럼 test가 아닌 spec으로 쓰는 스타일은 보통 BDD 스타일이다. 하지만 test나 spec 이나 상관없다.
import { test, expect } from '@playwright/test';
jest 의 test와 expect와 다르다. 그래서 일부로 겹치지 않도록 jest를 끈 것이다.데이터를 못 찾으면, 타임아웃 될 때까지 찾는다.
tests/home.spec.ts
import { test, expect } from '@playwright/test';
test('Filter products', async ({ page }) => {
await page.goto('/');
// Apple 이 보인다.
await expect(page.getByText('Apple')).toBeVisible();
// Search 로 찾은 레이블과 연결된 Input 을 찾는다.
const searchInput = page.getByLabel('Search');
// Input을 밀어버리고 Input에 a를 넣는다.
await searchInput.fill('a');
// Apple은 여전히 보인다.
await expect(page.getByText('Apple')).toBeVisible();
// Input을 밀어버리고 Input에 aa를 넣는다.
await searchInput.fill('aa');
// Apple이 안 보인다.
await expect(page.getByText('Apple')).toBeHidden();
});
tests/home.spec.ts
test('Click the “Increase” button', async ({ page }) => {
await page.goto('/');
await page.getByText('Increase').click();
await page.getByText('Increase').click();
await page.getByText('Increase').click();
await page.getByText('Increase').click();
await page.getByText('Increase').click();
await page.getByText('Increase').click();
await expect(page.getByText(6)).toBeVisible();
});
위 코드를 아래 코드로 간단하게 바꿀 수 있다.
test('Click the “Increase” button', async ({ page }) => {
await page.goto('/');
const count = 13_000;
await Promise.all((
[...Array(count)].map(async () => {
await page.getByText('Increase').click();
})
));
await expect(page.getByText(`${count}`)).toBeVisible();
});
Promise.all 를 사용해서 click 이 13000번 전부될 때까지 기다리게 한다.
tests/home.spec.ts
import { test, expect } from '@playwright/test';
test('Filter products', async ({ page }) => {
await page.goto('/');
// Apple 이 보인다.
await expect(page.getByText('Apple')).toBeVisible();
// Search 로 찾은 레이블과 연결된 Input 을 찾는다.
const searchInput = page.getByLabel('Search');
// Input을 밀어버리고 Input에 a를 넣는다.
await searchInput.fill('a');
// Apple은 여전히 보인다.
await expect(page.getByText('Apple')).toBeVisible();
// Input을 밀어버리고 Input에 aa를 넣는다.
await searchInput.fill('aa');
// Apple이 안 보인다.
await expect(page.getByText('Apple')).toBeHidden();
});
test('Click the “Increase” button', async ({ page }) => {
await page.goto('/');
const count = 13;
await Promise.all((
[...Array(count)].map(async () => {
await page.getByText('Increase').click();
})
));
await expect(page.getByText(`${count}`)).toBeVisible();
});
npx playwright test
다음과 같이 환경 변수를 직접 써줄 수 있다.
CI=true npx playwright test
true가 아닌 dddd 같은 문자열을 써도 true로 처리된다.
이유는 playwright.config.js 파일에서 headless: !!process.env.CI 로 잡았기 때문에
!!"dddd"는 암묵적 타입 변환이 되기 때문이다.어쨌든 CI=true 로 잡게 되면 Headless 로 처리된다. 브라우저가 시선을 땡기는게 정신사나우면 이렇게 해도 괜찮다.
test-results 안에 이미지가 많아지면 node_modules 처럼 github에 올리면 지옥이 된다.
.gitignore 파일로 막아주자.
.gitignore
/test-results/
인간 친화적인 E2E 테스팅 도구이다.
위에서 작성했던 테스트 코드들은 섬세한 것에 대한 모든 처리를 할 수 있다. 하지만 코드가 잘 읽히는 편은 아니다. 이것보다 조금 더 단순하면서 사람들이 읽기 좋은 게 필요할 때가 있다. 이때 많이 사용하는 것이 CodeceptJS
이다.
간단한 서비스들은 CodeceptJS
로 커버가 가능하다. 만약 조금 더 복잡하게 만들고 싶다면 CodeceptJS
이 뒤에서 Playwright
을 쓰도록 지원할 수 있다.
CodeceptJS 가 Playwright 보다 코드를 간단하게 쓸 수 있으며 함께 일하는 사람이 이해하기도 좋다.
하지만 복잡하고 디테일한 처리를 하고 싶을 때는 CodeceptJS
으로 충분하지 않다. 예를 들어, 온라인 포토샵 을 테스트 하기에는 곤란하다.
CodeceptJS
Feature('My First Test');
Scenario('test something', ({ I }) => {
I.amOnPage('https://github.com');
I.see('GitHub');
});
주의해야 할 점이 있다. React Testing 을 많이 하는게 중요한 게 아니다. UI 뒤에서 돌아가는 중요한 로직들에 대한 테스트가 중요하다. 그래서 가능하면 UI는 관심사의 분리를 통해서 비즈니스 로직하고 딱 나눠야 한다. UI는 자주 바뀐다. 테스트를 빡빡하게 잡으면 UI를 조금만 바꿔도 펑펑 터진다. 그리고 UI는 간단하게 버튼을 누르니까 '어떻게 연결이 된다' 정도의 테스트만 한다.
하지만 TextField
같은 범용 컴포넌트, 여러 곳에서 가져다 쓰는 컴포넌트는 테스트를 잡는 게 맞다. 고쳤을 때 펑펑 여러 곳에서 터지는 게 맞다. 테스트가 100개가 터지더라도 상관없다. 정말로 중요한 것에 대해서는 나중에 문제를 발견하는 게 아니라 미리 잡아야 한다.
정리하자면, 비즈니스 로직을 따로 분리 한다. 그리고 범용인 것들은 조금은 빡빡하게 만든다. 나머지 테스트들은 jest.mock으로 모듈에 대해 모킹하거나 MSW를 이용해서 모킹한다.
MSW는 테스트 환경 외에 웹 브라우저에서 실제로 백엔드 API는 안 나왔지만, 내가 개발하는 환경에서도 쓰고 싶을 때 써볼 수 있는 선택지이다. 대신 백엔드에 대해서 너무 빽빡하게 하지는 말아야 한다.
그리고 MSW는 가짜니까, 진짜로 확실하게 잘 돌아가는지 보려면 E2E 테스트 도구를 쓴다. 조금 더 인간 친화적인 도구를 원한다면 Codeceptjs를 사용한다.
어쨌든 우리가 서비스는 모두 프론트엔드를 통해서 통합이 된다. 백엔드가 아무리 열심히 만들어도 프론트엔드가 없으면 사용자한테 안간다. 사용자 입장에서 어떻게 되는 지를 볼 수 있는 그런 처리들을 하나하나 다 꼼꼼하게 해야 한다.
환경변수 CI에 따라서 Playwright 의 headless 여부를 바꿀 수 있다는 것을 알게 되었다. 이 부분을 공부하면서 자연스럽게 CI에 대해서도 공부하게 되었다. 과제를 제출할 때 명령어가 자동으로 나와서 내 프로젝트를 검사하는 부분이 있는데, 알고보니 이 부분도 Github Actions를 이용한 CI/CD 환경이었다. 이번 팀 프로젝트에는 꼭 이 환경을 넣어야겠다고 생각했다. npm lint
도 안 해보고 올린 PR 을 막을 수 있을 것 같다. 신난다.