본문 바로가기
Testing

단순 오타를 통해 배운 React-testing-library의 원칙과 flexible Matcher

by whale in milktea 2023. 7. 27.
import { render, screen } from "@testing-library/react"
import MyPage from "./MyPage"

test("유저가 없으면 로그인 문구와 버튼을 보여준다", () => {
  render(<MyPage />)
  // const loginRequestText = screen.getByText("로그인이 필요합니다") // 오류발생
  const loginRequestText = screen.getByText("로그인이 필요합니다.") // 오류해결
  const loginRequestButton = screen.getByyRole("button")
  expect(loginRequestText).toBeInTheDocument()
  expect(loginRequestButton).toBeInTheDocument()
  expect(loginRequestButton).toHaveTextContent("로그인")
})

에러가 발생한 코드 : 단순하게 볼 때에는 아무런 문제가 없다. 그래서 아래의 오류코드들을 쪼개보면서 이것저것 시도를 했다. 사실 이 결과는 아주 단순하게 오류가 발생한 코드에 마침표를 찍지 않아서 발생한 문제였다.

TestingLibraryElementError: Unable to find an element with the text: 로그인이 필요합니다. 
This could be because the text is broken up by multiple elements. In this case, 
you can provide a function for your text matcher to make your matcher more flexible.

이 과정에서 문득 아주 단순하게 여기던 나의 고정관념을 되짚어봤다.

 

# 의문점

이 테스트를 하는 이유는 테스트에 정의된 대로 동작하는가 아닌가를 테스트하기 위함 아닌가?

근데 왜 테스트코드에서 정의한 텍스트가 테스트의 대상이 되는 텍스트와 다르다고 하여 오류가 뜨는거지?

빠르게 보는 결론
1. RTL의 철학에 위배
2. RTL은 나와 같이 잦은 오타와 여러 경우의 수를 염두해서 유연하게 테스트할 수 있는 환경들을 이미 제공하고 있다.

 

1. RTL의 철학 : 테스트 코드는 사용자에게 보여지는 관점을 중심으로 테스트 되어야 한다.

개발자가 어떤 에러처리의 과정과 견고한 테스트를 하건 사용자에게 보여지는 것은 데이터 그 자체이다.

RTL은 이런 JS와 React의 유사성 철학에 따라 사용자에게 보여지는 그대로 테스트되어야 한다.

이 외에도 RTL 개발진이 테스팅 라이브러리를 개발하면서 가진 철학은 다음과 같다.

Utilities are included in this project based on the following guiding principles:

1. If it relates to rendering components, then it should deal with DOM nodes rather than component instances, and it should not encourage dealing with component instances
-> 컴포넌트 렌더링 시, 컴포넌트에서 생성하는 세부적인 인스턴스에 의존하지 말아야 한다.
-> 이는 컴포넌트 내부에서 발생하는 여러 상태에 따라 테스트하면 안되고, 실제 사용자가 보는 상황에 맞게 테스트해야 함을 의미한다.


2. It should be generally useful for testing the application components in the way the user would use it. We are making some trade-offs here because we're using a computer and often a simulated browser environment, but in general, utilities should encourage tests that use the components the way they're intended to be used.
-> 브라우저 환경에서 일부 성능 및 서버 통신 등 사용자가 알지 못하는 여러 요소들을 테스팅을 진행하기 위해 인스턴스 자체를 고려한 테스팅 환경을 제공하고 있다.
-> 그러나 원칙적으로는 사용자가 실제 사용하는 환경에서 맞추어서 테스트해야 한다.

3. Utility implementations and APIs should be simple and flexible.
-> 테스팅 도구는 간단하면서 유연한 여러 테스팅 환경을 제공해야 한다.

아래의 #에 위의 사용자 중심의 원칙을 지키지 않은 예시 코드를 명시함

#. 컴포넌트 인스턴스에 의존한 코드와 그렇지 않은 코드

// 컴포넌트의 클릭 상태에 따라 테스트하는 코드
import { render, screen } from "@testing-library/react";
import MyComponent from "./MyComponent";

test("컴포넌트의 특정 상태에 따라 텍스트가 다르게 렌더링되는지 테스트", () => {
  const component = render(<MyComponent />);
  // 특정 상태에 따른 텍스트 확인 (이러한 테스트는 컴포넌트의 내부 상태에 의존적)
  if (component.state.isClicked) {
    const textElement = screen.getByText("클릭되었습니다.");
    expect(textElement).toBeInTheDocument();
  } else {
    const textElement = screen.getByText("클릭해주세요.");
    expect(textElement).toBeInTheDocument();
  }
});
// 컴포넌트가 클릭되었을 때, (사용자는 버튼을 클릭해야 어떤 기능이 동작함을 알고 있다.)
import { render, screen } from "@testing-library/react";
import MyComponent from "./MyComponent";

test("컴포넌트의 특정 상태에 따라 텍스트가 다르게 렌더링되는지 테스트", () => {
  render(<MyComponent isClicked={true} />);
  const textElement = screen.getByText("클릭되었습니다.");
  expect(textElement).toBeInTheDocument();
});

 

2. RTL에서 제공하는 여러 유연한 테스팅 도구

1. 정규표현식을 pattern을 활용한다.

정규표현식은 /pattern/ 을 활용하여 해당 패턴이 포함된 결과를 리턴한다. 이를 활용하여 /로그인이 필요합니다/ 로 바꾸어서 패턴을 리턴하면 문제를 해결할 수 있다.

 

2. queryByText

getByText : 해당 텍스트가 포함되어있는지 여부를 검사하고 없을 경우 error를 리턴한다.

queryByText : 해당 텍스트가 포함되어있는지 여부를 검사하고 없을 경우 null을 리턴한다.

-> 즉, 단순히 에러를 발생시키지 않고 여부만을 판단할 경우 query를 사용하면 된다.

 

물론 유연한 도구를 사용할 경우 : Strict Mode와 Lazy Mode의 차이처럼 개발자가 원하는 명확한 검사가 이뤄지지 않을 가능성도 있기에 필요에 따라 사용할 수 있어야 한다.