
강의 목표
강의시간에 학습한 내용을 정리합니다.
강의 내용 정리
강의시간에 학습한 내용을 정리합니다.
테스팅의 필요성
- 테스트는 왜 필요한가?
- 코드 작성시 버그를 줄여줌
- 버그 발생 시, 이런 테스트가 있었더라면 발생한 버그를 미리 잡을 수 있었을 테스트를 새로 작성함으로써 수정
- 기존 코드를 변경(리팩토링)할 때 기존에 동작하던 것들이 고장나지 않는 것을 보장해줌
- 주요 동작들에 대해 주요한 시나리오는 전부 테스트해야
- 안심하고 새로운 버전을 빠르게/자주 출시할 수 있게 도와줌
- main 브랜치 테스트가 모두 통과되면 good to go
- 코드 작성시 버그를 줄여줌
테스트 설계: 무엇을, 어떻게
- 테스트 대상
- 유닛 테스트 : 하나의 기능 단위 ⇒ 하나의 함수, 하나의 클래스
- 통합 테스트(integration test) : 여러 유닛들이 함께 사용될 때 기대한 대로 동작하는지를 테스트
- e2e 테스트(end to end) : 특정 유저 시나리오에 대해 실제 프로덕션과 동일한 환경에서 진행하는 테스트
- 테스트 설계: 유닛 테스트
- 고정돼있는(미리 정해져 있는) input들에 대해 기대되는 output 값이 리턴되는지 확인
- 내부 구현을 모른다고 가정하고 input/output으로 테스트(블랙박스 테스팅)
- 일반적인 경우, 중요한 엣지 케이스들을 전부 커버할 것
- 유틸 함수/클래스에 대해서는 반드시 작성할 것
- 테스트 설계: 통합 테스트
- AAA 패턴 : Arrange, Act, Assert
- 예시) 버튼을 화면에 띄우고, 버튼을 클릭하면, XX 상태가 된다.
- Arrange : 버튼이 렌더링되고, 버튼을 클릭할 수 있게 선택한다.
- Act : 선택한 버튼을 클릭한다.
- Assert : 기대했던 상태로 변경되었는지 검증한다.
- AAA 패턴 : Arrange, Act, Assert
실습 : 간단한 테스트 작성
- tailwind next.js 프로젝트 계속 사용
- root 위치에 /utils 디렉토리 생성
- next.js jest 셋업하고, ts-node도 설치(npm i -D ts-node)
- 예제 테스트 코드와 jest 레퍼런스 참조해서 테스트 코드 완성
정답 코드
import slice from "./slice";
describe("slice", () => {
const array = [1, 2, 3];
it("should use a default `start` of `0` and a default `end` of `length`", () => {
const actual = slice(array);
expect(actual).toEqual(array);
expect(actual).not.toBe(array);
});
it("should work with a positive `start`", () => {
expect(slice(array, 1)).toEqual([2, 3]);
expect(slice(array, 1, 3)).toEqual([2, 3]);
});
it.each([3, 4, 2 ** 32, Infinity])(
"should work with a `start` >= `length`",
(start) => {
expect(slice(array, start)).toEqual([]);
}
);
it("should work with a negative `start`", () => {
expect(slice(array, -1)).toEqual([3]);
});
it.each([-3, -4, -Infinity])(
"should work with a negative `start` <= negative `length`",
(start) => {
expect(slice(array, start)).toEqual(array);
}
);
it.each([2, 3])("should work with `start` >= `end`", (start) => {
expect(slice(array, start, 2)).toEqual([]);
});
it("should work with a positive `end`", () => {
expect(slice(array, 0, 1)).toEqual([1]);
});
it.each([3, 4, 2 ** 32, Infinity])(
"should work with a `end` >= `length`",
(end) => {
expect(slice(array, 0, end)).toEqual(array);
}
);
it("should work with a negative `end`", () => {
expect(slice(array, 0, -1)).toEqual([1, 2]);
});
it.each([-3, -4, -Infinity])(
"should work with a negative `end` <= negative `length`",
(end) => {
expect(slice(array, 0, end)).toEqual([]);
}
);
});
테스팅의 주요 개념들
- 수동 vs. 자동화된 테스트
- 수동 테스트
- 매우 중요한 경로( SaaS 사이트의 회원 가입 / 결제 등 ) 에 대해 수동으로 진행
- 자동화된 테스트
- husky, git hook을 이용한 테스트 자동 실행
- CI/CD 연동 테스트
- 전체 앱의 기능, 동작들에 대해 다양한 실행 환경에서 테스트 진행
- 수동 테스트
- 테스팅 피라미드 - 유닛 vs 통합 vs e2e 테스트
- 유닛 테스트
- 하나의 기능, 역할을 담당하는 유닛을 고립된 상태에서 테스트
- 유닛 테스트가 가장 기본적 “이 코드가 우리가 기대하는 대로 동작"한다는, 우리가 테스트를 작성함으로써 달성하고자 하는 목표는 기본적으로 유닛 테스트를 통해 달성
- 통합 테스트
- 여러 유닛들이 갑이 연결되어 동작할 때 기대대로 동작하는지 테스트
- e2e 테스트
- 중요한 동작들에 대해 실제와 동일한 환경/방식으로 테스트
- 그리고 각각의 유닛이 기대대로 동작한다는 전제하에 유닛 사이의 상호작용을 통합테스트로 검증
- 유닛 테스트
- 테스트 기반 개발(TDD, Test Driven Development)
- 프로덕션 코드가 아니라 테스트 코드를 먼저 작성하고, 그 테스트 코드를 통과시킬 수 있는 프로덕션 코드를 작성해 나가는 방식
- 장/단점이 있는 개발 방식
- 테스트 환경을 준비/설정하는데 생각보다 큰 시간/비용이 발생한다는 점
- 테스트 프레임워크는 실제 환경과 최대한 유사한 환경에서 코드를 실행하려고 하는데, 그러면서도 빠르고, 재현 가능하고, … 게 동작해야 함.
- 이렇게 여러 요구 조건을 만족시키는 프레임워크는 구조적으로 복잡
- 테스트 더블(Test double)
- 실제 데이터, 객체, 모듈을 대신하는 객체
- 그 역할이나 동작 방식에 따라 dummy, fake, mock, stub, spy 등이 있음
- 테스트 프레임워크 , mock 프레임워크 별로 용어가 조금씩 다를 수 있음.
- stub
- 어떤 함수가 호출되면 어떤 정해진 값을 리턴하는 객체
- mocking 프레임워크 필요.
- mock
- 어떤 함수가 어떤 파라미터랑 같이 호출될 것인지를 지정할 수 있고, 그렇지 않을 경우 테스트가 실패하게 할 수 있음.
- mocking 프레임워크 필요.
- fake
- 실제 구현이 있지만 실제보다 단순화된 형태.
- mocking 프레임워크 불필요.
- 코드 커버리지
- 전체 코드 라인 중 테스트 코드에 의해 실행되는 라인의 비율
- 코드 커버리지 베스트 프랙티스
- 이 지표가 테스트의 품질이나 프로덕션 코드의 품질을 담보하지 않음
- 심지어 테스트에 의해 커버된 라인이 제대로 정확히 테스트 된 건지 알지 못함
- 하지만 커버리지가 지나치게 낮은 것은 문제
- 하지만 어느 만큼이 “지나치게 낮은” 것인지에 대한 합의는 없음
JS 테스트 프레임워크
- Jest & Vitest
좋은 테스트와 나쁜 테스트
- 한 번에 한 가지만 테스트하기
- “한 가지”는 하나의 “동작”을 의미 ( 하나의 함수에 여러 동작이 있는 경우도 있음 )
- 하나의 테스트에서 검증하는 범위를 좁혀서 테스트하기
- 테스트 이름 잘 짓기
- 테스트 로직 , 코드를 보지 않고 테스트 함수 이름만 보고도 무엇을 테스트하는지 알 수 있어야 함
- 테스트 이름에 테스트하는 시나리오, 기대하는 결과 값 포함하기
- 구체적이고 명시적인 input 사용하기
- 유닛 테스트는 어떤 input을 넣었을 때 기대하는 output이 나오는지 확인하는 것
- input을 만들 때 최대한 구체적이고 명시적으로 만들기
- production 코드와의 차이가 나는 부분
- 복잡한 로직이 필요하거나 코드가 반복되는 경우 별도의 유틸로 빼내기
- 원칙적으로 동작을 테스트하고 구현 디테일을 테스트하지 않기
- 구현 디테일이 바뀌어도 동작이 바뀌지 않는 한 테스트는 변경되지 않도록 작성
- 단, 예외적으로 구현 디테일을 테스트해야 되는 경우도 있음.
- 주로 mock을 이용해 특정 함수, 코드가 호출되었는지를 확인하는 경우
- ex) 데이터를 읽어오는데 DB가 아니라 캐시에서 읽어오는지를 검증
- Mock 남용하지 않기
- isolation vs realism
- mock을 남용하면 테스트 대상이 정확히 동작하는 것인지 확신하기 어려워짐
- 가독성과 유지보수성이 떨어짐
- 네트워크 통신 등 속도가 너무 느리거나 동작 결과가 불안정하지 않으면 되도록 실제 구현 쓰기
- 실제 구현을 쓰기 어렵다면 fake 구현 쓰기
Jest 테스트 프레임워크
- Jest 소개
- Meta에서 개발한 JS 테스팅 프레임워크
- 여러 matcher들을 제공함
- 필요한 경우 jest-extended 같은 matcher 라이브러리를 추가로 사용하는 것도 가능
- expect(결과값).
- toBe(), toEqual(), toHaveBeenCalled() …
- 테스트 실행 환경 설정 : node vs. jsdom
- 실행 환경 ( runtime environment )
- 프로그램에게 제공되는( 접근 가능한 ) global object, library, API
- 브라우저 = window, document, fetch, Web API(e.g. DOM API …),
- Node = process.env
- 소스 코드 파싱, 기계어로 번역
- 메모리 관리, 쓰레드 관리
- OS와 상호작용 등
- 프로그램에게 제공되는( 접근 가능한 ) global object, library, API
- Jest 설치하기
- React
- CRA: 자동으로 설정돼 있음.
- Next.js
- CNA: “--example with-jest”로 간편하게 설정
- npx create-next-app@latest --example with-jest with-jest-app
- 수동으로 설정하는 경우
- jest 설정을 위한 패키지를 개발종속성으로 설치 ( 다음 중 하나 )
- npm install -D jest jest-environment-jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom ts-node
- yarn add -D jest jest-environment-jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom ts-node
- pnpm install -D jest jest-environment-jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom ts-node
- 기본 jest 구성파일을 생성
- npm init jest@latest
- yarn create jest@latest
- pnpm create jest@latest
- jest 설정을 위한 패키지를 개발종속성으로 설치 ( 다음 중 하나 )
- CNA: “--example with-jest”로 간편하게 설정
- 기타
- babel, webpack 등과 같이 사용하기
- React
- Jest matcher들
- Matcher
- expect() 함수의 리턴 객체에서 호출할 수 있는 다양한 비교 함수들
- toBe(), toBeNull(), toBe{$...}
- toEqual() = Object.is(deep equal)
- toHaveBeenCalledTimes(number), toHaveBeenCalledWith(arg1, arg2, …)
- toHaveProperty(keyPath, value?)
- toBeCloseTo(number, numDigits?)
- toBe() vs. toEqual() vs. toStrictEqual()
- toStrictEqual()
- 값이 undefined 인 속성들
- 값이 undefined 인 배열
- 등도 비교에 포함함
- Matcher
- 수식어
- .not
- .resolves
- .rejects
- 여러 테스트를 구조화하기
- describe()로 관련된 테스트끼리 묶기.
- 모든 테스트 전/후에 실행해야 되는 코드: beforeEach() , afterEach()
- 전체 테스트 전/후에 실행해야 되는 코드: beforeAll() , afterAll()
- before, after 셋업은 describe() 안에서 호출도 가능
- 기타
- .only(), .skip()
- 비동기 코드 테스트; 4가지 방법
- Promises: 테스트 코드에서 promise 리턴하기
- Async/Await:
- callbacks(done() 함수):
- .resolves, .rejects 수식어:
- expect(fetchData()).resolves.toBe('peanut butter')
- 비동기 코드 테스트할 때는 꼭 이 중 한가지 방법을 쓸 것 → 안 그러면 제대로 테스트되지 않고 통과될 수 있음
- Mock을 사용해 테스트하기
- jest.fn()으로 mock 콜백 만들기
- jest.mock(‘모듈 이름’)으로 모듈 mocking 하기
- jest.requireActual()으로 일부만 mocking 하고 나머지는 실제 코드 이용하기
- mockFn.mockImplementation()으로 구현 대체하기
- jest.mock(’모듈 이름’)
- user module을 mock하기
- mock 하려는 파일과 같은 위치에 mocks 디렉토리 만들고 mock 파일 위치
- node module을 mock하기
- jest.mock(‘모듈 이름’). e.g. ject.mock(‘axios’)
- 또는 mocks 디렉토리에 mock 파일 놓기
+ 팀 미팅 후기

그렇게 됐다.
한 번 쯤은 팀장해보는 것도 좋다고 생각 했으니 이번 기회에 열심히 해봐야겠다.
반응형
'교육 > 코드잇 스프린트 : 단기심화 5기' 카테고리의 다른 글
| [ 코드잇 스프린트 ] 교육기간 8일차 TIL (1) | 2024.11.18 |
|---|---|
| [ 코드잇 스프린트 ] 교육기간 7일차 TIL (1) | 2024.11.16 |
| [ 코드잇 스프린트 ] 교육기간 5일차 TIL (1) | 2024.11.14 |
| [ 코드잇 스프린트 ] 교육기간 4일차 TIL (3) | 2024.11.13 |
| [ 코드잇 스프린트 ] 교육기간 3일차 TIL (0) | 2024.11.12 |
