Test Tip

테스트 실행 숏컷

  • vscode >Keyboard Shorcuts > test.run > run test in current file or run test at cursor

VSCode Extension

올바른 테스트 작성법

인터페이스를 기준으로 테스트 작성하기

  • 내부 구현에 대한 테스트 코드는 강한 의존성 때문에 깨지기 쉽고 유지보수 하기 어렵다.

  • 인터페이스를 기준으로 캡슐화에 위반되지 않으며 종속성이 없는 테스트를 작성하자

100% 커버리지보다는 의미 있는 테스트인지 고민

  • 커버리지를 쫓다보면 큰 유지 보수 비용이 발생하며 제데로 된 검증이 되었다는 착각이 들수 있다.

  • 의미있는 테스트가 무엇인지 고민해보자

가독성 있는 테스트 코드 작성법

1. 테스트 하고자 하는 내용을 명확히 작성하기

테스트 디스크립션을 상세히 명확히 작성하는것이 앱의 동작을 이해하는데 도움된다.

// 검증 대상 - 리스트에서 체크된 항목들 제외

// BAD
it('리스트에서 항목이 제데로 삭제된다.', () => {})

// GOOD
it('항목들을 체크한 후 삭제 버튼을 누르면 리스트에서 체크된 항목들이 삭제된다.', () => {})

2. 하나의 테스트에는 가급적 하나의 동작만 검증하자

테스트 코드에서도 검증 범위의 책임을 나누는것이 중요하다.

  • 간단한 테스트는 상관없지만, 다양한 컴포넌트들이 조합되었을 때 시나리오를 검증해야 한다면 하나의 테스트에서 한번에 검증하는것이 아닌 여러 개로 나누어 검증하는 것이 가독성과 유지보수성이 높다.

  • 검증에 필요한 코드들이 많아지고 디스크립션도 장황해진다.

  • 여러 개를 검증하다 보니 특정 로직이 수정되어도 테스트 자체가 깨지게 된다.

  • 어떤 동작에서 테스트가 실패했는지 파악하기 어려울 수 있다.

  • 검증의 책임이 명확히 나눠져 있기 때문에 이런 테스트 코드는 유지보수하기 쉽다.

// BAD
it('장바구니를 담긴 상품들이 정상적으로 노출되고, 수량을 변경하면 가격이 재계산 된다. 그리고 삭제 버튼을 누르면 상품이 삭제된다.', 
  () => {}
 )
 
 // GOOD
it('장바구니에 담긴 상품들을 정상적으로 렌더링 한다.', () => {})
it('장바구니에 담긴 상품의 수량을 수정하면 가격이 재계산 된다.', () => {})
it('장바구니에 담긴 항목의 삭제 버튼을 누르면 리스트에서 삭제된다', () => {})

단일 책임 원칙 (SRP, Single Responsibility Principle)

  • 모든 클래스는 하나의 책임을 갖고, 그와 관련된 책임을 캡슐화하여 변경에 견고한 코드를 만들어야 한다.

좋은 테스트 구조

테스트는 독립적인 테스트 케이스가 있고, 테스트 전후에 실행되는 것들(Before**, After**)이 있다.

beforeAll(() => {})
beforeEach(() => {})
afterEach(() => {})
afterAll(() => {})

it('...', () => {
  // Arrange, Given
  const productService = new ProductService(new StubProductService());
  
  // Act, When
  const items = await productService.fetchAvailableItems();
  
  // Assert, Then
  expect(items.length)toBe(1);
  expect(items).toEqual([{ item: 'A', available: true }])
})

준비 (Arrange, Given)

테스트를 위한 object를 생성하고 여러가지 데이터를 준비하는 과정

  • 준비하는 단계에서는 준비과정을 여러개의 테스트에 걸쳐서 반복해서 사용한다면 재사용할 수 있도록 유틸리티 함수로 정의해서 사용

실행 (Act, When)

테스트하고자 하는 코드를 실행

  • 코드를 실행했을 때 의도적으로 실패해보기

  • expect에 다른값을 넣는다든지, 실패하도록 만든 뒤에 실패했을 때 실패하지 않기 위해서 코드를 어떻게 수정해야하는지 확인해보기

  • 버그를 수정할 때 실패하는 테스트를 먼저 만든 후, 버그가 이런 상황에서 발생하구나를 검증한 다음에 버그를 수정해서 이 테스트 코드가 성공할 수 있도록 만드는것이 중요하다.

    • 실패하지 않는 테스트는 필요 없다.

    • 모든 테스트 코드는 의도적으로 실패할 수 있어야 한다.

검증 (Assert, Then)

실행한 코드를 우리가 예상한값과 같은지 검증

  • 내가 하나의 테스트 함수 안에서 검사하는것이 많다면, 여러개의 테스트로 분리할수 없는지 고민해보자.

