인프런 제로초 Jest 테스트 강의

테스트 문법 등은 이미 jest 문서에 나와 있다. 강의에서는 "테스트를 왜 해야 하는가, 어떻게 테스트를 할 것인가"에 대해 다룬다.

jest는 테스트 프레임워크라서 모든게 다있다. vitest에서도 다 지원

물론 e2e 테스트에는 supertest, superagent같은게 있지만 일단 안해도 됨

테스트를 짜야 하는 경우

TDD를 하는 게 좋지만 엄청 우선적인 건 아니다.

원칙

react, nest 테스트의 경우 따로 라이브러리가 필요하다. 이 강의는 jest를 거의 주로 다룬다. 맛보기 정도는 한다.

vitest를 쓰면 ESM 문법을 그냥 쓸 수 있다. 기존 jest는 윈도우 같은 경우 NODE_OPTIONS=--experimental-vm-modules 쓰고 cross-env 쓰고 뭐 난리를 쳐야 한다. 그래서 강의는 jest지만 vitest로 실습함. 또 나는 vitest 쓸거니까..

test는 내부 expect가 전부 성공해야 성공한다.

객체비교는 toStrictEqual을 쓰자. not을 앞에 붙이면 반대 결과 테스트. 근데 toStrictEqual은 생성자가 다르고 내용만 같은 경우 통과를 못한다. 이 경우 toMatchObject를 쓰자.

함수 호출의 테스트를 위해 vi.fn혹은 vi.spyOn을 쓸 수 있다. 또한 mockImplementation, mockReturnValue와 해당 메서드의 Once 버전 등을 통해 값도 모킹할 수 있다.

한번 spy를 쓰면 다음부터는 원래 함수가 호출되지 않는다. mockClear를 쓰면 초기화된다. spy function을 만들고 테스트 마지막에 mockClear글 한다. spyFn.mockClear()

모든 mock 함수에 대해 한번에 작업해주는 clearAllMocks, resetAllMocks, restoreAllMocks도 있다.

테스트 라이프사이클

beforeAll, beforeEach, afterEach, afterAll로 각 테스트 전후에 실행할 코드를 작성할 수 있다. 가령 beforeEach라면 매 테스트 전에 DB를 초기화하는 등에 사용. beforeAll, afterAll은 각 파일의 all 테스트 전후에 한 번만 실행된다.

beforeAll -> beforeEach -> test -> afterEach -> ...(모든 다른 테스트) -> afterAll

beforeEach라면 mockRestore 같은 것을 쓸 수 있다.

그리고 테스트 코드도 JS기 때문에 스코프, 클로저, 이벤트 루프 등의 규칙을 전부 따른다. 따라서 예를 들어 spyFn 같은 경우 특정 테스트 스코프에서 정의했다면 beforeEach에서 가져다 쓸 수 없다. 이 경우 let등을 통해 전역 변수로 선언해줘야.

describe를 통해 테스트 그룹을 만들어서 그룹화할 수 있다. 이 경우 그룹 안에 있는 beforeEach, afterEach는 그 그룹 안의 테스트 전에 실행된다. 물론 파일을 만들어서 그룹을 지을 수도 있지만... 즉 before/after 이거를 같은 파일의 특정 테스트들에만 적용할 때 describe를 쓰면 된다.

만약 파일 전체의 beforeEach가 있고 테스트 그룹의 beforeEach가 있다면 파일 전체의 beforeEach가 먼저 실행된다. 스코프 범위를 따라간다고 생각하면 된다. beforeAll, afterAll도 마찬가지로 describe의 전후에 실행된다.

테스트 잠깐 미루기

보통 precommit 등으로 테스트를 실행하곤 하는데, 너무 급해서 테스트를 미루고 싶을 때가 있다. 이럴 때 test.skip을 쓰면 된다. 이 경우 테스트는 실행되지 않는다. xtest를 쓸 수도 있다. 이것도 같은 기능이다.

혹은 이후에 만들어야 할 테스트가 있다면 test.todo를 쓰면 된다.

