티스토리 뷰

반응형

최근 팀 내에서 e2e 테스트를 도입하기 위해 초기 컨벤션을 적용하는 작업을 진행하였다.

대부분 테스트 도구에서 제시하는 문법이나 구조를 따르지만 이 외에 팀원들과 논의하여 적용한 부분들도 있어서 소개해보고자 한다.

 

Cypress vs Playwright

처음에는 기존에 cypress로 작성된 e2e 코드가 있었기 때문에 이것을 정상화시키는 동시에 커버리지를 올린다는 생각으로 그대로 진행했었다.

그러나 생각보다 Cypress 실행속도가 느렸고, 다른 팀에서도 Playwright를 대부분 도입했다고 하여 재검토 요청을 받아 Playwright를 처음으로 사용해보았다.

 

솔직히 여기저기서 많이 언급하는 실행속도의 경우는 개인적으로 엄청 큰 차이는 못느꼈다.

그리고 여러 브라우저 지원의 경우도 우리는 기존에도 크로미움만 주로 지원해서 엄청난 메리트로 느끼지는 못했다.

그런데 Playwright를 직접 사용해보니 사용성 측면에서 DX가 굉장히 좋았다.

설치부터 테스트 실행까지 Cypress에 비해 편리하다는 느낌을 받았다.

계속 테스트 코드를 작성하고 실행시켜보고 싶은 느낌이랄까..?

 

IDE 내에서 각 TC마다 간편하게 실행시켜볼 수 있는것도 그렇고, 익스텐션에서 제공하는 셀렉터를 자동으로 추천해주는 Pick locator 기능도 굉장히 좋았다. 그리고 아직 사용해보지 않았지만 인터렉션에 따라 자동으로 테스트 코드를 작성해주는 Codegen 기능도 있다고 하여 바로 Playwright로 갈아타게 되었다.

 

하지만 기존에 작성한 Cypress 기반의 컨벤션 내용도 다양한 레퍼런스를 통해 정리한 것이라서 (참고로 인강까지 결제함...)

팀에서는 버려진 내용이지만^^ 누군가에게는 도움이 되길 바라며 같이 올려본다..!

 

(아래부터 갑자기 말투 바뀜 주의)


목표

(Cypress, Playwright 동일 내용이라 한번만 작성)

  1. 주요 기능에 대한 테스트를 자동화하여 배포 후 크리티컬한 이슈가 없게 하자 (안심하고 기능개발 및 리팩토링을 진행하자)
  2. 추후 히스토리나 도메인 지식을 모르는 사람이 봐도 동작을 이해할 수 있도록 테스트 코드를 일종의 기능명세서로 만들자
  3. 사용자 관점에서의 테스트를 작성하여 내부 구현이나 특정 기술 스택에 의존하지 않도록 하자 (블랙박스 테스팅)

컨벤션 (Cypress)

대부분의 경우 Cypress와 Cypress Testing Library 문법과 컨벤션을 따르기 때문에 공식문서를 참고하시면 좋습니다.

Description은 가급적 한글로 작성

누가 봐도 이해할 수 있는 기능명세서 역할을 하도록 빠른 파악을 위해 한글로 작성하도록 합니다.

물론, Selector나 Assertion은 한글이 아닐 수 있습니다.

다국어 테스트의 경우에도 description은 한글로 작성하고자 합니다.

DOM 구조 혹은 특정 기술에 의존하지 않는 구문으로 작성

  • Cypress Testing Library 사용
    • Cypress의 get API를 사용해도 DOM 접근이 가능하지만 CSS 셀렉터와 비슷한 구문을 사용하기 때문에 구현에 의존적인 선택자를 작성하게 되는 단점이 있습니다.
    • 좀 더 사용자 친화적인 구문을 작성할 수 있도록 Testing Library를 사용하여 작성하면 아래와 같이 요소를 탐색할 수 있습니다.
    // 구현에 의존적인 선택자로 요소 탐색
    cy.get('.list > li')
    
    // 사용자가 앱을 사용하는 방식과 유사하게 요소 탐색
    cy.findByLabelText('상품명')
    
  • 사용자 정의 속성 (data-testid)

반복되는 구문 및 Util성 구문

  • Cypress에서 제공하는 Custom Commands 를 사용하여 Cypress 구문을 확장합니다.
    • 아래는 Url 검증 코드와 로그인 코드를 Custom Commands로 선언하여 사용한 예시입니다.
    • 이와 같이 여러 테스트파일에 걸쳐 반복적으로 사용되는 구문을 아래와 같이 선언하여 사용할 수 있습니다.
    // 선언
    Cypress.Commands.add('assertUrl', url => {
      cy.url().should('eq', `${Cypress.env('baseUrl')}${url}`);
    });
    
    // 사용
    cy.assertUrl('/account/sign-in');
    
    // 선언
    Cypress.Commands.add('signIn', () => {
      localStorage.setItem(LOCAL_STORAGE_ACCESS_TOKEN, ACCESS_TOKEN);
    	// 중략...
    });
    
    // 사용
    cy.signIn();
    
  • 모든 반복 코드를 Cypress API로 확장할 필요는 없으며, 동일한 페이지 내에서 반복되는 구문은 확장 없이 해당 페이지 내에서 함수를 만들어서 사용해도 무방할 것 같습니다.
  • 그 외 반복적인 selector나 intercept의 경우 as 구문을 사용하여 alias를 지정할 수 있습니다.
// 선언
cy.findAllByRole('table').eq(1).findAllByRole('row').as('itemList');

// 사용
cy.get('@itemList').findByText('1개').should('exist');

API Mocking

