테스트 코드 잘 작성해보기 ~ Good Code, Bad Code와 함께~

·2023년 7월 18일
0
post-thumbnail

서론

최근 Good Code, Bad Code 책을 완독하며 "좋은 단위 테스트"에 대하여 인상깊게 보게 되었다.
책을 읽었으니 이제 실천해 볼 차례!
최근 진행하고 있는 프로젝트에서 작은 단위의 기능 테스트에 책에서 얘기한 방법론을 적용해보기로 했다.


요구사항


컴포넌트의 기능은 간단하다.

  1. 내려주는 props에 따라 상품의 타입이 스위치 된다.
  2. 구매가 즉시 결정되는 상품은 “즉시 구매”, 예약은 “대기 예약” 이라는 뱃지 문구를 노출한다.
  3. 즉시 구매 뱃지는 blue 색상을, 대기 예약 뱃지는 pink 색상의 디자인을 갖고 있다.

너무 간단한 기능이라서 구현이은 전혀 어렵지 않다. API 호출에 따라 들어오는 데이터를 boolean 타입의 prop로 전달해 분기처리한다.

요구사항 또한 심플하기 때문에 어렵지 않은 테스트가 예상되지만,
나는 테스트 코드 작성 경험이 적고, 어디서부터 어디까지 뭘 해야할지 모르는 테린이기도 하고🥲.. 
책에서 배운 내용을 최대한 녹여내는 연습이 목적이기 때문에 이렇게까지 하는게 맞을까? 싶을 정도로 고민해보았다.

그 결과, 하나의 뱃지당 요구사항은 2개였기 때문에, 총 4개의 테스트코드가 나왔다.

우선 책 내용을 신경쓰지 않고 평소 나였다면 썼을 법한 테스트 코드를 우선 작성하고, 이후 책 내용을 다시 보며 코드를 개선했다.

고작 이런 간단한 네 개의 테스트 케이스인데 리팩토링만 세 번을 했다.

결과 코드

사용한 라이브러리는 프론트엔드 TDD에서 주로 자주 사용되는 Testing-Library, jest를 이용하였으며
컴포넌트는 React코드로 작성되었다.

beforeEach(cleanup);

describe('Badge Component에서 reservationBadgeStat props의 boolean값이', () => {

  test('true이면 "즉시 구매" 텍스트가 노출된다.', () => {
    render(<Badge reservationBadgeState />);
    const badge = screen.getByRole('paragraph');
    expect(badge.textContent).toEqual('즉시 구매')
  });

  test('true이면 blue 색상의 글씨와 배경의 클래스를 갖고있다', () => {
    render(<Badge reservationBadgeState />);
    const badge = screen.getByRole('paragraph');
    expect(badge).toHaveClass('bg-blue/10 text-blue-dark');
  });

  test('false이면 "대기 예약" 텍스트가 노출된다.', () => {
    render(<Badge reservationBadgeState={false} />);
    const badge = screen.getByRole('paragraph');
    expect(badge.textContent).toEqual('대기 예약')
  });

  test('false이면 pink 색상의 글씨와 배경의 클래스를 갖고있다', () => {
    render(<Badge reservationBadgeState={false} />);
    const badge = screen.getByRole('paragraph');
    expect(badge).toHaveClass('bg-pink/10 text-pink');
  });

});
{reservationBadgeState ? (
	<p className="before:bg-check-blue bg-blue/10 text-blue-dark">즉시 구매</p>
	) : (
	<p className="before:bg-check-pink bg-pink/10 text-pink">대기 예약</p>
)}

Good Code, Bad Code에서 말하는 좋은 테스트 코드란 여러가지가 있지만, 그 중 내가 집중하여 고민해보고자 한 것은 아래의 세가지가 있다.

  1. 잘 설명되는 실패
  2. 훼손의 정확한 감지
  3. 구현 세부사항에 대해 독립적

위 내용을 따르기 전의 나의 첫 테스트 코드를 💩 Bad Code 로,

이후 개선한 코드를 ✨ Good Code 로 예시를 들어

좋은 테스트 코드에 대해서 이야기해보고자 한다!


