💻 Frontend

코드숨_코드리뷰 스터디 3주차 회고

date
May 21, 2023
slug
codesoom3
author
status
Public
tags
React
TDD
summary
코드숨 코드리뷰 스터디 3주차 회고_테스트코드
type
Post
thumbnail
코드숨3주차.png
category
💻 Frontend
updatedAt
May 21, 2023 12:56 PM

학습한 내용

이번 주차에서는 Jest와 React-testing-library를 활용하는 방법과 테스트코드를 BDD 방식으로 구성하는 방법에 대해서 학습했다.
 
여러 내용들을 학습했기 때문에 너무 길어져서 토글로 담았다.
가장 많이 사용되는 Jest, React-testing-library의 기본적인 사항들과, 활용할 수 있는 다양한 장치(함수모킹, 디버깅), BDD를 학습했다.
 
1. Jest란 무엇인가

1. What is Jest

jest는 자바스크립트 테스트 프레임워크이다.
바닐라 자바스크립트에서도 사용이 가능하며, 모던 프레임워크 타입스크립트도 지원하며, babel 적용까지 모두 지원한다. 아래와 같은 장점이 있다.
 
Jest의 장점
 

1-1) 설치

npm i -D jest @types/jest babel-jest

@types/jest

Jest의 타입 정의를 가지고 있는 모듈입니다. TypeScript에서 주로 사용되지만 이 모듈을 설치하게 되면 편집기 내에서 자동완성을 지원하기 떄문에 설치한다.

babel-jest

jest를 babel로 jsx를 통해 컴파일해서 사용하고 있기 때문에 babe-jest도 설치해야한다.
 

1-2) 기본적인 테스트 구조

// test를 실행하기 위해서는 jest에서 지원하는 test()함수를 이용
test('name', ()=>{
	// assertion => A(actual)가 B(expect)여야 한다.
	expect(1 + 1).toBe(2);
})

assertion

test 함수는 테스트명과, 단언문(함수)를 통해 테스트가 이루어진다.
단언문이란, 테스트를 하고 싶은 내용이며 이 단언문은 “A(actual)가 B(expect)여야 한다.”는 내용을 담는다.
 

Signature

test('add', ()=>{
	expect(add(1,3)).toBe(4);
})
expect에 넘겨주는 특정 함수를 Signature라고 한다.
위에서 Signature는 name(add), parameters(x,y), return(result)로 구성된다.
 

npx jest(테스트 실행)

npx jest
npx jest --watchAll
작성한 jest를 실행할 때는 npx jest를 하면 테스트 결과가 터미널에 나온다.
이 때 저장할 때마다 jest를 실행하기 위해서는 npx jest --watchAll을 실행하면 자동으로 실행한다.

TDD Cycle

// RED
function add(){
	return null
}

// GREEN
function add(){
	return 4
}

// REFACTORING
function add(x,y){
	return x+y
}

test('add', ()=>{
	expect(add(1,3)).toBe(4);
})
TDD 사이클은 RED-GREEN-REFACTORING의 단계를 거친다.
RED 단계 : 실패하는 테스트를 작성하는 단계를 ,
GREEN 단계 : 성공시키기 위한 프로덕션 코드 작성. 답을 하드코딩하는 것도 가능
REFACTORING 단계 : 테스트에 통과하는 코드를 작성하고, 리팩토링하는 단계
2. React-testing-library란 무엇인가

2. React에서 사용하기 (with React-testing-library)

React-testing-library이란?

리액트 테스팅 라이브러리는 사용자와 동일한 방식으로 DOM 쿼리를 사용할 수 있게 해준다. 실제 사용자가 우리의 앱을 사용하는 방식으로 테스트하여 우리의 앱이 올바르게 동작하는지 테스트할 수 있다.
 

React-testing-library 설치 및 사용 (render)

@testing-library/jest-dom은 jest의 matcher들을 확장하여 테스트의 의도를 더 명확하게 표현할 수 있다.
npm i -D @testing-library/react @testing-library/jest-dom
import { render } from '@testing-library/react';