testit로 대체 가능하다. it("should be ...", () => {})처럼 쓰면 영어로는 자연스러우니까 영어권 사람들은 it를 쓰고는 한다.

날짜 테스트

JS 실행 시간이 있기 때문에 날짜를 테스트할 때 1ms 정도 차이가 나서 테스트가 실패할 수 있다. 이럴 때 vi.useFakeTimers()를 쓴다.

vi.useFakeTimers().setSystemTime(new Date(2024, 9, 22));

테스트가 끝나면 vi.useRealTimers()를 써서 원래 시간으로 되돌림. 날짜를 쓰는 테스트의 경우 이걸 beforeEach, afterEach에 넣어두면 편하다.

그리고 테스트가 긴 시간을 요하는 경우 vi.runAllTicks()를 이용해 promise 즉 마이크로태스크들을 전부 실행 가능하다. vi.runAllTimers()는 setTimeout, setInterval을 전부 실행한다.

vi.advanceTimersByTime(1000)처럼 시간을 특정 시간만큼 빨리감기도 가능하다. 저걸 쓰면 1000ms가 지난 것처럼 작동한다. 이걸 사용하려면 vi.useFakeTimers()를 써놔야 한다.

이외에도 jest object에 getRealSystemTime같은 다양한 시간 관련 기능들이 있다. vitest의 mocking 글

거의 모든 테스트 기능이 있지만 메서드를 하나하나 다 외울 필요는 없다.

toBeCloseTo는 적당히 가까운 값 비교할 수 있다ㅋㅋ

테스트가 5초 안에 끝나지 않으면 실패한다. 이 시간은 test의 2번째 인수로 설정할 수 있다. test("...", () => {}, 10000)처럼. 그런데 그보다는 advanceTimersByTime등으로 시간을 조작해서 시간을 빨리 감는 게 낫다.

expect가 실행이 된지를 체크하는 expect.assertions(1)도 있다. 이걸 쓰면 expect가 실행이 안되면 에러를 띄운다.

비동기 테스트는 이런 어썰션이나 시간 조작을 통해 여러 조치를 취해놓는게 좋다.

함수 호출 순서, 모듈 모킹등

함수 호출 순서를 테스트할 때는 스파이 함수를 만들고 나서 spyFn.mock.invocationCallOrder를 통해 호출 순서를 알 수 있다. 이걸 통해 호출 순서를 테스트할 수 있다.

이외에도 spyFn.mock.calls처럼 스파이 함수의 mock 속성에는 함수 호출시마다 함께 호출된 인수들의 순서대로의 배열 등 다양한 정보가 있다. 스파이 함수가 자기가 호출된 정보를 저장하는 게 바로 mock 객체기 때문이다.

또는 jest-extended의 toHaveBeenCalledBefore, toHaveBeenCalledBefore를 쓸 수도 있다.

의미있는 테스트를 위해서는 인수 중 가장 중요한 걸 테스트

함수 테스트 시 인수가 복잡하거나 복잡한 형태 객체일 때는 fn.mock.calls[n].argN을 통해 인수 중 가장 중요하다 여겨지는 1개 혹은 몇개만 테스트한다.

테스트의 핵심은 모킹을 얼마나 잘하는가. 사이드 이펙트 등을 얼마나 적절히 테스트하는가.. 예를 들어 결제 기능이라면 테스트할때마다 실제 결제가 일어나면 안된다. 그럴때 결제 함수를 호출하는 척 하는 등 모킹을 잘 해야한다.

모듈의 모킹

모듈 파일을 통째로 갈아끼고 싶다면 __mocks__ 폴더 활용. 개별적으로 모킹을 해야 한다면 vi.mock의 2번째 인수를 쓰는 게 좋다. __mocks__ 폴더는 전체 모듈에서 어떤 모킹을 해야 할때 쓴다. 이때 클래스들도 알아서 모킹됨