본문

1. 잘 설명되는 실패

💩 Bad Code

test('reservationBadgeState가 false이면 대기 예약 뱃지가 렌더된다', () => {
    render(<Badge reservationBadgeState={false} />);
    const badge = screen.getByRole('paragraph');
    expect(badge).toHaveTextContent('대기 예약');
    expect(badge).toHaveClass('bg-pink/10 text-pink');
  });

위 테스트 코드는 두 가지 요구사항을 테스트하는 점으로 Bad Code에 해당된다.

  1. 뱃지에 대기 예약이라는 텍스트가 있는지
  2. pink 색상에 해당하는 class를 포함하고 있는지

만약 둘 중 하나의 기능이 실패할 경우, 테스트는 무엇을 고장냈는지 잘 설명하지 못하고 그저 렌더가 실패했음을 알려주게 된다.

✨ Good Code

이에 위 테스트 케이스를 한 가지 사항만 검사하도록 나누면 아래와 같은 형태가 된다. (최종 완성은 아니고 분리만)

  test('reservationBadgeState가 false이면 대기 예약 텍스트를 노출한다', () => {
		render(<Badge reservationBadgeState={false} />);
    const badge = screen.getByRole('paragraph');
    expect(badge).toHaveTextContent('대기 예약');
  });

  test('대기 예약 뱃지는 pink 색상을 갖고있다', () => {
    render(<Badge reservationBadgeState={false} />);
    const badge = screen.getByRole('paragraph');
    expect(badge).toHaveClass('bg-pink/10 text-pink');
  });

두 개로 나뉘어서 중복 코드가 많아진 것이 이상해 보이지만 두 요구사항이 독립적으로 분리되어 추후 기능의 변화나 리팩토링을 하면서 테스트가 실패하더라도 어디서 실패했는지 빠르고 명확히 알 수 있다는 점에서 훨씬 Good Code 라고 볼 수 있을 것 같다.

2. 훼손의 정확한 감지

즉시 구매가 완료되는 상품은 “즉시 구매” 라는 문구를 노출하는 요구사항이 있었다.

나는 아래와 같이 총 세 가지의 테스트 코드를 작성하게 되었고 전부 일반적으로 기대하는 테스트의 역할을 잘 수행해주긴 한다. 누군가는 굳이? 라고 할 수 있지만 하지만 훼손의 정확한 감지를 목적으로 코드를 작성하고 있기 때문에 최악의 최악을 가정하고 코드를 작성하게 되었다 😅

💩 Bad Code 1

 test('reservationBadgeState가 true이면 즉시 구매 텍스트를 노출한다', () => {
    render(<Badge reservationBadgeState />);
    expect(screen.getByText('즉시 구매')).toBeInTheDocument()
  })

getByText는 주어진 값과 >일치<하는 텍스트 노드가 있는 요소를 검색한다. 정확히 일치하는 텍스트를 찾고는 있지만, 만약 잘못된 분기 처리로 “대기 예약” 뱃지를 보여줘야 할 때 “즉시 구매”의 뱃지도 렌더링 되었다면? 이때도 테스트는 통과하기 때문에 정확한 감지라고 하기엔 아쉬움이 있었다.

💩 Bad Code 2

  test('reservationBadgeState가 false이면 대기 예약 텍스트를 노출한다', () => {
		render(<Badge reservationBadgeState={false} />);
    const badge = screen.getByRole('paragraph');
    expect(badge).toHaveTextContent('대기 예약');
  });

이번에는 getByRole 을 통해 p태그를 찾고있다. 이 쿼리는 스크린 내에서 단 하나의 p태그만 있을 것으로 기대하기 때문에 다른 뱃지가 있다면 테스트에 실패한다. 그리고 toHaveTextContent 로 텍스트의 여부를 확인했지만, 아쉽게도 해당 메소드는 “정확한 일치 여부”를 알려주지 못했다.
예를 들어 “대기 예약 123” 라는 문구를 노출 해도 테스트는 통과되기 때문에, 이는 훼손을 정확히 감지해주지 못했다.

✨ Good Code

