실무에 바로 적용하는 프런트엔드 테스트 - 2부. 테스트 심화: 시각적 회귀・E2E 테스트
시각적 회귀 테스트 강의의 Storybook 코드에서 play 함수 안에 matcher를 넣지 않고 userEvent.click(...)만 사용한 이유가 궁금합니다. import { userEvent, within } from '@storybook/testing-library'; import AvailableUsers from '@/pages/login/components/AvailableUsers'; export default { component: AvailableUsers, title: '로그인/사용자 리스트', }; export const Folded = { name: '접힌 상태', }; export const Expanded = { name: '펼친 상태', play: async ({ canvasElement }) => { const canvas = within(canvasElement); await userEvent.click(canvas.getByText('⚠️ 사용 가능한 유저 리스트')); }, }; 제가 이해한 바로는, 이 스토리는 storybook에서 아코디언이 펼쳐진 시각적 상태 변화를 보여주기 위한 용도이고, 실제 기능 로직 검증은 Vitest에서 별도의 테스트 코드로 작성하는 방향이라고 생각했습니다. 1. 여기서 play 안에 matcher를 넣지 않은 것은 Storybook을 시각적 상태 확인용으로 사용하려는 의도인가요? 2. 그리고 이 컴포넌트의 동작 검증은 Storybook이 아니라 Vitest에서 담당하도록 역할을 나눈 것으로 이해하면 될까요? 3. 실무에서는 보통 storybook을 어디까지 작성하는지 궁금합니다. - 물론 회사마다 다르겠지만, 강의해주시는 강사님 기준으로 의견을 듣고 싶습니다.
강의를 들으며 장바구니 페이지에 대해 Cypress로 테스트 코드를 작성하다가 궁금한 점이 몇가지 있어서 질문을 작성합니다 :) 장바구니 페이지에서 '장바구니의 상품을 삭제한 후 장바구니에 담겨있는 총 수량과 가격이 변경된다.'라고 테스트 시나리오를 작성했는데 이런 걸 e2e로 테스트 하는 게 맞는지 잘 모르겠습니다. 이런 부분은 unit 테스트로 넘겨야 하는 부분일까요? e2e테스트는 어떤 걸 중심으로 하면 좋을지 잘 모르겠습니다. 그냥 제가 필요하다고 생각되는 게 있으면 진행을 하면 되는 걸까요? 저도 모르게 저 대신 클릭 등을 수행하고 값을 확인하는 용도로 다루게 되는 것 같습니다. 그래도 일단 1번 질문에서 작성한 테스트 시나리오에 대한 코드를 직접 작성해봤습니다. 이 때, 장바구니 페이지에서 장바구니 아이템 데이터가 getServerSideProps를 통해 주입되고 있더라구요. 이 상황에서 API 모킹을 어떻게 해야할지 몰라서 Claude Sonnet 4.6이랑 구글링을 통해 몇가지 코드를 작성해봤습니다. 단순하게 테스트 코드가 작성되지 않고 다소 복잡하게 작성이 되는 것 같아서 뭔가 이 방향이 맞지 않는 것 같다는 생각이 들었습니다. 그래서 테스트 시나리오 자체를 제가 잘못 생각한 것 같다는 생각이 들기도 했습니다. SSR 환경에서 어떤 코드로 작성을 하는 게 맞는 방식인지 알 수가 없어서 한 번 확인해봐주시면 감사하겠습니다 :) a. nock + intercept 사용 - SSR은 서버 프로세스에서 진행되기 때문에 intercept 할 수 없다고 하여 nock을 사용했습니다. 코드는 해당 링크 를 참고 했습니다. - 그리고 router.replace를 통해 데이터를 refetch 될 때도 백엔드 api를 intercept를 할 수 없다고 해서 '/_next/data/**/*.json'를 intercept 했습니다. - 개인적으로 아래 코드는 'nock'과 'intercept'를 혼용해서 쓰다보니 장기적으로 봤을 때 유지보수성 측면에서 좋지 않은 코드라고 느꼈습니다. it.only('장바구니의 상품을 삭제한 후 총 수량과 가격이 변경된다', () => { // 1) 초기 로드용 nock (SSR - 서버사이드 fetch) cy.task('nock', { hostname: Cypress.env('API_URL'), method: 'GET', path: '/carts', statusCode: 200, body: FIVE_CART_ITEMS, }); // 2) router.replace 후 재조회용 intercept (클라이언트사이드 fetch) cy.intercept('GET', '/_next/data/**/cart.json', { body: { pageProps: { carts: FOUR_CART_ITEMS_AFTER_DELETE }, __N_SSP: true, }, }).as('refetch'); // 삭제 API mock cy.intercept('POST', '/api/cart', { statusCode: 200, body: { data: { name: '4' } }, }).as('deleteCart'); cy.visit('/cart'); // action cy.getByCy('cart-item-delete-button').first().click(); cy.wait('@deleteCart'); cy.wait('@refetch'); // 페이지 갱신 대기 // assertion cy.contains('Unbranded Rubber Chair').should('not.exist'); cy.getByCy('cart-item').should('have.length', 4); cy.getByCy('cart-item-total-amount').should('have.text', '4'); cy.getByCy('cart-item-total-price').should('have.text', '836.00'); }); b. intercept 사용 그래서 초기 로드에도 intercept를 사용했습니다. it.only('장바구니의 상품을 삭제한 후 총 수량과 가격이 변경된다', () => { // 1) 초기 로드용 (SSR - 서버사이드 fetch) cy.intercept('GET', '/_next/data/**/*.json', { body: { pageProps: { carts: FIVE_CART_ITEMS }, __N_SSP: true, }, }); // 2) router.replace 후 재조회용 intercept (클라이언트사이드 fetch) cy.intercept('GET', '/_next/data/**/*.json', { body: { pageProps: { carts: FOUR_CART_ITEMS_AFTER_DELETE }, __N_SSP: true, }, }).as('refetch'); // 삭제 API mock cy.intercept('POST', '/api/cart', { statusCode: 200, body: { data: { name: '4' } }, }).as('deleteCart'); cy.visit('/cart'); // action cy.getByCy('cart-item-delete-button').first().click(); cy.wait('@deleteCart'); cy.wait('@refetch'); // 페이지 갱신 대기 // assertion cy.contains('Unbranded Rubber Chair').should('not.exist'); cy.getByCy('cart-item').should('have.length', 4); cy.getByCy('cart-item-total-amount').should('have.text', '4'); cy.getByCy('cart-item-total-price').should('have.text', '836.00'); }); c. API 모킹을 사용하지 않기 - ssr 환경에서 delete 테스트를 위해 api 모킹을 사용하는 게 오히려 복잡하게 느껴져서 API 모킹을 사용하지 않고 코드를 작성하는 방식을 생각했습니다. - 대신에 테스트 할 때마다 수량과 가격이 변경될 거라고 생각해서 수량과 가격을 캡쳐하는 코드를 추가했습니다. it.only('장바구니의 상품을 추가한 뒤 삭제하면 해당 상품이 사라진다', () => { const cartItem = { "id": "24", "name": "Practical Frozen Pants", "price": "607.00", "imageUrl": "https://cdn.pixabay.com/user/2023/05/21/19-38-51-804_250x250.jpg" } // prepare: API로 상품 직접 추가 cy.request('POST', `${Cypress.env('API_URL')}/carts`, cartItem).then(({ body }) => { cy.visit('/cart'); // 삭제 전 수량과 가격 캡처 cy.getByCy('cart-item-total-amount').invoke('text').then((beforeAmount) => { cy.getByCy('cart-item-total-price').invoke('text').then((beforePrice) => { // action: 방금 추가한 항목 삭제 cy.contains('Practical Frozen Pants') .closest('[data-cy="cart-item"]') .find('[data-cy="cart-item-delete-button"]') .click(); // assertion cy.contains('Practical Frozen Pants').should('not.exist'); cy.getByCy('cart-item-total-amount').should('have.text', String(Number(beforeAmount) - 1)); cy.getByCy('cart-item-total-price').should('have.text', (Number(beforePrice) - cartItem.price).toFixed(2)); }); }); }); }); 일반적으로 delete에 대해 테스트할 때 어떤 식으로 하는지 궁금합니다. 삭제 성공 여부 / 해당 데이터가 보이지 않는지 체크 정도만 하고 삭제가 진행됨으로써 총 아이템 개수 수량이 변경되는 것에 대해서는 테스트 코드로 굳이 짚고 넘어가지 않나요? 실무에서 테스트 데이터는 보통 어떻게 관리하나요? 전부 api 모킹으로 테스트 해야하는 건지, 아니면 테스트용 DB를 따로 생성해서 거기서 테스트를 진행해야하는 건지 궁금합니다. 아니면 상황에 따라서 api 모킹을 해야할 때가 있고, DB를 연결해서 테스트 해야할 때가 있나요? 한번에 질문이 다소 많아서 죄송합니다 ㅠㅠ 완강하고 며칠동안 혼자서 공부해보다가 제대로 제가 하고 있는 건지 모르겠어서 여쭤봅니다 ㅠㅠ 긴 글 읽어주셔서 감사합니다!!
그냥 블로그 같은 가이드 보고 폴더를 막 생성했더니 폴더가 어디에 접속해야 있는지도 모르겠고, e2e 폴더 같은건 전혀 없는데요, e2e 폴더 내에 파일을 생성해야하는 이유가 뭔가요? 꼭 폴더를 강사님이 세팅한대로 설정해야하나요? 처음 폴더 설정이 finder (폴더)로 보여져서 생성되지 않아, 테스트파일 생성 위치와 구조를 처음에 어떻게 잡아야 할지 모르겠습니다.
실무에 바로 적용하는 프런트엔드 테스트 - 2부. 테스트 심화: 시각적 회귀・E2E 테스트
안녕하세요 e2e 테스트는 비용이 많이 들어서 어떻게 관리를 하는지 궁금합니다. - 파이프라인에서 매번 CI 에서 돌리기에는 서버나, 시간이 개발 생산성을 잡아 먹을 것 같은데요. 점점 e2e 테스트가 쌓여 나갈떄 어떤 전략을 취할 수 있는지 궁금해요. GPT로 리서치하니까 보통 2가지를 병행해서 스케줄러 + 온디맨드 실행 같이 관리 한다고 하는데, 실제 현업에서는 어떻게 활용하고 계신지, 제가 리서치한 내용 말고 더 효율적이거나, 추천할만한 전략 , 이외의 고려사항 소개해주실 수 있으면 알려주시면 감사하겠습니다. 스케줄 실행 (Nightly/주말 풀스윗) EventBridge → ECS Fargate RunTask (Spot 가능) → Playwright 러너 대상: 스테이징(or PR 프리뷰 URL) b. 산출물: 트레이스/비디오/리포트 S3 업로드, Slack에 요약/링크 온디맨드 실행 (PR 코멘트/수동 트리거) Bitbucket Pipeline에서 aws ecs run-task 호출(또는 ChatOps 슬래시 커맨드) 태그/폴더/샤드 인자 넘김 → 필요한 부분만
첫번째 사진 두번째 사진 첫번째 사진은 초록색 화살표 표시가 생겨서 디버깅을 할 수 있는데, 두번째 사진은 새로고침, 종료 후 다시 실행 등등 다양한 방법을 시도했음에도 불구하고 초록색 화살표가 생기지 않아서 디버깅을 할 수 없습니다. Playwright Test for VSCode는 설치 했습니다. 원인이 무엇일까요?
실무에 바로 적용하는 프런트엔드 테스트 - 2부. 테스트 심화: 시각적 회귀・E2E 테스트
안녕하세요 선생님 강의 잘 듣고 있습니다. 이번 강의에서 질문이 있습니다! 1. 커스텀 쿼리도 Retry-ability 지원되고 커스텀 쿼리 안에 커맨드도 Retry-ability가 지원되면 n의 m제곱 번의 재시도가 발생하는 것일까요? 2. 커스텀 커맨드와 커스텀 쿼리 중에 뭘 사용할 것인지는 Retry-ability 지원 유무와 체이닝을 기준으로 선택하면 될까요? 예시코드를 봤을 때 getCardButton 와 getProductCardByIndex 둘 다 DOM 요소를 조회해서 subject를 리턴하여 체이닝을 통해 후속 작업을 하는 것처럼 보이는데 왜 getProductCardByIndex 는 커스텀 커맨드로 작성하는지 잘 모르겠습니다..
실무에 바로 적용하는 프런트엔드 테스트 - 2부. 테스트 심화: 시각적 회귀・E2E 테스트
질문 git clone https://github.com/practical-fe-testing/test-example-shopping-mall.git 를 했는데 폴더안에 gitignore 과 readme 파일만 있는데 해당 브랜치를 clone해서 사용하는게 아닌건가요? 참고사항 대체방안으로 zip파일로 다운받아서 사용하고 있긴합니다!
실무에 바로 적용하는 프런트엔드 테스트 - 2부. 테스트 심화: 시각적 회귀・E2E 테스트
unit-test-example에서 테스트를 실행하니 테스트코드 통과 여부에 관계없이 Test result not found만 뜨며 실패합니다. 이유를 모르겠어요ㅠㅠ Test result not found. If you set `vitest.commandLine` please check: Did you set `vitest.commandLine` to `run` mode? (This extension requires `watch` mode to get the results from Vitest api) Does it have the ability to append extra arguments? (For example it should be `yarn test --` rather than `yarn test`) Are there tests with the same name? Can you run vitest successfully on this file? Does it need custom option to run?
실무에 바로 적용하는 프런트엔드 테스트 - 2부. 테스트 심화: 시각적 회귀・E2E 테스트
시간: 9:00 Q. ‘비밀번호를 입력하지 않고 로그인 버튼을 클릭할 경우 “비밀번호를 입력하세요” 경고 메시지가 노출된다’ 를 테스트 할 때, 비밀번호가 입력하지 않은 상태인지 확인하는 부분은 없어도 되는 건가요?? 비밀번호를 입력하는 요소가 null 인지 확인하는 테스트는 없어도 되는 건가요??
실무에 바로 적용하는 프런트엔드 테스트 - 2부. 테스트 심화: 시각적 회귀・E2E 테스트
Q1. 장바구니, 구매 버튼 클릭 시 해당 상품의 이름을 cy.findByText('Handmade Cotton Fish 장바구니 추가 완료!').should('exist) 상품 이름을 직접 입력하는데, (통합 테스트에서 모킹 데이터를 사용한다면 모킹 데이터는 직접 설정하기 때문에 가능하지만), 실제 api를 사용하면 첫번째 상품 데이터가 변경할 수도 있기 때문에 실패할 수도 있기 때문에 다르게 작성해야 하지 않을까요?? 질문 드립니다 Q2. 로그인을 커스텀 커맨드에서 설정해서 session을 사용해 설정해서 테스트할 때, 웹에서는 로그인 성공/실패 여부는 확인할 수 없는 것 아닌가요?? 로그인이 실패하게 되면 로그인 관련 테스트는 모두 실패하게 되는데 그냥 이 결과로 로그인 성공/실패 확인을 하는 건가요??
실무에 바로 적용하는 프런트엔드 테스트 - 2부. 테스트 심화: 시각적 회귀・E2E 테스트
Q. cy.intercept()는 회원 가입 요청에 대한 성공/실패 응답을 모킹해서 받는데, 이 때 의도하는 코드는 1번이지만 응답이 정해져 있기 때문에 2번처럼 틀린 요청으로도 결과는 똑같이 나오게 될 거라고 생각됩니다. 실제 응답이 아니라 응답을 정해 놓고 전달하기 때문에 실제 동작과 다를 수도 있어서 이 부분은 '실패, 성공 시 문구 노출'이 나타나는 동작이 있다는 document 로만 이해해도 될까요?? 1 성공 데이터 요청 -> 성공 데이터 응답 실패 데이터 요청 -> 실패 데이터 응답 2 실패 데이터 요청 -> 성공 데이터 응답 성공 데이터 요청 -> 실패 데이터 응답
수강생분들의 질문을 기다립니다! - 에러에 해당하는 질문은 "에러가 발생한 상황에 대한 충분한 설명", "에러 메세지", "에러가 난 코드 스크린샷"을 함께 첨부해주세요. - 언어에 해당하는 질문은 구글링 및 서치 후에 구체적으로 질문해주시면 좋습니다. - 간단한 진로 및 방향성에 대한 질문은 메일로 보내주세요. - 패키지 버전 관리은 실무 환경과 트랜드에 맞추어 강의를 업데이트 하고 있습니다. 강의를 그대로 따라갔는데 에러가 발생한다면 패키지 버전을 강의에서 사용하는 버전과 동일하게 맞춰주세요! - 강의 노트, QA 목록, 공지 사항을 먼저 확인해주세요. - 논리적이고 구체적인 질문은 학습 효과를 올립니다 :) 강의를 보시다가 혹시나 npm run test:e2e 를 돌려봤는데 npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! amamov.com @1.8.0 test:e2e: jest --config ./test/jest-e2e.json npm ERR! Exit status 1 npm ERR! npm ERR! Failed at the amamov.com @1.8.0 test:e2e script. npm ERR! This is probably not a problem with npm. There is likely additional logging output above. npm ERR! A complete log of this run can be found in: npm ERR! /Users/anhyeongjun/.npm/_logs/2022-10-13T13_02_17_408Z-debug.log 이런 error를 맞이하신다면 { "moduleFileExtensions": ["js", "json", "ts"], "rootDir": ".", "testEnvironment": "node", "testRegex": ".e2e-spec.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, "moduleNameMapper": { "^src/(.*)$": "<rootDir>/../src/$1" } } moduleNameMapper이부분이 들어가있는지 확인하신 후 없으면 넣어주면 test가 성공적으로 진행될 수 있습니다. 다만 윤상석 선생님 추가해서 test를 돌려보면 Jest did not exit one second after the test run has completed. This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with --detectOpenHandles to troubleshoot this issue. 이런 오류가 나는데 이유가 무엇일까요?