cypress e2e 테스트에서의 트러블 슈팅에서 시작된 실행 명령어 알아보기

리버 river·2024년 3월 18일
0

트러블 슈팅

목록 보기
3/3

이번 주에는 점심 뭐먹지 2단계 미션을 진행하면서 cypress로 e2e 테스트 코드를 작성해야했다.

미션의 package.json에 등록되어있는 cypress 테스트 실행 명령은 아래와 같았다.

npx cypress open

(우테코 미션에서는 npm run test 명령어였다)

나는 이번에 e2e 테스트 코드를 한 파일에 적지 않고 아래와 같이 컴포넌트 단위로 짜게되었는데, e2e 테스트 코드를 작성한 후 코드를 리팩토링할 때 마다 아래 테스트들을 하나 하나씩 눌러 돌려보는데 너무 귀찮았다.
e2e 테스트 구성

jest 테스트 코드를 돌릴 때 처럼 분명히 한번에 모든 e2e테스트를 돌려보는 명령어가 있을거라 생각했고 그리해서 발견한 것이 아래의 cypress 테스트 실행 명령어였다.

npx cypress run --headed
  • 브라우저가 열리고 모든 e2e테스트가 자동으로 빠르게 실행된다.그리고 그 결과가 터미널에 출력된다.
npx cypress run
  • 브라우저가 열리지 않고 모든 e2e테스트가 자동으로 빠르게 실행된다.그리고 그 결과가 터미널에 출력된다.

마주한 에러

⬇️ npx cypress run으로 cypress를 실행한 결과 스샷

내가 딱 원하는대로 모든 e2e케이스를 자동으로 돌려주고 결과를 터미널창에서 깔끔하게 보여준다.

이렇게 편안함에 딱 만족할려던 차에!

npx cypress open로 돌릴 때는 잘 통과되던 테스트가
npx cypress run으로 실행하니 통과하질 못하는 상황이 벌어졌다.npx cypress run --headed로 실행해봐도 마찬가지였다.

참고로 이 테스트가 돌아갈 때 사용되는 비즈니스 로직 변경은 아무것도 없었다.

이유가 도대체뭘까?

에러 메시지

일단 에러메시지를 살펴보자.

CypressError: Timed out retrying after 4000ms: cy.find() failed because the page updated as a result of this command, but you tried to continue the command chain. The subject is no longer attached to the DOM, and Cypress cannot requery the page after commands such as cy.find().
Common situations why this happens:
- Your JS framework re-rendered asynchronously
- Your app code reacted to an event firing and removed the element
You can typically solve this by breaking up a chain. For example, rewrite:
cy.get('button').click().should('have.class', 'active')
to
cy.get('button').as('btn').click()
cy.get('@btn').should('have.class', 'active')
https://on.cypress.io/element-has-detached-from-dom

번역본 ⬇️

CypressError: 4000ms 후에 재시도하는 동안 시간이 초과되었습니다: cy.find()가 이 명령으로 인해 페이지가 업데이트 되었기 때문에 실패했고, 명령 체인을 계속 시도했습니다. 대상이 더 이상 DOM에 연결되어 있지 않고, cy.find()와 같은 명령 후에 Cypress가 페이지를 다시 쿼리할 수 없습니다.
이런 상황이 발생하는 일반적인 경우는:
여러분의 자바스크립트 프레임워크가 비동기적으로 다시 렌더링되었습니다
앱 코드가 이벤트 발생에 반응하여 요소를 제거했습니다
이 문제를 해결하는 일반적인 방법은 체인을 분리하는 것입니다. 예를 들어, 다음과 같이 작성하세요:
cy.get('button').click().should('have.class', 'active')

cy.get('button').as('btn').click()
cy.get('@btn').should('have.class', 'active')
로 바꾸세요.
https://on.cypress.io/element-has-detached-from-dom

찾아보니 해당 에러 메시지는 페이지가 업데이트되어 대상 요소가 DOM에서 분리되었기 때문에 cy.find() 명령어가 실패한 것이라한다. 이는 비동기적인 렌더링이나 이벤트 처리로 인해 요소가 제거되었을 가능성이 있다라고한다.

select에서 sort값을 고르면('이름순','거리순') 음식점 데이터를 비동기적으로 받아온 후 정렬해서 렌더링하는 방식으로 구현했기때문에 위에서 언급된 비동기적인 렌더링이 이 에러의 원인인것같다.

특정 cypress 실행 환경에서만 테스트가 실패하고있기 때문에 각 명령어의 실행환경 차이점을 알아보자.