이런 vi.mock을 이용하면 node_modules의 모듈도 모킹할 수 있다. vi.mock("라이브러리이름", () => {})처럼. 물론 노드모듈즈랑 같은 위치에 __mocks__/라이브러리이름 폴더를 만들어서 모킹할 수도 있다.

속성은 jest에서는 replaceProperty를 써서 대체하는데 vitest에서는 spyOn이나 stubEnv를 쓴다고 한다.

모킹을 하다가 원본을 가져오고 싶다면 vi.requireActual을 쓰면 된다. vi.requireActual("파일경로")를 통해 모킹하고 있는 파일의 원본을 가져올 수 있다. 테스트 중 원본 객체 메서드가 필요할 때 쓴다. 또는 일부 속성, 메서드만 모킹할 때 이런 식으로 쓸 수 있다.

vi.mock("./mockFunc", ()=>{
  return {
    vi.requireActual("./mockFunc"),
    mockMethod: vi.fn(), // 모킹할 메서드 입력
  }
})

테스트 간 간섭 끊기

테스트 간에 모듈 import 등의 캐시를 날리기 위해서 beforeEach에 vi.resetModules()를 쓴다. 이걸 쓰면 모듈 import 캐시가 날아가서 다음 테스트에 영향을 주지 않는다. 원래 JS에서는 모듈 임포트를 하면 그게 캐시에 남아 있다.

test.only를 쓰면 테스트 중에 이 테스트만 실행한다. 따라서 이를 이용하면 특정 테스트만 실행함으로써 이 테스트가 다른 테스트에 의존하는지 알 수 있다. 원래라면 테스트는 독립적이어야 하므로 only가 있든 없든 성공해야 하는데 만약 only가 있을 때 실패한다면 다른 테스트에 의존하고 있는 것이다.

each로 중복 줄이기

테스트 간에 반복되는 구조가 있을 때 test.each를 쓸 수 있다. 고차함수를 받아서 테스트를 여러 번 실행한다.

// 혹은 {a:1, b:2, expected:3} 이런 식으로 객체의 배열을 넣어도 된다
test.each([
  [1, 1, 2],
  [1, 2, 3],
  [2, 1, 3],
])("add %i + %i = %i", (a, b, expected) => {
  expect(a + b).toBe(expected);
});

유사한 값도 통과시키기

예를 들어 expect.anything()을 쓰면 null, undefined 외에 모든 값을 통과시킨다. expect.any는 생성자가 같은지만 비교한다. 예를 들어 expect.any(String)은 문자열만 통과시킨다. 랜덤 함수 등 테스트시에 유용하다.

아까도 다룬, 부동소수점 비교 등 가까운 값 통과를 위한 expect.closeTo(number, delta)도 있다.

이외에도 expect에는 다양한 메서드가 있다. 공식 문서를 참고하자.

유용한 실행옵션

vitest 커맨드라인 인터페이스 문서. 테스트가 많아지면 성능도 매우 중요해지는데 이런 CLI 옵션들을 잘 활용하면 성능을 높일 수 있다.

테스트의 afterAll등에서 vi.clearAllTimers(), db.close()등 연결을 끊어 주는 코드를 쓰는 게 좋다.

챕터 2 시작: 강의도 좋지만 바로 나의 소스코드에도 테스트를 추가해 보고, 그 과정에서 코드를 수정하면서 더 테스트하기 좋은 코드를 만들어보자. 따라치기보다는 내 소스코드에 적용을 해보자.

테스트의 핵심 : 모킹, 어떤 부분을 테스트에서 expect로 테스트할 것인가.

Ch2. 실전 코드 테스트

테스트를 통한 리팩토링

서버 컨트롤러 등을 함수로 안 뺐다고 하자. 그런데 테스트를 하게 되면 이런 걸 따로 함수로 분리하게 된다. 테스트 작성을 위해 더 테스트하기 좋은 클린 코드로 리팩토링을 하게 되는 것. 가독성도 올라감

테스트 작명