좋은 테스트 원칙

First 원칙

  • Fast: 느린것에 대한 의존성 낮추기,

    • 테스트가 빠르게 수행되어야지 몇개의 테스트를 가지고 있더라도 빈번히 테스트를 수행해서 문제가 없는지 검증하는것이 중요

  • Isolated: 최소한의 유닛으로 검증하기, 독립적이고 집중적으로 유지

    • 하나의 테스트에서 너무 많은것을 동시에 테스트해서 어디서 어떤것이 잘못되어 테스트가 실패했는지 잘 모르게 작성하지 말고 최소한의 단위로 어디서 실패했는지 한눈에 알 수 있도록 독립적으로 작성

  • Repeatable: 반복이 가능하도록 만들어라, 테스트코드가 실행될 때마다 동일한 결과 유지

    • 언제, 몇번 실행하냐에 따라서 다른 결과를 제공한다면 나쁜 테스트 코드

    • 다른 테스트 코드에 의존하거나, 외부적 환경(네트워크, 디비)에 의존하는 테스트는 불안정

    • 환경에 영향을 받지 않도록.. 작성

  • Self-Validating: Assert library(expect, tobe 등)을 사용하면 스스로 결과를 검증 가능

    • 우리가 사용한 테스트 라이브러리는 기본으로 제공

    • 자동화를 통한 검증단계 도입(CI/CD)

      • 새로운 기능을 추가할 때 기존 테스트 코드에 영향을 주는지 확인하지 않으면 테스트 코드는 불필요

  • Timely: 시기적절하게 테스트 코드 작성

    • 코드를 추가하거나 기능이 수정되고 나서 사용자에게 배포된 후 테스트 코드 작성하는건 의미가 없다.

    • 코드 작성할 때, 리팩토링 전, 사용자에게 배포하기 전에 시기적절하게 테스트 코드 작성해서 예상하지 못한 문제를 빠르게 잡아내는것이 중요

좋은 테스트 범위

Right-BICEP, 모든 요구사항이 정상 동작하는지 확인

  • Boundary conditions: 모든 코너케이스에 대해 테스트하기

    • ex: 잘못된 포맷의 인풋, null, 특수문자, 잘못된 이메일, 작은 숫자, 중복, 순서가 맞지 않는 경우

  • Inverse relationship: 역관계를 적용해서 결과값을 확인

    • 일관성을 유지 (덧셈 -> 뺼셈, 추가 -> 제거)

  • Cross-check: 다른 수단을 이용해서 결과값이 맞는지 확인

    • ex: 추가된 과일 갯수를 구하는 함수를 테스트 한다면, 결과값이 맞는지 확인하기 위해 전체 과일 갯수 - 예전 과일 갯수 값과 동일해야 한다

    • 특정 알고리즘을 구현했다면 정확하게 동작하는지 확인하기 위해서 동일한 알고리즘을 구현한 다른 라이브러리를 이용해서 우리 알고리즘과 라이브러리 알고리즘 결과값이 똑같아야 한다.

  • Error conditions: 불행한 경로에 대해 우아하게 처리 하는가?

    • 네트워크 에러, 메모리 부족, 데이터베이스 중지 등

    • 예상할 수 있는 모든 에러 케이스에 대해서 테스트가 통과하는지

  • Performance chracteristic: 성능 확인은 테스트를 통해 정확한 수치로 확인

    • 성능 개선의 척도와 확인도 데이터를 통해 확인

좋은 테스트 조건

테스트도 각각 상황 및 조건에 맞게 어떤 결과값을 예상하는지를 테스트하는것이 중요 -> CORRECT 원칙

단순히 하나의 사실에 의거해서 테스트를 작성하는게 아닌, 여러가지 조건들을 테스트해서 테스트 코드도 꼼꼼히 작성

CORRECT 원칙은 테스트 코드 뿐만 아니라 베이스 코드를 작성할 때도 유념

  • Conformance: 특정 포맷을 준수

    • 전화번호, 이메일, 아이디, 파일 확장자 등

    • 인풋이 포맷에 적합할 때, 적합하지 않을 때, 우리 코드가 어떤식으로 동작하는지 예상하는 테스트 코드를 작성

  • Ordering: 순서 조건 확인하기

    • 순서가 중요한 경우

    • 테스트 코드가 배열의 순서를 중요하게 생각하는 코드라면, 순서가 잘못되었을 때 코드가 어떻게 반응할것인지 예상하는것들도 모두 테스트로 나타내야함

  • Range: 숫자의 범위

    • 제한된 범위보다 크거나 작은 경우

  • Reference: 외부 의존성 유무, 특정한 조건의 유무

    • ~일때, ~가 되었을 때, 어떤 특정한 상황/상태 일경우 이런 동작을 한다.

  • Existence: 값이 존재하지 않을 때 어떻게 동작?

    • null, undefined, 0, ''

  • Cardinality: 0-1-N 법칙에 따라 검증

    • 하나도 없을 때, 하나만 있을 때, 여러개 있을 때

  • Time: 상대, 절대, 동시에 일들

    • 순서가 맞지 않은 경우, 소비한 시간, 지역 시간

    • 순서가 맞지 않을 때, 특정 시간을 지나치게 소비했을 때, 시간을 검사한다면 지역마다 나라마다 시간이 달라지는 경우 코드가 어떻게 동작하는지 검사