// 모든 곳에서 import하기 귀찮으니까 
// jest.config.js와 jest.setup.js에서 모듈 설정하기
import '@testing-library/jest-dom'

// jest.config.js
// module.exports = {
//   setupFilesAfterEnv: [
//    './jest.setup',
//  ],
// }

// jest.setup.js
// import '@testing-library/jest-dom';
render를 시켜주는 함수를 React-testing-library에서 import하고
dom을 활용하기 위해서는 jest dom을 import한다. 이 때 jest dom은 거의 모든 테스트 파일에서 사용하기 때문에 별도의 config와 setup 파일 설정을 통해 보통 사용한다.
 

테스팅에서 DOM 이벤트 발생시키기 (fireEvent)

import { render, fireEvent } from '@testing-library/react';

import Item from './Item';

test('Item', () => {
  const task = {
    id: 1,
    title: '뭐라도 하기',
  };

  const handleClick = jest.fn();

  const { container, getByText } = render((
    <Item
      task={task}
      onClickDelete={handleClick}
    />
  ));

  expect(container).toHaveTextContent('뭐라도 하기');
  expect(container).toHaveTextContent('완료');

  expect(handleClick).not.toBeCalled();

  fireEvent.click(getByText('완료'));

  expect(handleClick).toBeCalledWith(1);
});
  • container : 보통 render함수를 container 객체에 담아 사용한다.
  • toHaveTextContent : 렌더한 테스트에서 특정 content를 갖고 있어야한다.
  • getByText : 특정 텍스트를 통해 특정 dom에 대한 정보를 가져온다.
  • fireEvent : 테스팅에서 DOM 이벤트를 편리하게 발생시켜주는 메서드입니다. click, change 등의 이벤트를 발생시킬 수 있다.
3. Jest에서 함수 모킹하기

3. 함수 모킹하기

3-1) jest.fn()

jest.fn()이란 함수를 모킹할 때 사용한다.
why? 일반적으로 테스트하려는 코드가 의존하는 부분을 직접 생성하기가 너무 부담스러운 경우 mocking을 사용한다.
 
사용법은 간단하다. 아래와 같이 모킹하고자 하는 함수를 선언해주면 끝이다
특히 리액트에서 함수를 실행할 때는 실제 함수 호출이 아니라 props로 전달해주는 경우가 많은데
이 때 해당 함수가 실행되었는지 확인용으로 테스트코드를 짤 때 굉장히 유용하다.
const handleClick = jest.fn();
 
이러한 mock function은 해당 값들을 기억하기 때문에 몇번 불렸는지, 어떤 인자를 넘겨줬는지 알 수 있다.

mockFn("a");
mockFn(["b", "c"]);

expect(mockFn).toBeCalledTimes(2);
expect(mockFn).toBeCalledWith("a");
expect(mockFn).toBeCalledWith(["b", "c"]);
 
또한 default value는 undefined인데, 이를 조정하는 것도 가능하다.
아래와 같이 3가지의 메서드를 사용할 수 있다.
mockReturnValue(리턴 값) : 어떤 값을 리턴해야할지 설정 가능 mockResolvedValue(Promise가 resolve하는 값) : 가짜 비동기함수 생성 mockImplementation(구현 코드) : 함수 재구성
mockFn.mockReturnValue("I am a mock!");
console.log(mockFn()); // I am a mock!

mockFn.mockResolvedValue("I will be a mock!");
mockFn().then((result) => {
  console.log(result); // I will be a mock!
});

mockFn.mockImplementation((name) => `I am ${name}!`);
console.log(mockFn("Dale")); // I am Dale!
 

3-2) jest.spyOn()

jest.spyOn()은 가짜 함수를 생성하는게 아니라 선언된 함수를 “몰래” 추적하는 것이다. 실제 함수를 호출하는게 아니라 호출이 된다면~ 이라는 가정을 갖고 얼마나, 어떤 값을 가지고 호출이 되는지 spy로 붙는 것이다.
(사실 몰래 한다는 것이 어떤 식으로 이루어지는지는 아직 명확히 이해는 못했다)
const calculator = {
  add: (a, b) => a + b,
};

const spyFn = jest.spyOn(calculator, "add");