cypress 실행 명령어들의 실행환경

npx cypress open

  • 이 명령어는 Cypress의 대화형 테스트 러너(Test Runner)를 실행한다.
  • Cypress의 그래픽 사용자 인터페이스(GUI)가 열리고, 테스트 파일 목록이 표시된다.
  • 개발자가 테스트 파일을 선택하면 해당 테스트가 브라우저에서 실행되며, 실시간으로 테스트 진행 상황을 확인할 수 있다.
  • 테스트 실행 중에 개발자는 브라우저를 직접 조작하거나 디버깅 도구를 사용할 수 있다.
  • 개발자가 테스트 실행을 제어할 수 있으므로, 필요한 경우 수동으로 대기 시간을 조정하거나 특정 상황에 맞게 대처할 수 있다.
    • 이러한 이유로 npx cypress open에서는 cy.find()실행에서 런타임 타임아웃 에러가 발생하지 않는 경향이 있다.
  • 따라서 개발 과정에서는 npx cypress open을 사용하여 테스트를 작성하고 디버깅하는 것이 효과적이다.

npx cypress run --headed

  • 이 명령어는 Cypress 테스트를 headed 모드로 실행한다.
  • 실제 브라우저 창이 열리고 테스트가 자동으로 실행되지만, 개발자의 상호작용은 제한된다.
  • 테스트 실행 과정은 브라우저에서 시각적으로 확인할 수 있지만, 개발자가 직접 브라우저를 조작하거나 디버깅 도구를 사용할 수 없다.
  • 테스트 실행 속도와 브라우저 렌더링 속도는 로컬 환경에서의 수동 실행과 다를 수 있다.
    • 이로 인해 npx cypress run --headed에서는 cy.find() 실행에서 런타임 타임아웃 에러가 발생할 수 있다.
  • CI/CD 파이프라인에서는 npx cypress run을 사용하여 완전히 자동화된 테스트 실행을 수행하는 것이 일반적이다.

npx cypress run

  • 이 명령어는 Cypress 테스트를 헤드리스(headless) 모드로 실행한다.
  • 브라우저 창이 열리지 않고 백그라운드에서 테스트가 실행된다.
  • 테스트 실행 과정이 시각적으로 표시되지 않으며, 개발자의 상호작용은 불가능하다.
  • 헤드리스 모드에서는 브라우저의 동작 속도와 렌더링 속도가 다를 수 있다.
    • 이로 인해 npx cypress run에서도 cy.find() 실행에서 런타임 타임아웃 에러가 발생할 수 있다.
  • 테스트 실행 과정을 시각적으로 확인하고 싶지만 개발자의 개입은 최소화하고 싶은 경우에는 npx cypress run --headed를 사용할 수 있다.

정리해보자면 npx cypress open으로 실행할 때는 브라우저에서 직접 테스트를 확인하면서 진행하기 때문에 비동기 작업의 완료를 기다릴 수 있다고한다.
하지만 npx cypress run으로 실행할 때는 자동화된 환경에서 테스트가 빠르게 진행되므로, 비동기 작업이 완료되기 전에 다음 테스트 단계로 넘어갈 수 도 있다는 것이다.
그래서 이 경우 cy.wait()같은 명령어로 명시적인 시간을 추가해주는 작업 등을 해줘야 될수도있다.

그럼 여기서 다시 드는 의문 !

'이름순','거리순'은 둘다 똑같은 sortKey라는 함수를 통해 데이터를 처리하고, 렌더링 로직도 똑같은데,
왜 '이름순'은 통과하고 '거리순'은 통과하지 못하는가?라는 의문이 들었다.
⬇️ 아래는 sort하는 함수이다.

  sortByKey(data: RestaurantInfo[], sorting: SortingValues): RestaurantInfo[] {
    const result = data.slice().sort((a, b) => {
      if (sorting === '이름순') {
        if (a.name < b.name) return -1;
        if (a.name > b.name) return 1;
        return 0;
      }
      if (sorting === '거리순') {
        return a.distance - b.distance;
      }

      return 0;
    });

    return result;
  }

'이름순'정렬과 '거리순'정렬의 차이점은 sortByKey 함수안의 각각의 정렬 로직 차이 밖에 없다.

에러 원인 - 가설 1

따라서 여기서 내린 결론은 '거리순'정렬로직 보다 '이름순'정렬 로직 처리 속도가 더 빨라서, '거리순'정렬 테스트에서만 저런 에러가 나는게 아닌가? 였다.

if (sorting === '거리순') {
  return a.distance - b.distance;
}
if (sorting === '이름순') {
  if (a.name < b.name) return -1;
  if (a.name > b.name) return 1;
  return 0;
}