test('reservationBadgeState가 false이면 대기 예약 텍스트를 노출한다', () => {
	render(<Badge reservationBadgeState={false} />);
  const badge = screen.getByRole('paragraph');
  expect(badge.textContent).toEqual('대기 예약')
});

위 1,2 의 케이스를 합쳐 정확한 한 개의 태그 안에 텍스트가 요구사항과 정확히 일치하는지 검증하고있다.

💡 **반대로 뱃지가 없는 상황도 테스트해야 하는거 아닌가?**

더 꼼꼼한 테스트를 위해 반대의 케이스를 not.toBeInDocument()로도 검사 해야하는가 더 고민해보았지만 이 테스트에선 다른

태그가 없다는 것도 기대하고 있기 때문에 충분하다고 판단해 그만두었다. 또한 뱃지는 추후 다른 케이스가 다양하게 추가될 여지가 있어, 그러면 관리해야할 반대의 테스트도 늘어나 유지보수 면에서 좋지 않다고 생각했다.

3. 구현 세부 사항에 독립적

💩 Bad Code

test('즉시 확정 뱃지는 blue 색상을 갖고있다', () => {
    render(<Badge reservationBadgeState />);
    const badge = screen.getByRole('paragraph');
    expect(badge).toHaveStyle({ backgroundColor: "#01C5FD" });
  });

어떻게 보면 이렇게 CSS style 자체를 검증하는 것은 명확한 테스트로서의 장점이 있겠지만,

나는 이 프로젝트에서 tailwind CSS를 사용중이고 모든 색상 코드는 테마로 관리하고 있었다. 그렇기 때문에 blue 색상이 어떤 색상코드를 갖고 있는지 확인하는 것은 오히려 구현 세부 사항에 가깝다.

만약 추후 blue의 테마 색상이 다른 코드로 변경된다면 해당 테스트도 함께 수정해줘야 하는 상황이 벌어질 것이다.

✨ Good Code

test('reservationBadgeState가 true이면 blue 색상의 글씨와 배경의 클래스를 갖고있다', () => {
    render(<Badge reservationBadgeState />);
    const badge = screen.getByRole('paragraph');
    expect(badge).toHaveClass('bg-blue/10 text-blue-dark');
});

근본적으로 내가 원하는 것은 blue 색상의 배경과 글씨색을 갖고 있는지 테스트 하는 것이고, 이는 tailwind CSS로 지정한 class를 갖고 있는지로 충분한 검증이 될 것이라 판단했다.

+)

색상을 테스트 할때는 “즉시 확정”이라는 뱃지로 검증하지 않고 단순히 boolean값에 따른 테스트로만 작성했는데, 뱃지의 텍스트와 색상은 별개로 분리되어야 더욱 독립적인 테스트가 완성되어 명확한 훼손을 감지하고 실패를 더 잘 설명할 수 있을 것이라 생각했다.

마치며, 느낀 점

결국 테스트코드도 개발 과정이고 좋은 테스트 코드를 작성한다는 것은 더욱 단단한 프로젝트를 만드는 일이라는 것을 다시 한번 깨달았다.  

당장 완성하는 것에 몰두하지 않고 개발 요구사항에 대한 명확한 이해와 가독성, 사이드 이펙트의 가능성 등에 대해서 더 꼼꼼히 고민하게 되어 TDD의 장점이 더욱 빛나는 것을 느꼈다. 간혹 디테일 한 부분을 놓치는 내 부주의함을 극복하기 위해선 역시 테스트 코드 작성이 정답이다 😂 당장은 귀찮은 과정이어도 습관이 될 수 있도록 열심히 공부해야지

+)
이 글에 작성된 생각과 방법들은 책을 읽고 든 지극히 저의 개인적인 감상이며
아직 많은 것이 부족한 주니어 개발자입니다🥲
혹시 누군가 이 글을 읽고 다른 느낀점이 있다면 얼마든지 댓글 부탁드립니다 🙏🏻 감사합니다

profile
나 예인쓰, 응애인디

1개의 댓글

comment-user-thumbnail
2023년 7월 18일

정말 유익한 글이었습니다.

답글 달기