구체적인 구현이 아니라 기능을 설명한다. 예를 들어 "로그인 했을 시 next를 호출한다" 처럼 말이다. "isAuthed가 true면 next를 호출한다" 같이 쓰면, 나중에 함수 이름이 변경되었을 때 테스트 문구도 변경해야 한다.

따라서 테스트 제목은 개발자가 아는 구체적인 구현이 아니라 해야 하는 기능에 대해, 비개발자같은 문장으로 작성한다. 기획자뿐 아니라 나 자신도 구체적인 구현을 까먹기 때문에 이렇게 작성하는 게 좋다.

레드 그린 리팩터

먼저 실패하는 테스트를 한번 작성해 보고 그 다음에 성공하자. 그래서 "틀렸을 때 제대로 실패하는지"를 보고 성공하는 테스트를 다시 작성한다.

Promise 관련 테스트 등에서 무조건 성공하는 테스트를 작성해 버렸을 수 있기 때문이다. 따라서 실패하는 걸 먼저 해봐서 실패를 확인한 뒤 성공하는 테스트를 작성한다.

기획자의 말을 먼저 테스트로 작성 -> 아직 구현이 안되었으니 테스트 실패 -> 구현 -> 테스트 성공 -> 리팩터링(이건 TDD에서)

커버리지

jest 설정파일에서 collectCoverage를 true 설정하면 커버리지 확인 가능. 이때 이상적으로는 테스트 커버리지를 100%를 향해 가는 게 좋다. 보고서에서 필요한 부분을 알려준다. 가령 브랜치의 몇%가 테스트되지 않았다던가.

이때도 일단 "특정 부분에 대한 테스트"가 존재하기만 하면, 심지어 내부에 expect가 없는 테스트의 경우에도 테스트 커버리지를 올린다. 해당 if문을 true로 만들어서 실행하게끔만 만들어도 커버리지가 올라간다! 만약 중간에 false인 if문이 있으면 에러가 날 수 있는 상황이라면 커버리지가 100%라도 에러가 날 수 있다. 모든 문장이 실행됨 != 모든 로직이 테스트됨

따라서 테스트 커버리지가 100%라고 해서 모든 부분이 테스트되었다고 할 수는 없다. 고로 레드 그린 리팩터(실패한 다음에 성공하기)를 통해 테스트를 작성하는 게 좋다.

커버리지 올리기

내 코드는 테스트하고 남의 코드는 모킹. 예를 들어 express에서 쓰는 req같은 건 모킹. 컨트롤러 내의 if문 같은 건 내가 쓰는 코드니까 테스트.

즉 내가 짠 코드만 테스트 경계 안에 놓고, 라이브러리 코드는 남의 코드이므로 모킹을 한다. 라이브러리 코드는 라이브러리에 맡기자. 예를 들어 express의 req 객체는 모킹한다. 테스트 경계 바깥에 있는 것들은 신경쓰지 않는다.

테스트 경계를 잘 잡는 게 중요하다. "로그인을 했을 때 어떤 동작을 한다"는 테스트가 있다면 로그인을 체크하는 건 라이브러리에 맡기고 테스트하지 않고, 로그인 여부를 모킹한 후 테스트할 수 있다.

커버리지 100%여도 에러 발생 가능

에러가 발생하는 함수의 경우에는 익명함수로 한번 감싸서 테스트해야 한다. 그렇지 않으면 함수를 실행하면서 JS단에서 에러가 나버림.

비동기 코드 등에서 await을 빼먹거나 그럴 경우 분명 해당 부분에 대한 테스트는 있으니 커버리지는 올라간다. 하지만 실제로는 비동기 부분에서 에러가 날 수 있다! 비동기 조심.

부작용이 있는 부분을 모킹

외부 사이드 이펙트를 일으키는 코드를 모킹하자. 예를 들어 DB 요청이 있다든지, DB에 데이터를 저장한다든지. 라이브러리를 가져다 쓰는 경우에는 모킹을 하는 게 좋지만 그 중에서도 특히 외부 사이드 이펙트를 일으키는 부분을 모킹하는 게 좋다.