무엇을 중요하게 생각해야 하나?

  • itteration speed vs Realistic envirionment

    • itteration speed: 특정한 일을 시작해서 끝날때까지 걸리는 속도

      • 즉, 기능 구현후 테스트 코드 작성하고 검증하는 단계를 한바퀴 돌아오는 속도

      • 코드 수정 후 기존 테스트를 동작해서 결과 확인해서 완성하는 한바퀴를 iteration이라 한다.

      • 테스트 작성하고 테스트 동작하는 속도가 빠르면 빠를 수록 iteration speed는 빠르다고 볼 수 있다.

    • Realistic envirionment: 사용자가 실제로 앱을 사용했을 때의 환경과 최대한 가까운것

"테스트 도구를 선택할 때는 itteration speedRealistic envirionment이라는 몇 가지 절충점을 고려하는 것이 좋습니다: 일부 도구는 변경 후 결과 확인까지 매우 빠른 피드백 루프를 제공하지만 브라우저 동작을 정확하게 모델링하지 못합니다. 다른 도구는 실제 브라우저 환경을 사용하지만 반복 속도가 느리고 CI 서버에서 더 취약(flaky) 할 수 있습니다"

  • How much to Mock

    • 얼마만큼 Mock을 할것인가

    • 컴포넌트를 테스트하는 경우 유닛인지 통합인지 경계가 불명확하다.

    • 어디까지 유닛테스트로 부르고, 어디까지 통합 테스트로 부를것인지는 사실 중요하지 않다.

      • util, api, 단일 컴포넌트는 유닛 테스트, 다른 컴포넌트 또는 외부 리소스를 사용한다면 통합 테스트 ㅇㅇ

"컴포넌트의 경우 '단위' 테스트와 '통합' 테스트의 구분이 모호할 수 있습니다. 폼을 테스트하는 경우 해당 테스트에서 폼 내부의 버튼도 테스트해야 할까요? 아니면 버튼 컴포넌트에 자체 테스트 스위트가 있어야 할까요? 버튼을 리팩토링하면 양식 테스트가 중단될 수 있을까요? 팀과 제품에 따라 다른 답이 나올 수 있습니다."

React Testing Library

  • 조금 더 테스트를 간편하게 만들어주고, 테스트 자체만으로 유지보수성을 높혀줌

  • 리액트 컴포넌트를 내부 구현사항에 의존하지 않고 테스트를 간편하게 만들어줌

    • 리엑트 컴포넌트를 테스트할 때 내부적으로 어떤 CSS를 사용하는지, 어떤 코드를 가지고 있는지 구현사항에 의존하지 않고 화면상에, 사용자 입장에서 '특정 텍스트를 갖는 버튼을 가져와서 클릭하거나, 텍스트가 보이는지 안보이는지' 조금 더 외부적인 사용자 관점에서 테스트를 작성할 수 있도록 도와줌

    • 이런 접근법은 리팩토링을 쉽게 만들어주고, 웹 접근성과 코드를 작성하는데 좋은 원칙을 따라갈 수 있게 도와줌

    • 검증하려는 기능과 관계없는 코드가 수정되도 테스트가 실패하는 상황을 만들지 말자

  • 단점으로 RTL 자체로는 자식 노드들을 가볍게 렌더링하는건 제공하지 않음

    • 컴포넌트 하나만 테스트하더라도 내부 컴포넌트들을 모두 렌더링하게됨

    • 내부 컴포넌트들이 갖는 의존성까지 모두 설정해줘야해서 까다로움

      • jest/vitest 의 mock 활용

"React 테스트 라이브러리는 구현 세부 사항에 의존하지 않고도 React 컴포넌트를 테스트할 수 있도록 도와주는 헬퍼 세트입니다. 이 접근 방식을 사용하면 리팩토링이 쉬워지고 접근성을 위한 모범 사례로 나아갈 수 있습니다. 자식 없이 컴포넌트를 '얕게' 렌더링하는 방법은 제공하지 않지만, Jest와 같은 테스트 러너를 사용하면 모킹을 통해 이를 수행할 수 있습니다."

Last updated