const result = calculator.add(2, 3);

expect(spyFn).toBeCalledTimes(1);
expect(spyFn).toBeCalledWith(2, 3);
expect(result).toBe(5);
이렇게 하면 실제로 함수는 호출이 되지는 않으나, add함수에 spy가 붙어서
호출이 된다고 가정하고, 호출 횟수와 인자값, 결과값을 알 수 있다.
 
참고자료)
 

3-3). clearMock

함수를 모킹할 때 모킹한 함수는 정리를 해주는 것이 좋다.
 

왜 정리해야 하는가?

다음 테스트 케이스를 실행하기 전에는 현재 테스트 케이스에서 사용했던 Mock을 정리해주는 것이 좋다. 다음 테스트 케이스에 영향을 줄 수도 있기 때문이다. 예를 들어 console.log 의 테스트 대역을 만들기 위해 jest.spyOn을 사용한 이후에는 console.log 는 다른 함수가 될 수도 있다.
아래 테스트가 통과하는 것으로 위의 내용을 검증해볼 수 있다.
const consoleLog = console.log;
test("spyOn으로 console.log를 mocking하면, console.log는 다른 함수가 된다.", () => {
  jest.spyOn(console, "log");
  const consoleLogAfterMocking = console.log;
  // 결과는 Success.
  // console.log는 spyOn으로 mocking한 이후 다른 함수가 된다.
  expect(consoleLog).not.toBe(consoleLogAfterMocking);
});
물론 spyOn은 테스트 대역을 만들 때 유용하게 사용될 수 있지만, 다른 테스트 케이스 입장에서는 사이드 이펙트를 일으킨 주범이 될 수 있다. 따라서 모든 테스트 케이스가 독립적으로 실행되게 하기 위해선 mock을 정리하는 것이 좋다.
 

정리하는 방법

정리를 하는 메서드는 크게 3가지이다. 갈 수록 정리하는 방법이 더욱 강력(?)해진다.
clearMocks는 mockFn.mock.calls 와 mockFn.mock.instances 를 초기화하고,
resetMocks는 mock 함수의 구현(ex. jest.fn() 에 넘기는 함수)을 undefined 을 반환하는 빈 함수로 초기화
restoreMocks는 spyOn으로 spy한 객체값들 모두다 초기화한다.
jest.clearMocks
jest.resetMocks
jest.restoreMocks
 
구체적인 것은 아래의 글에서 확인가능하다.
 
4. beforeEach와 afterEach

4. beforeEach, afterEach

  • 테스트를 하기 위해 선행되거나 이후에 적용되어야 할 것들을 before Each와 afterEach를 통해 한번에 적용시킬 수 있다.
  • 해당 기능은 다음에 테스트코드를 짤 때 경험해보면서 더 학습하기로 한다.
5. 테스트코드 작성하다가 디버깅하기

5. 디버깅하기

테스트하다가 막힐 때 현재까지 어떤 작업을 했는지 알 수 있는 것이 있다.
아래와 같이 디버그 함수를 호출하면 호출지점에서 어떤 작업까지 테스트가 되었는지 알 수 있다.
import { screen } from '@testing-library/react';

screen.debug()
6. 무엇을 어떻게 테스트할 것인가? (컴포넌트의 역할과 BDD 적용)

6. 테스트코드를 짤 때 가장 중요한 2가지

테스트 코드를 짜야하는데 어떻게 시작할지 막막하다면 2가지를 해봐야한다.
  1. 테스트코드를 짜고자 하는 컴포넌트의 역할은 무엇인지
  1. Given-When-then으로 나누어 테스트하고자 하는 것이 무엇인지
 

6-1) 테스트 코드를 짜고자 하는 컴포넌트의 역할은 무엇인가?

윤석님이 주신 코드리뷰를 보면 해당 내용이 잘 드러나있다.
컴포넌트는 대게 함수만 전달하는 컴포넌트에서는 함수만 전달하고, 해당 결과값이 무엇인지는 모른다.
이와 같이 테스트코드도 우리가 만드는 컴포넌트에 대해서만 테스트를 해야한다.
 
 