하지만 '거리순'정렬 로직은 두 숫자 차이 계산이기 때문에 매우 빠르고, '이름순'정렬은 두 문자열 비교이기때문에 비교적으로 숫자열 비교보다 느리다고한다.하지만 이 경우 정렬 로직 둘 다 매우 간단한 연산이기때문에 이런 차이가 테스트 결과에 큰 영향을 미칠 가능성은 낮아보인다.

그럼 도대체 이유가 뭘까?

에러 원인 - 가설 2

곰곰히 생각하던 중 두 정렬의 큰 차이점 하나가 떠올랐다.

바로 각 정렬이 정렬 필터의 기본값인지 아닌지이다.

정렬 필터의 기본값은 "이름순"이었다.
그리고 이 때 '이름순'정렬 테스트는 통과했지만 '거리순'정렬 테스트는 통과하지 못했다.

애초에 첫 페이지가 이름순으로 이미 정렬되어있었기 때문에 '이름순'정렬 테스트에서는 cy.find() 명령어 실행 중 타임아웃 에러가 발생하지 않았고,
'거리순'으로 바꿨을 때 비로소 e2e 테스트에서 sorting로직이 처음 발생한것이고 이에 따라 cy.find() 타임아웃 에러가 발생이 한게 아닐까?

⬇️바로 정렬 카테고리 기본값을 아래와 같이 '거리순'으로 바꿔보았다.

바로 테스트를 돌려보았다.. 과연 내 생각이 맞을까??

맞았다 😭 단순했던 원인을 참 멀리멀리 돌아서 찾은 느낌이다..

그렇다면 해결 방법은?

비동기적인 렌더링이 원인이었기때문에 렌더링이 다 될 때 까지 기다리게 하면 해결된다.

1.명시적으로 대시 시간 추가해주기

  it('거리순을 선택하면 거리순으로 정렬되어야한다.', function () {
    cy.get('#sort-filter').select('거리순');
    cy.wait(2000); // 명시적인 대기 시간 추가

    // ....
      });
  });

2.재시도 옵션 추가

 it('거리순을 선택하면 거리순으로 정렬되어야한다.', function () {
    cy.get('#sort-filter').select('거리순');

    const sortedRestaurants = [...this.restaurantsData].sort((a, b) => a.distance - b.distance);

    cy.get('.restaurant-list-container .restaurant', { timeout: 10000 }) // 재시도 옵션 추가
      .each(($restaurant, index) => {
        cy.wrap($restaurant)
          .find('.restaurant__name')
          .should('have.text', sortedRestaurants[index].name);
        cy.wrap($restaurant)
          .find('.restaurant__distance')
          .should('have.text', `캠퍼스부터 ${sortedRestaurants[index].distance}분 내`);
      });
  });

3.랜더링 완료 확인하기

it('거리순을 선택하면 거리순으로 정렬되어야한다.', function () {
    cy.get('#sort-filter').select('거리순');

    const sortedRestaurants = [...this.restaurantsData].sort((a, b) => a.distance - b.distance);

    cy.get('.restaurant-list-container .restaurant') 
      .should('have.length', sortedRestaurants.length) // 렌더링 완료 확인
      .each(($restaurant, index) => {
        cy.wrap($restaurant)
          .find('.restaurant__name')
          .should('have.text', sortedRestaurants[index].name);
        cy.wrap($restaurant)
          .find('.restaurant__distance')
          .should('have.text', `캠퍼스부터 ${sortedRestaurants[index].distance}분 내`);
      });
  });
  • Cypress.Commands.add('waitForRender', () => {
      cy.get('.restaurant-list-container').should('be.visible');
    });``` // 이렇게 command에 등록후 사용할 수도있음. cy.waitForRender(); 

트러블 슈팅 회고

sort 작업에 비동기적인 작업이 들어간다는 사실을 초장에 잘 짚고 넘어갔으면

비동기적인 렌더링이 원인이다
-> 근데 e2e 테스트 에러는 '이름순','거리순'중에 한 케이스에서만 에러가 난다
-> 그럼 e2e 테스트에서 sort 작업이 한 번만 이루어지는게 아닌가?
-> 카테고리 필터의 초기값 차이?

이렇게 빠르게 에러 원인을 파악할 수 있었을거같은데, 내가 짠 코드를 내가 잘 파악하지 못해 트러블 슈팅을 오랫동안 하게된것같다.

profile
프론트엔드 개발자

0개의 댓글

관련 채용 정보