테스트코드는 왜 쓰는 걸까?
가장 큰 이유는 코드가 제대로 동작하는 지를 확인하기 위해 작성한다고 볼 수 있다.
끊임없이 확장해 나가는 특성상, 새로 작성한 코드때문에 기존의 코드가 망가지면 안되지 않는가? 그래서 문제가 없는지 확인을 해봐야한다.
그런데 일일이 서버를 키고 Swagger나 Playground를 통해서 하나씩 눌러보면서 테스트를 하는 것은 정말 소모적인 행동이기에 이를 대신할 테스트 코드가 필요하다.
Jest는 단순성에 중점을 둔 유쾌한 JavaScript 테스트 프레임워크 라고 공식 사이트에 안내가 된다.
Babel , TypeScript , Node , React , Angular , Vue 등 을 사용하는 프로젝트에서 작동하며 테스트를 병렬로 안정적으로 실행할 수 있다고 한다.
작업을 빠르게 하기 위해 Jest는 이전에 실패한 테스트를 먼저 실행하고 테스트 파일이 걸리는 시간에 따라 실행을 재구성한다고 한다.
자체적으로 지원하는 Mocking API도 제공하고 있어서 손쉽게 사용할 수 있으며 통과하지 못한 테스트 코드의 에러 원인도 텍스트로 안내해주고 있다.
테스트 방식에도 여러가지가 있다.
이 중 오늘 소개할 Jest 라이브러리는 unit test
방식에 조금 더 적합한 라이브러리라고 한다. unit test
는 무엇일까?
함수처럼 가장 작은 단위를 테스트 하는 방식.
최종적으로 배포 환경과 동일하게 모든 것이 잘 작동하는지 확인하는 테스트. 이 Test 방식은 모든 환경이 production 상황과 동일하게 진행되기에 실제로 Database에 데이터가 담기기도 한다.
통합 테스트 라고 불린다. 유닛 단위들을 모아서 함께 테스트를 하는 방식으로, 서버의 구성 요소들이 다 모여서 서로서로 함께 잘 작동하는 지를 테스트 하기 위한 방식이다.
Mocha 프레임 워크
는 통합 테스트에 조금더 적합하다고 한다.
nodeJS의 테스팅 라이브러리는 주로 Jest와 Mocha로 나뉘는데 이 둘에 대한 차이는 여기서 볼 수 있다.
나는 NestJS에서 기본적으로 지원하는 Jest
를 이용할 예정이였기에 unit test
를 진행해 보았다.
우선 Jest 라이브러리로 TestCode를 작성하기 전에 TestCode를 어떻게 인식하는지 보자면 파일이름으로 인식된다.
파일 이름이 *.spec.ts
이런식으로 .spec
이 들어가면 해당 파일을 test파일로 읽어들여 테스트를 진행해준다.
우선 기본적인 구조와 알아둬야할 문법은 다음과 같다.
describe ('부모', () => {
beforeEach(() => {
})
it('자식', () => {
})
})
구조는 보통 위와 같은데 보통은 '부모'
이 자리에 Test할 이름을 쓴다. Class명과 같은 unit을 묶을만한 이름 말이다. 그리고 화살표 함수를 실행하고 그 안에 테스트할 함수 혹은 작은 단위들 쓴다.
그러면 각각의 문장은 뭘 뜻하는가?
describe ('', () => {})
: 여러개의 테스트 모아놓은 그룹 단위. 최상위 폴더? 다 감싸고 있는 느낌이라고 생각해도 좋다.it ('', () => {})
: 최상위 폴더 안에 있는 작은 파일들. 테스트의 단위를 말한다.beforeEach (() => {})
: Testing 이전에 실행되는 부분. 보통 테스트를 하기전에 초기화 와 같은 작업들이 필요할 경우 실행하게 된다. 이 친구는 ''로 이름 같은것을 쓰지 않는다.가벼운 예제로는 이렇다.
# 계산.spec.ts
describe('Calculation', () => {
it('plus 테스트', () => {
const a = 1;
const b = 2;
expect(a + b).toBe(3);
});
it('multiply 테스트', () => {
const a = 1;
const b = 2;
expect(a * b).toBe(2);
});
});
Calculation
는 폴더 이름이라고 생각하면 된다. 이 describe
안에 Calculation
과 관련이 있는 함수를 테스트 할 것이다.
describe('Calculation', () => {
...
})
이제 이 폴더(그룹)은 Calculation
와 관련있는 함수를 따로따로 테스트 할 것이다.
it('plus 테스트', () => {
const a = 1;
const b = 2;
계
expect(a + b).toBe(3);
});
Calculation 안에는 plus라는 것이 있다고 생각해보자. 이걸 테스트 하기 위해선 다음과 같다.
우선 최상위 폴더 안에 있는 작은 파일들은 어디에 쓴다고 했는가? it
이다. 그렇기에 it
의 이름으로 plus 함수의 이름을 사용했다.
const a = 1;
const b = 2;
그리고 그 안에는 위와 같이 해당 함수에서 사용되는 매개변수인 a, b의 값을 각각 할당한다. 그럼 그 다음줄은 뭘까?
expect(a + b).toBe(3);
expect의 사전적 의미는 예상하다 다. toBe는 될것이다 라는 의미다.
그러면 생각해보라. 갑분 영어시간
(a+b)는 예상합니다. (3)이 될거라고. 이게 무슨뜻인가!?
테스트 코드는 동작을 확인하기 위해서 해당 함수를 실행해본다. 즉 우리가 인자값도 정의를 했으니 a+b가 3일 경우 정상적으로 테스트가 통과되고 3이 되지 않을 경우 테스트가 실패로 끝난다.
테스트 코드의 형태는 대부분 expect().Matchers()
형태로 이루어져있는데, 이때 Matchers()
가 어떤 것들이 있는지 자주 쓰이는 것들을 알아보자.
toBe() : 기본 값을 비교하거나 개체 인스턴스의 참조 ID를 확인하는 데 사용.
toEqual() : 개체 인스턴스의 모든 속성을 재귀적으로 비교하는 데 사용.
📌 toBe() 와 toEqual()의 차이는?
객체(Obj)를 비교할 때 큰 차이를 보인다.
toBe()는===
비교연산자와 같은 느낌이라면, toEqual()는==
비교연산자와 같은 느낌이다.# 객체 const obj1 = { name: "ABC" }; const obj2 = { name: "ABC" };
# 통과 여부 : toBe() expect(obj1).toBe(obj2); // failed expect(obj1).toBe(obj1); // passed
# 통과 여부 : toEqual() expect(obj1).toEqual(obj2); // passed메소드 expect(obj1).toEqual(obj1); // passed
이런 식으로 다르게 처리가 되니 상황에 맞게 사용해야한다.
단순 원시적인 타입 값에 대한 비교에서는 큰 차이가 나지 않는다.
toStrictEqual() : toEqual()보다 엄격하게 체크한다.
toBeTruthy() : 값이 무엇인지 상관하지 않고 Boolean 값으로 체크할 때 사용한다.
toBeCalledWith() : 특정 인수로 호출되었는지 확인하는 데 사용한다.
# 함수 예시
const text = 'Hello World'
function test (word) {
return word;
}
test(text);
# spec.ts 예시
const text = 'Hello World'
expect(test).toBeCalledWith(text);
이외에 Matchers()는 Jest 공식 사이트에서 확인할 수 있다.
Mocking은 단위 테스트를 작성할 때 특정 코드가 어딘가에 의존하는 부분을 가짜로 대체하는 방법을 말한다.
왜 쓸까?
데이터 베이스에 연동해 데이터를 가져오는 등 외부의 어딘가에 의존하는 것은 테스트 환경을 조성하는 것 보다 더 많은 노력을 들게 만든다. 그러면 결국 더 빠르고 편리하게 테스트를 할 수 없는 환경에 이르는 것이다.
그렇기에 특정 기능만 분리해서 간단하게 특정 값이 된다는 가정
을 만들어 진행하게 되는 것이다.
Mocking은 TestCode를 어떻게 작성할 수 있을까?
# app.controller.ts
@Controller()
export class AppController {
contructor(
private readonly appService: AppService,
){}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
# app.controller.spec.ts
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
let appService: AppService;
beforeEach(() => {
appService = new AppService();
appController = new AppController(appService);
});
describe('getHello', () => {
it('Hello World 리턴해야됨', async () => {
// 가짜 서비스 미리 만들어놓기
jest
.spyOn(appService, 'getHello')
.mockImplementation(() => 'Hello World!');
// 테스트해보기
const result = await appController.getHello();
expect(result).toBe('Hello World!');
});
});
});
NestJS 프로젝트를 생성하면 기본적으로 생성되는 app.controller를 통해 testCode를 작성해보았다.
describe('AppController', () => {})
여기는 예제1에서 설명했던 것과 같이 그룹을 명시해서 해당 그룹 안에 관련된 함수를 각각 테스트 할 것이다. 큰 폴더! 클래스 이름! 으로 생각해도 좋다.
let appController: AppController;
let appService: AppService;
beforeEach(() => {
appService = new AppService();
appController = new AppController(appService);
});
beforeEach를 통해 테스트를 진행하기전에 꼭 해야할일을 명시해주어야 한다.
app.controller
에서는 app.service
에 의존해 사용하고 있으니 이 부분을 연결해주어야 한다.
그럼 이렇게 연결된 후에는 무얼 하면 될까? class의 함수를 테스트 해보면 된다. 이 함수는 어디에 쓴다고? it
!!
it('Hello World 리턴해야됨', async () => {
// 가짜 서비스 미리 만들어놓기
jest
.spyOn(appService, 'getHello')
.mockImplementation(() => 'Hello World!');
// 테스트해보기
const result = await appController.getHello();
expect(result).toBe('Hello World!');
});
참고자료 및 공식문서
https://hoony-gunputer.tistory.com/entry/jest-%EC%97%AC%EB%9F%AC%EA%B0%80%EC%A7%80-totoBe-toEqual-toStrictEqual-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0
https://jestjs.io/docs/getting-started