만약 테스트 DB를 사용할 경우, 암튼 사이드 이펙트 결과도 테스트를 위해 준비되어 있을 경우 모킹 안 해도 됨.

어떤 테스트, 상황인지에 따라 모킹을 하는지가 달라진다.

-> 유닛 테스트의 경우 웬만하면 모킹을 하는 게 좋다. 통합 테스트의 경우에는 테스트 DB등을 사용할 수 있지만 유닛 테스트는 모킹을 하는 게 좋은 듯?

코드 순서대로 테스트를 만들자

테스트 데이터를 모킹하거나 테스트를 만들다 보면 자연스럽게 예외를 찾거나 논리적인 허점(ex: 회원가입 시 이메일이 없으면 어떡하지?)을 찾게 된다. 코드도 고치게 되고..

그리고 그 과정에서 테스트 추가도 가능. 이메일이 없으면 어떻게 한다든지

테스트 구성은 코드 순서에 따라서.

예를 들어 회원가입 입력 검증의 if문이 이메일 확인 -> 닉네임 확인 -> 패스워드 확인이면 테스트도 그 순서대로 작성한다. 그렇게 해야 위에 테스트를 가져다 쓰기도 편하고 가독성도 좋다.

테스트는 중복을 허용한다

일반적인 코드에서는 중복을 피하고 함수로 빼거나 외부 스코프에 변수를 선언하는 걸 추천한다. 하지만 테스트에서는 좀 중복되는 게 나을 수 있다. 테스트 각각만 보고도 데이터에 대한 감을 잡을 수 있게끔 한다. 나중에 읽기 편한 게 중요하다.

TS any도 좀 써도 된다. 테스트가 중요한 거지 타입이 중요한 건 아니니까.

테스트는 실패 한번 하고 성공하기. 레드그린 리팩터

미래를 대비하는 테스트

"절대 호출되면 안되는, 사이드 이펙트를 일으키는 함수"가 호출되지 않는 것도 테스트하자. 실행이 되는 것뿐 아니라 실행이 안되는 것도 중요하다. 예를 들어 이메일이 없으면 회원가입이 안 되는 걸 테스트한다면 에러 발생 뿐 아니라 회원가입 함수가 '호출되지 않는 것' 도 테스트한다.

테스트 2번

사이드 이펙트가 있는 함수들은 모킹을 한다. 그리고 비동기는 늘 조심!

모킹을 안 하면 DB 요청이 가버리거나 아니면 다른 테스트에서 모킹한 메서드가 그대로 쓰일 수도 있다. 따라서 모킹을 까먹지 말자.

테스트는 돌릴 때마다 결과가 바뀌면 안 된다. 예를 들어 랜덤 암호화가 있다든가. 그런 경우에는 모킹을 해야 한다. 그리고 이런 경우를 막기 위해서 테스트를 2번씩 실행해 보는 게 좋다. 2번째 실행할 때는 랜덤함수, 또는 실제 DB에 데이터가 저장되어 버린 등의 이유로 테스트 실패가 뜰 수 있다. 모킹 실수를 알아채기 위해서.

물론 bcrypt같은 암호화 라이브러리를 쓰는 경우 해당 라이브러리 함수를 가져와서 모킹에 쓸 수도 있다.

매개변수로 만들면 모킹이 쉽다

모킹하기 싫은 대상을 매개변수로 넘기고 기본값을 주면 모킹이 편해질 수도 있다.

고차함수 테스트

고차함수를 모킹하면 된다. 콜백을 넘길 수도 있음

const req = {
  login: jest.fn((user, cb) => {
    cb();
  }),
};

아무튼 if문 분기 등을 다 테스트하는 방향으로 가는 게 중요

if문 없을 때 커버리지 올리기

그냥 함수들 한번씩 호출해 주고 toBeDefined같은 확인 해주면 됨

