도커 컴포즈로 여러 도커 컨테이너를 실행하며, 컨테이너 간 통신으로 시스템을 연동해 테스트
UI를 조작하면 영속 계층에 의도한 내용이 저장되고, 그 내용이 화면에 반영되는지를 검증
E2E 테스트에 도커 컴포즈를 도입하면 테스트 환경을 실행하고 종료하는 것이 편리해짐
CI에서 하나의 작업으로 실행할 수 있기 때문에 개발 워크플로에 포함시켜 쉽게 자동화 가능
Playwright
마이크로소프트가 공개한 E2E 프레임워크
크로스 브라우징 지원
다양한 기능 제공
디버깅 테스트
리포터
트레이스 뷰어
테스트 코드 생성기
설치 및 설정
$npminitplaywright@latest
# 타입스크립트와 자바스크립트 중 타입스크립트를 선택? Do you want to use Typescript or Javascript? - Typescript# 테스트 파일을 저장할 폴더를 설정. e2e라는 이름의 폴더를 사용? Where to put your end-to-end tests? - e2e# 깃허브 액션의 워크플로를 추가할지 말지 선택. - NO? Add a GitHub Actions workflow? (y/N) - false# 플레이라이트 브라우저를 설치? Install Playwright browsers (canbedonemanuallyvia'npx playwright install')? (Y/n) - true
설치가 완료되면 package.json에 필요한 모듈이 추가되고, 설정 파일 양식과 샘플 테스트 코드가 생성됨
처음 시작하는 E2E Test
브라우저 자동화 테스트는 테스트마다 브라우저를 열어 지정된 URL로 접속하는것으로 시작됨
page.goto 에 URL 지정
수동으로 브라우저를 조작하면서 애플리케이션 기능을 검증하는 것을 코드로 대체해 테스트를 자동화 가능
import { test, expect } from'@playwright/test';test("has title",async ({ page }) => {awaitpage.goto("https://playwright.dev/");// 페이지 제목에 "Playwright"가 포함됐는지 검증한다.awaitexpect(page).toHaveTitle(/Playwright/);});test("get started link",async ({ page }) => {awaitpage.goto("https://playwright.dev/");// "Get started"라는 접근 가능한 이름을 가진 링크를 취득하고, 링크를 클릭한다.awaitpage.getByRole("link", { name:"Get started" }).click();// 페이지 URL에 "intro"가 포함됐는지 검증한다.awaitexpect(page).toHaveURL(/.*intro/);});
로케이터
플레이라이트의 핵심 API
현재 페이지에서 특정 요소를 가져온다.
1.27.0 버전에는 테스팅 라이브러리로부터 영향을 받은 접근성 기반 로케이터가 추가됨
플레이라이트도 접근성 기반 로케이터를 우선적으로 사용하는 것을 권장한다
테스팅 라이브러리와 다른 점은 대기 시간이 필요한지에 따라 findByRole 등을 구분해서 사용하지 않아도 된다.
인터렉션은 비동기 함수이기 때문에 await 로 인터렉션이 완료될 때까지 기다린 후 다음 인터렉션이 실행하는 방식으로 작동됨
test.describe("로그인 페이지", () => {constpath="/login";test("로그인 성공 시 리다이렉트 이전 페이지로 돌아간다",async ({ page }) => {awaitpage.goto(url("/my/posts"));awaitexpect(page).toHaveURL(url(path)); //로그인 화면으로 리다이렉트됨awaitlogin({ page }); // 로그인 인터렉션을 실행하는 유팉 함수awaitexpect(page).toHaveURL(url("/my/posts")); })})
프로필 기능 E2E 테스트
UI를 조작해서 프로필 갱신 API 요청을 발생시킴
API Routes가 작동하고 DB 서버에 값이 저장됨
세션에 저장된 값이 갱신됨
새로운 페이지 제목은 갱신된 세션값을 참조
getServerSideProps로 로그인한 사용자 정보 취득하기
서버 사이드 렌더링을 위한 데이터 취득 함수인 getServerSideProps 는 로그인 상태를 검사하는 고차함수(withLogin) 에 래핑되어있음
인수인 user에는 로그인한 사용자의 정보가 저장됐는데, 해당 로그인 정보를 기반으로 페이지의 데이터를 요청하거나 프로필 정보를 취득한다.
user 객체는 세션에 저장된 정보를 불러옴
Page.getPageTitle =PageTitle( ({ data }) =>`${data?.authorName}님의 프로필 편집`);// 로그인 상태 확인을 포함한 getServerSidePropsexportconstgetServerSideProps=withLogin<Props>(async ({ user }) => {return {// 프리즈마 클라이언트를 래핑한 함수를 통해 데이터베이스에서 데이터를 취득 profile:awaitgetMyProfileEdit({ id:user.id }), authorName:user.name // 제목에 사용할 유저명을 Props에 포함시킴 }})
프로필 정보를 갱신하는 API Routes
Next.js는 API Routes라는 웹 API 구현 기능을 제공함
UI를 조작해 비동기로 데이터를 취득하거나 갱신하는 요청을 받았을 때 서버 프로세스에서 작업을 처리하여 JSON 같은 형식으로 API 응답을 반환한다.
"대부분 코드가 프리즈마 의존성있는 코드라 일단 스킵"
import { expect, test } from'@playwright/test';import { UserName } from'../prisma/fixtures/user';import { login, url } from'./util';test.describe("프로필 편집 페이지", () => {constpath="/my/profile/edit";constuserName:UserName="User-MyProfileEdit";constnewName="NewName";test("프로필을 편집하면 프로필에 반영된다.",async ({ page }) => {awaitpage.goto(url(path));awaitlogin({ page, userName });// 여서부터 프로필 편집 화면awaitexpect(page).toHaveURL(url(path));awaitexpect(page).toHaveTitle(`${userName}님의 프로필 편집`);awaitpage.getByRole("textbox", { name:"사용자명" }).fill(newName);awaitpage.getByRole("button", { name:"프로필 변경하기" }).click();awaitpage.waitForURL(url("/my/posts"));// 페이지 제목에 방금 입력한 이름이 포함됨awaitexpect(page).toHaveTitle(`${newName}님의 기사 목록`);awaitexpect(page.getByRole("region", { name: 프로필" })).toContainText(newName);awaitexpect(page.locator("[aria-label='로그인한 사용자']")).toContainText(newName); })})
Like 기능 E2E 테스트
기획
로그인한 사용자만 Like를 누를 수 있다.
단, 자신이 작성한 기사에는 누를 수 없다.
검증
다른 사람이 작성한 기사에는 Like를 누를 수 있어야 한다.
자신의 기사에는 Like를 누를 수 없어야 한다.
다른 사람이 작성한 기사에는 Like 버튼을 누를 수 있어야 한다.
test("다른 사람의 기사에는 Like 할 수 있다.",async ({ page }) => {awaitpage.goto(url("/login"));awaitlogin({ page, userName:"Jpub" });awaitexpect(page).toHaveURL(url("/"));// 여기서부터 기사 목록 페이지awaitpage.goto(url("/posts/10"));constbuttonLike=page.getByRole("button", { name:"Like" });constbuttonText=page.getByTestId("likeStatus");// like 버튼이 활성화되고, 현재 Like 수는 0awaitexpect(buttonLike).toBeEnabled();awaitexpect(buttonLike).toHaveText("0");awaitexpect(buttonText).toHaveText("Like");awaitbuttonLike.click();// like를 클릭하면 카운트가 1증가하고 이미 like 누른 상태가 된다.awaitexpect(buttonLike).toHaveText("1");awaitexpect(buttonText).toHaveText("Liked");})
본인이 작성한 기사에는 like 버튼을 누를 수 없다.
test("본인이 작성한 기사에는 Like 할 수 없다.",async ({ page }) => {awaitpage.goto(url("/login"));awaitlogin({ page, userName:"Jpub" });awaitexpect(page).toHaveURL(url("/"));// 여기서부터 기사 목록 페이지awaitpage.goto(url("/posts/90"));constbuttonLike=page.getByRole("button", { name:"Like" });constbuttonText=page.getByTestId("likeStatus");// like 버튼이 비활성화되고, Like 문자도 사라짐awaitexpect(buttonLike).toBeDisabled();awaitexpect(buttonText).not.toHavetext("Like");})
신규 작성 페이지 E2E 테스트
이른바 CRUD라 불리는 기능의 페이지 테스트
CRUD 기능 테스트는 다른 테스트에 영향을 미치지 않는지 세심히 살펴봐야 한다.
기본적으로 Publish 기능 테스트는 새로운 기사를 작성한 후 해당 기사를 대상으로 CRUD해야 한다.