Cypress의 intercept API를 사용하여 API를 stubbing, spying 합니다.

Stubbing: 네트워크 호출에 대해 미리 정해진 응답을 반환하는 것
Spying: 네트워크 호출이 어떻게 이루어졌는지 요청/응답에 대해 기록

 

mocking 응답에 대한 json 객체는 fixtures 폴더 하위에 구성되어 있으며 해당 응답을 가지고 아래와 같이 작성하면 됩니다.

import fixturesUserInfo from '../fixtures/user/info.json';

cy.apiIntercept('GET', '/.api/users', {
  statusCode: 200,
  body: fixturesUserInfo,
});

각 API path에 따른 응답값은 이미 정해져 있기 때문에 호출할 때 마다 해당 json을 불러오는 것은 불편할 것으로 보여 이 부분은 추가적으로 검토한 후 다시 정리하도록 하겠습니다.

자동화

앞으로 테스트를 자동화 하여 코드를 통합하기 전 반드시 한 번 이상 테스트를 실행해 볼 수 있도록 하고자 합니다.

cypress에서 github action을 지원하고 있어서 이것을 우선적으로 적용해보려고 합니다.

 

 

 


 

 

컨벤션 (Playwright)

대부분의 경우 Playwright 문법과 컨벤션을 따르기 때문에 공식문서를 참고하시면 좋습니다.

폴더 구조

아래와 같은 폴더 구조를 가집니다.

e2e
ㄴ fixture // mocking용 json 파일 묶음 폴더
ㄴ support // 공용 유틸성 파일 묶음 폴더
ㄴ {도메인}
  ㄴ xxx.spec.ts
ㄴ xxx.spec.ts

 

테스트 파일은 1depth, 2depth 혼재되는 방식으로, 기본적으로 1depth로 하고, 테스트 파일이 길어지거나 맥락별로 파일을 분리할 필요가 있다면 2depth로 진행합니다.

Description은 가급적 한글로 작성

누가 봐도 이해할 수 있는 기능명세서 역할을 하도록 빠른 파악을 위해 한글로 작성하도록 합니다.

물론, Selector나 Assertion은 한글이 아닐 수 있습니다.

다국어 테스트의 경우에도 description은 한글로 작성하고자 합니다.

DOM 구조 혹은 특정 기술에 의존하지 않는 셀렉터 사용

마크업 클래스보다는 더 명시적인 값으로 요소 선택하기 (Locators 문서 참고)

  • 사용자 친화적인 값
// 👎
page.locator('button.buttonIcon.episode-actions-later');

// 👍
page.getByRole('button', { name: 'submit' });
  • 사용자 정의 속성 (data-e2e)
const Dialog = () => {
  return (
    <div data-testid="dialog">
      // ...
    </div>
  )
}

page.getByTestId('dialog')
  • Playwright Test for VSCode 익스텐션을 설치하면 Pick locator 기능을 지원하여 마우스 커서를 올려 손쉽게 요소 선택에 대한 추천을 받을 수 있습니다.
    • VSCode에서 Pick locator 선택

  • 열린 브라우저에서 특정 요소 위에 마우스를 올리면 아래와 같이 셀렉터 추천

page.evaluate() 사용

Playwright scripts는 브라우저와는 다른 환경에서 실행되기 때문에 Web API 처럼 브라우저 환경에서 접근 가능한 객체에 접근하려면 page.evaluate() API를 사용해야 합니다. (링크)

아래는 로그인 환경을 만들기 위해 localStorage에 접근하는 코드입니다.

async signin() {
  await this.page.evaluate((token) => 
		localStorage.setItem('access-token', token), 'foo');
}

POM(Page Object Models) 사용

Playwright에서 제시하는 Page Object Models을 사용하여 재사용 가능한 코드를 관리합니다.

  • 동일 로직, 동일 요소 선택에 대한 코드 공통화
  • 테스트 케이스 가독성 향상

아래 예시는 로그인과 같은 일반적인 작업을 캡슐화하기 위해 Common이라는 helper class를 만들었습니다. (네이밍 변경 가능)

// e2e/support/common.ts
export default class Common {
  readonly page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  async signin() {
    await this.page.evaluate((token) => localStorage.setItem('access-token', token), 'foo');

    await this.page.route('/.api/users', async (route) => {
      await route.fulfill({ json: fixturesUserInfo });
    });
  }
}

이제 테스트 코드에서 Common을 사용하여 손쉽게 로그인 로직을 호출할 수 있습니다.

// e2e/main.spec.ts
test('로그인시 "장바구니"와 "마이페이지" 버튼이 보인다', async ({ page }) => {
  const common = new Common(page);
  await common.signin();

  await expect(page.getByLabel('장바구니')).toBeVisible();
  await expect(page.getByLabel('마이페이지')).toBeVisible();
});

API Mocking

stubbing할 응답은 /tests/e2e/fixtures 경로에 json 파일로 존재합니다. 기존에 cypress용으로 존재하던 파일들을 그대로 옮겨온 것이라 수정이 필요할 수 있습니다.

import fixturesUserInfo from '../fixtures/user/info.json';

await this.page.route('/.api/users', async (route) => {
  await route.fulfill({ json: fixturesUserInfo });
});

각 API path에 따른 응답값은 이미 정해져 있기 때문에 호출할 때 마다 해당 fixture를 불러오는 것은 불편할 것으로 보여 이 부분은 추가적으로 검토한 후 다시 정리하도록 하겠습니다.

CI

playwright 설치 시 자동 추가된 playwright.yml 파일을 활용하여 테스트 해보고 업데이트 예정입니다.

    •  
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함