if문 없는 함수들은 그냥 1번 쭉 실행되게끔만 해보면 커버리지가 쭉 올라간다. 이런 것들은 물론 크게 의미있는 테스트는 아니다. 하지만 "의미있는 테스트인가?"보다는 "커버리지가 올라가는가?" 원칙을 좀 더 우선적으로 따지자. 커버리지를 올리고 또 그러면서 코드를 한번 더 보는 것.

참고로 커버리지에는 if문 외에 삼항 연산자도 분기로 취급된다. 삼항 연산자도 커버리지에 영향을 주므로 코드 보면서 신경쓰자. 또한 || 등도 분기로 처리됨에 주의한다. 가령 다음과 같은 문장.

const env = process.env.NODE_ENV || "development";

여기서 process.env.NODE_ENV가 있으므로 뒷 문장이 실행이 안되어서 테스트 커버가 안된거(반절만 실행 안되어 있으니 노란색으로 뜬다)로 뜬다.

익명 함수들이 주로 테스트 커버리지 올리기를 방해한다. 따라서 익명 함수들을 따로 함수로 빼서 테스트를 하면 커버리지를 올릴 수 있다.

비슷한 DB 테스트 등의 코드는 복붙으로 테스트 생성도 가능

__mocks__로 파일 통째로 모킹

__mocks__ 폴더를 만들어서 파일을 통째로 모킹할 수 있다. 이때는 파일명이 같아야 한다. 그리고 jest.mock를 써서 특정 경로 파일을 모킹하겠다고 명시 가능하다.

TODO: vitest 모킹 문서 읽기

if가 false인 것까지 테스트

if (조건) {}형식의 코드의 경우 if가 true가 되는 부분만 테스트해도 기본적으로 모든 코드가 실행된다. 따라서 if가 true일 경우만 테스트해도 커버리지가 올라간다. 그러나

커버리지 100%를 향한 여정

커버리지를 올리기 위해서는 정말 모듈 캐싱을 초기화하고 등등 jest 내부 기능을 잘 써줘야 한다.

특정 파일을 테스트하면 거기서 import한 파일까지 전부 테스트 커버리지에 뜬다.

부작용이 있는 함수는 모킹을 하자. 중요한 건 사이드 이펙트가 실행되는 게 아니라 "의도한 대로 코드 로직이 돌아가는지" 이므로 사이드 이펙트를 모킹해도 된다. 이때 jest.mock, jest.fn, jest.spyOn, 혹은 직접 만든 에러, res 객체 등을 사용

테스트에서 어려운 건 익명 함수를 함수로 빼는 것뿐 아니라, 모킹을 하기 위해서 여러 트릭들을 사용하는 것도 있다. 가령 express의 listen을 모킹하기 위해 이런 걸 사용

jest.mock("express", () => {
  // 실제 express를 한번 호출
  const app = jest.requireActual("express")();
  jest.spyOn(app, "listen").mockImplementation();
  const express = () => app;
  // express에 있는 속성들을 전부 얕은 복사
  Object.assign(express, jest.requireActual("express"));
  return express;
});

이때 모킹 등을 한번 하면 다음 테스트에 영향을 미칠 수 있으니 주의.

또한 익명 함수들은 테스트 힘드니까 따로 이름 있는 함수로 빼서 테스트하자. 익명 함수는 커버리지를 낮추는 요인이라는 걸 다시 한번 상기하자.

테스트 팁

async 함수를 보면 웬만하면 그 테스트를 describe로 감싸주자. 보통 비동기 함수는 catch가 있기 때문에 어차피 그 함수에 대해 2개 이상의 테스트를 작성하게 될 거라서.

커버리지와 상관없이 '특정 함수가 호출되는지' 뿐 아니라 '특정 함수가 호출되지 않는 것'까지 테스트하자.

하나의 함수가 너무 로직이 복잡하면 테스트도 복잡해진다. 그럴 경우 함수를 나눠서 테스트하기 쉽게 만드는 방법도 있다. 콜백이나 then 같은게 너무 깊이 들어가 있으면 기본적으로 "따로 함수로 빼기"를 생각하자.