6-2) Given-When-then으로 나누기

주어진 값과 특정행동 결과값 3가지로 나누어서 사고를 해야한다.
// GIVEN
const tasks = [{ id: 1, text: '책 읽기' }];

// When
const { container } = renderList();

// Then
expect(container).toHaveTextContent('책 읽기');
 

7)BDD란?

사용자의 행동에 따라 테스트를 하는 기법이다. TDD > BDD라고 생각하면 되고, TDD를 좀 더 직관적이고 쉽게 설명하기 위한 일종의 작성법이라고 보면 된다.
 
BDD 는 describe-context-it 3가지 단계로 이루어진다.

7-1)describe

describe는 말그대로 설명하는 것이다. 이 테스트는 특정 작업을 위한 것이라고 설명한다.
describe의 특징은 중첩이 가능하다는 것이다.
describe("pow", function() {

  describe("x를 세 번 곱합니다.", function() {

    function makeTest(x) {
      let expected = x * x * x;
      it(`${x}을/를 세 번 곱하면 ${expected}입니다.`, function() {
        assert.equal(pow(x, 3), expected);
      });
    }

    for (let x = 1; x <= 5; x++) {
      makeTest(x);
    }
이렇게 describe를 중첩으로 사용하게 되면 특정 작업의 하위 작업을 유추하기 쉽다.
 

7-2)Context

Context는 특정 조건이 달라질 때 사용하는 것이다. 처음에 D-C-I 이 세가지가 무조건 세트처럼 움직여야 하는 줄 알았는데 그렇지 않고, 컨텍스트는 행동에 따른 Case가 2개로 나눠질때 사용한다.
describe('Input', () => {
  it('placeholder가 보인다.', () => { });
 
  describe('input 입력', () => {
    context('숫자를 입력하면', () => {
      it('handleChange 함수가 호출된다', () => {}); 
    });
    
    context('텍스트를 입력하면', () => {
      it('handleChange 함수가 호출되지 않는다', () => {}); 
    });
  });
});

7-3)It

it은 테스트를 통해 기대한 결과이다.
보통 하나의 it에 하나의 expect()가 들어가게끔 설계한다.
it('할 일이 없어요가 보인다.', () => {
      const { container } = renderList();
      expect(container).toHaveTextContent('할 일이 없어요!');
    });
  });
 
참고자료
 

회고

  • 테스트코드도 결국은 어떻게 컴포넌트 구조를 만들것인가가 핵심
사실 Jest 같은 테스팅 프레임워크의 문법이나 활용하는 방법은 익숙해지면 쉬울 것 같다.
결국 가장 중요한 건 테스트코드를 통해 무엇을 어떻게 테스트할 것인가가 가장 중요하다.
그리고 이 과정에서 설계를 얼마나 잘하느냐가 핵심이다.
그리고 이런 과정을 체계적으로 고민할 수 있는게 TDD라는 것을 알게 된 것 같다.
무엇보다 약간 묘한 쾌감(?)같은게 있다..
 
  • 해야할 것을 잘하기 위해선 하지 말아야할 것을 안해야한다.
벌써 3주차다. 그래도 코드숨 학습과정은 밀리지 않고 하고 있다고 생각이 든다.
그렇지만, 계속 이력서와 포트폴리오 작성하고,
이런저런 회사에 지원한다고 시간이 너무 많이 잡아먹고 있다. 그러다보니 학습하는데 몰입의 정도가 그렇게 높지는 않다.
 
이럴 때 일수록 이것저것 신경쓰지말고, 해야할 것들을 해야한다.
그리고 해야할 것들을 잘 하기 위해선 하지말아야할 것들을 안해야한다.
 
내가 특히 하지 말아야할 것
  1. 채용공고를 너무 과도하게 찾아본다거나,
  1. 안되면 어쩌지 걱정하는 시간이 많아진다거나,
  1. 쉬는 시간을 여유롭게 잡고 유튜브를 본다거나.
 
이것들만 안해도 내 몰입의 정도는 훨씬 높아질 것이다.
여유를 갖고 해야할 것들에 집중하자.
해내는 사람이 되어야하니까