유닛 테스트에서는 진짜 테스트 DB를 쓰기보다는 모킹을

테스트에서는 중복이 좀 있어도 된다. 데이터를 변수화하는 게 오히려 안 좋을 수 있음. 중복이 좀 있어도 테스트를 읽기 쉽고 데이터를 이해하기 쉽게 만들자.

테스트가 프로미스 리턴 시 프로미스가 완료될 때까지 기다려 준다

it("deserializeUser 유저 조회 시 에러가 나면 done 콜백으로 에러를 호출해야한다", () => {
  const done = jest.fn();
  const { afterDeserialize } = require("./index");
  const error = new Error();
  jest.spyOn(User, "findOne").mockRejectedValue(error);
  const promise = afterDeserialize(1, done);
  return promise.then(() => {
    expect(done).toHaveBeenCalledWith(error);
  });
});

현실적으로 100% 커버리지에는 약간의 꼼수가 필요하다. 90~95% 달성하면 꽤 괜찮을지도?

테스트 각각은 독립적으로 실행되어야 한다. 그래서 테스트 간에 영향을 주는 부분은 모킹이나 beforeEach 등에서 초기화를 하는 걸 통해 해결해야 한다. 외부 API(유료)등을 쓰는 경우에도 모킹을 해야만 한다. classist들의 경우 실제 데이터를 넣어서 테스트를 해야 하지만 이런 외부 API는 모킹을 해야 한다.

mockist는 모킹을 많이하기 때문에 탑다운 개발이 가능

테스트 종류

유닛 테스트 - 통합 테스트 - e2e 테스트. 그런데 유닛테스트와 e2e는 겹치는 게 없지만, 통합 테스트는 나머지 둘과 모호할 때가 있다. 프론트는 유닛-통합이, 백엔드는 통합-e2e가 모호한 편

e2e테스트, 통합 등은 여러 유닛테스트를 합친 거라 테스트 파일을 테스트할 코드 파일과 함께 두기가 애매하다. 그래서 따로 프로젝트 루트에 폴더를 만들거나 한다.

통합 테스트

통합 테스트에서는 보통 라우터가 잘 동작하는지 테스트한다. 유닛이 뭉쳐서 동작하는 거니까... 모듈을 테스트하는 게 아니라 서버를 테스트하는 거다. 통합테스트등을 할때는 테스트DB(실제DB랑 다른)를 써야 한다. json-server라든지.

통합 테스트는 supertest로. supertest의 request를 사용

다만 supertest는 비동기니까 done을 써야 한다. done을 쓰면 비동기 테스트가 끝날 때까지 기다려준다. 혹은 promise니까 test에서 해당 promise를 return해도 된다.

통합 테스트 실행시 서버 실행/종료를 beforeEach/afterEach 혹은 beforeAll/afterAll에 넣어두면 좋다. 서버 중복 실행은 에러를 띄우거나 테스트가 종료가 안되게 하므로 꼭 주의. 그리고 로그인 전에 가입을 해야 한다든가 하는 경우가 있다. 또는 로그아웃 테스트 전엔 로그인이 필요하다든지...즉 테스트 전에 필요한 작업이 있는지 늘 생각하자.

supertest agent를 사용하면 테스트 간에 세션을 공유할 수 있다. 그런데 테스트를 2번 실행시 가입이 2번 되거나 하는 등의 문제가 있을 수 있다. 따라서 테스트 시 beforeAll 등에서 DB를 초기화해주는 코드를 넣어주는 등의 조치를 하자.

Supertest 사용법 (통합 API 테스트)

https://inpa.tistory.com/entry/JEST-📚-supertest-api-요청테스트

비동기 테스트 등에서 expect가 호출되었는지 궁금하면 expect.assertions(n);을 쓰면 된다. 이걸 쓰면 expect가 n번 실행이 안되면 에러를 띄운다.

supertest가 모킹이 확실히 쉽다. 모킹할게 없다. 그냥 서버를 띄우고 테스트하면 된다.

react test

원래 react에서는 act함수로 테스트를 할 수 있도록 도와준다.

react 공식 테스트 도구

이걸 쓰면 BDD(행위 기반 테스트)를 더 잘 할 수 있다. 기획자가 사용자의 입장에서 행위를 기획하는 걸 테스트로 더 잘 표현할 수 있음. 사용자가 어떤 행동을 하고 어떤 결과가 나오는가? 를 테스트. 예전에는 enzyme을 써서 실제 state, props등 구체적인 구현을 검사했지만 지금은 트렌드가 넘어와서 행위 기반 테스트를 많이 한다.

테스팅 라이브러리의 원칙

대충 사용자가 어떻게 하는지를 생각해서 테스트하라는.

https://testing-library.com/docs/guiding-principles

테스팅 라이브러리 쓸 때는 render, screen이 자주 쓰이는 함수. getByRole 등을 사용한다. 이렇게 heading, link, button 등 접근성 role을 기반으로 DOM노드를 가져와서 테스트 진행(각 컴포넌트의 role https://www.w3.org/TR/html-aria/#docconformance) 이런 role을 알아둬야 웹 접근성을 챙길 수 있다.

byRole에서 접근성 어쩌구를 챙기라고 하는 문서

https://testing-library.com/docs/queries/byrole

이외에 text, labeltext 등 다양한 쿼리가 있다. 이때 사용자에게 중요하지 않은 속성으로 DOM 노드를 가져오는 건 권장되지 않는다. 사용자는 버튼을 클릭하고 결과를 보는 게 중요하지 그 id나 class 같은 건 하나도 안 중요하니까, BDD에서 권장을 안 한다.

자세한 사항은 react testing library 문서를 참고하자.

치트시트 https://testing-library.com/docs/react-testing-library/cheatsheet

네트워크 요청들을 모킹하기 위해선 msw라는 라이브러리를 쓴다.

유닛 테스트를 하고 싶다? 근데 요즘은 리액트에서 비즈니스 로직은 거의 다 훅에 들어 있음 -> 테스팅 라이브러리의 renderHook 사용. 이걸로 훅을 유닛 테스트 할 수 있다. 물론 엄밀히 말하면 훅도 내부 함수가 있기 때문에 유닛 테스트라고 완벽히 말하기엔 애매하지만 대충 그렇다.

msw, renderHook을 쓰면 테스트가 더 쉬워짐

e2e 테스트는 cypress로 보통 많이 한다. cypress는 jest 없이 할수도..

스냅샷 테스트

테스트가 2번 실행해 봐야 한다는 원칙이 있었다. 처음 했을 때랑 2번째 했을 때랑, 사이드 이펙트 등으로 결과가 달라질 수 있기 때문!

스냅샷 테스트 = 박제 랑 비슷

toMatchSnapshot을 써서 테스트하면 스냅샷이 생기고, 이후에 테스트를 다시 돌렸을 때 스냅샷과 비교해서 다르면 에러를 띄운다. 스냅샷은 테스트 결과를 저장해 놓은 것. 스냅샷이 없으면 새로 만들어서 저장한다.

만약 의도해서 스냅샷을 바꾼 거라면 스냅샷 파일을 업데이트하거나 -u 옵션을 쓰면 된다.

박제 파일 만들기 싫으면 toMatchSnapshot 대신 toMatchInlineSnapshot을 쓰면 된다. 이걸 쓰면 스냅샷 파일이 따로 생기지 않고 코드 안에 스냅샷을 넣는다.

난 그냥 스냅샷 파일 만들래..

AI로 테스트 코드 작성하기

소스코드 넣고, jest/vitest로 유닛 테스트 짜달라고 하기 "위 코드에 대해 jest로 유닛 테스트 짜줘" 물론 100% 믿으면 안되는데 구조를 그럭저럭 잘 짜준다. 또한 타자가 매우 줄어든다.

내가 mockist인지 classicst인지 생각을 해보자. 나는 classist인듯? 값 넣는게 좋아~