Search

10. 단위 테스트 시작하기

NestJS 프로젝트를 개발하다 보면 코드량이 많아지면서 수동 테스트만으로는 한계가 느껴질 때가 있다. 이 글에서는 수동 테스트에서 자동 테스트로 전환하는 방법과 NestJS에서 자주 사용하는 테스트 유형을 살펴본다.
NestJS 프로젝트에서는 크게 두 가지 테스트 유형을 사용한다.
1.
단위 테스트 (Unit Test): 클래스 하나에 집중하여 해당 클래스의 메서드들이 예상대로 동작하는지 확인하는 테스트이다. 주로 의존성 주입과 관련하여 각 메서드의 개별 동작을 검증한다.
2.
통합 테스트 / 엔드투엔드 테스트 (Integration / E2E Test): 애플리케이션 전체를 실행하여 요청과 응답이 예상대로 이루어지는지를 확인한다. 실제 웹 서버를 구동하여 API 엔드포인트를 테스트하는 데 사용된다.

1. 단위 테스트

단위 테스트를 작성하기 전에, NestJS 의존성 주입(DI) 시스템을 이해하는 것이 중요하다. 단위 테스트를 작성할 때 의존성 주입으로 인해 복잡도가 증가할 수 있다.
NestJS 프로젝트를 생성하면 기본적으로 test 디렉토리가 포함되어 있다.
이 디렉토리에는 기본적인 엔드투엔드 테스트 파일이 포함되어 있으며, 추가로 서비스와 컨트롤러에 대한 단위 테스트 파일도 작성할 수 있다.

2. Auth Service 단위 테스트 작성

인증 서비스의 단위 테스트를 작성해보자.
인증 서비스에는 회원가입(SignUp)과 로그인(SignIn) 메서드가 포함되어 있다.
인증 서비스를 테스트하려면 UserServiceUserRepo의 인스턴스를 함께 생성해야 하는 문제가 발생한다.
UsersServiceUsersRepo를 이용하고, UsersRepo는 SQLite를 사용하기 때문에 단순히 AuthService를 테스트하려고 해도 의존성 문제로 인해 복잡해진다.
이를 해결하기 위해서 실제 UsersService 대신 가짜 UsersService를 만들어 사용한다. 테스트 파일 안에 임시 클래스를 정의하여 필요한 메서드만 포함시키면, 의존성을 단순화할 수 있다.
테스트를 위해 소형 DI 컨테이너를 만들어, 실제 애플리케이션에서 사용하는 것과 유사한 구조를 재현한다.
이 컨테이너 안에 인증 서비스와 가짜 UsersService를 넣어 실제 서비스와 동일하게 작동하도록 속인다.

2.1. 인증 서비스 테스트 파일 생성

먼저, 프로젝트의 users 디렉터리 안에 auth.service.spec.ts라는 파일을 새로 생성한다.
우선 필요한 모듈을 가져온다.
다음으로, 첫 번째 테스트 블록을 작성한다.
it('can create an instance of auth service', async () => { const module = await Test.createTestingModule({ providers: [AuthService], }).compile(); const service = module.get(AuthService); expect(service).toBeDefined(); });
TypeScript
복사

2.2. 테스트 코드 실행

테스트를 실행하려면 터미널에서 다음 명령어를 입력한다.
npm run test:watch
TypeScript
복사
테스트가 실행되면 인증 서비스 인스턴스를 만들 수 없다는 오류가 발생한다.
이는 인증 서비스의 의존성인 UsersService가 설정되지 않았기 때문이다.

2.3. 가짜 UsersService 작성

이를 해결하기 위해 가짜 UsersService를 만든다. 가짜 서비스는 필요한 메서드만을 최소한으로 구현하여 테스트를 단순화한다.
const fakeUsersService = { find: () => Promise.resolve([]), create: (email: string, password: string) => Promise.resolve({ id: 1, email, password }), };
TypeScript
복사
이제 테스트 모듈의 프로바이더 설정에 이 가짜 서비스를 추가한다.
const module = await Test.createTestingModule({ providers: [ AuthService, { provide: UsersService, useValue: fakeUsersService, }, ], }).compile();
TypeScript
복사
수정된 코드를 실행하면 테스트가 정상적으로 동작한다. 이는 인증 서비스가 의존하는 UsersService를 가짜로 대체하여 문제를 해결했기 때문이다.
NestJS 애플리케이션에서 테스트 모듈 안의 프로바이더 배열을 살펴보면, 이 배열이 DI 컨테이너에 주입할 모든 클래스 목록이라는 점을 알 수 있다.
프로바이더 배열은 DI 컨테이너에 주입하려는 모든 클래스의 리스트이다. 이 배열에 클래스를 추가하면 DI 컨테이너는 그 클래스를 인스턴스화할 수 있는 방법을 알게 된다. 그리고 해당 클래스의 인스턴스를 생성할 때 필요한 모든 의존성 인스턴스도 함께 생성한다.
테스트 모듈에서 첫 번째 줄은 다음과 같이 동작한다. AuthService를 프로바이더 배열에 추가함으로써 DI 컨테이너는 AuthService의 인스턴스를 만들 수 있게 된다. DI 컨테이너는 AuthService의 생성자 인수를 통해 이 클래스의 의존성이 UsersService라는 것을 인식한다.
두 번째 요소는 조금 더 흥미롭다. DI 시스템을 속이거나 경로를 바꾸는 객체로, UsersService 대신 fakeUsersService를 사용하도록 규칙을 정의한다. 이는, 누군가 UsersService의 인스턴스를 요청하면 fakeUsersService를 반환한다는 의미이다.
이렇게 생성된 AuthService 인스턴스에서는 UsersService 속성이 fakeUsersService 객체로 대체된다. 이로 인해, AuthService의 메서드에서 UsersService를 사용할 때 실제 구현 대신 가짜 메서드가 호출된다.
auth.service.spec.ts
const fakeUsersService = { find: () => Promise.resolve([]), create: (email: string, password: string) => Promise.resolve({ id: 1, email, password }), };
TypeScript
복사
users.service.ts
async signup(email: string, password: string) { const users = await this.userService.find(email); if (users.length) { throw new BadRequestException('email in use'); } }
TypeScript
복사
테스트에서 signup 메서드를 호출하면 내부에서 usersService.find 메서드가 실행되는데, 이때 실제 구현이 아닌 fakeUsersService.find 메서드를 사용하여 빈 배열을 반환하게 된다.
이를 응용하면 이메일 주소 사용 여부를 테스트할 때에는 find 메서드의 반환값을 이메일 주소가 이미 사용 중인 것으로 설정할 수 있다. 이를 통해 signup 메서드의 동작을 검증할 수 있다.

가짜 객체에 find와 create 메서드만 정의한 이유

실제 사용자 서비스에는 다양한 함수가 연결되어 있지만, 이 가짜 객체에는 find와 create 메서드만 정의했다. 그 이유는 인증 서비스에서 이 두 가지 메서드만 사용하기 때문이다.
signup 메서드는 내부적으로 createfind를 호출한다.
signin 메서드는 find만 호출한다.
따라서 가짜 서비스에서는 이 두 메서드만 있으면 충분하다. 실제 사용자 서비스의 모든 함수를 정의할 수도 있지만, 호출되지 않기 때문에 굳이 작성할 필요가 없다.

Promise.resolve를 사용하는 이유

find와 create 메서드는 본질적으로 비동기 함수이다. SQLite 데이터베이스와의 통신은 시간이 걸리기 때문에 이 함수들은 Promise를 반환한다.

2.4. 타입스크립트 호환성 문제 개선

실제 UsersService를 대체하여 가짜 서비스로 사용하려면 타입 호환성 문제가 발생한다. UsersService에는 다양한 메서드와 속성이 있지만, 가짜 서비스에는 이를 모두 구현하지 않았기 때문이다.
이를 해결하기 위해 TypeScript의 Partial 타입을 활용한다.
const fakeUsersService: Partial<UsersService> = { find: () => Promise.resolve([]), create: (email: string, password: string) => Promise.resolve({ id: 1, email, password } as User), };
TypeScript
복사
Partial<UsersService>를 사용하면 UsersService의 속성을 부분적으로만 정의해도 타입 검사를 통과할 수 있다. 이렇게 하면 find와 create 메서드만 가짜로 구현하면서도, 타입스크립트의 타입 검사를 통과할 수 있다.
이때, create 메서드는 User 엔터티 인스턴스를 반환해야 하는데, 가짜 서비스에서는 간단한 객체로 반환하려다 보니 타입 호환 문제가 발생한다. 이를 해결하기 위해 as User를 사용하여 타입 캐스팅을 수행한다. 이렇게 하면 User 엔터티처럼 취급되어, create 메서드가 올바른 타입을 가지게 된다.

2.5. 테스트 파일 리팩토링하기

import { Test } from '@nestjs/testing'; import { AuthService } from './auth.service'; import { UsersService } from './users.service'; import { User } from './users.entity'; it('can create an instance of auth service', async () => { // Create a fake copy of the users service const fakeUsersService: Partial<UsersService> = { find: () => Promise.resolve([]), create: (email: string, password: string) => Promise.resolve({ id: 1, email, password } as User), }; const module = await Test.createTestingModule({ providers: [ AuthService, { provide: UsersService, useValue: fakeUsersService, }, ], }).compile(); const service = module.get(AuthService); expect(service).toBeDefined(); });
TypeScript
복사
auth.service.spec.ts
기존 코드에서는 하나의 it 블록에서 테스트 설정을 모두 처리하고 있다.
이 작업을 모든 테스트마다 반복하다 보면 코드 중복이 발생할 수 있다.
매번 같은 작업을 반복하는 대신, 공통 설정을 한곳에 모아 테스트 코드의 가독성을 높일 필요가 있다.
리팩토링의 핵심은 테스트 파일의 공통 설정을 한 번만 수행하고, 이를 각 테스트에서 재사용하는 것이다.
이를 위해 다음과 같은 리팩토링 단계를 거친다.

1) 공통 설정 코드 이동

기존의 it 블록 상단에서 테스트 설정을 모두 잘라낸다.
이후, beforeEach 블록에 해당 설정을 넣어 공통화한다.
이렇게 하면 각 테스트가 실행되기 전에 한 번씩 설정이 이루어지므로 중복이 사라진다.
beforeEach(async () => { // Create a fake copy of the users service const fakeUsersService: Partial<UsersService> = { find: () => Promise.resolve([]), create: (email: string, password: string) => Promise.resolve({ id: 1, email, password } as User), }; const module = await Test.createTestingModule({ providers: [ AuthService, { provide: UsersService, useValue: fakeUsersService, }, ], }).compile(); service = module.get(AuthService); }); it('can create an instance of auth service', async () => { expect(service).toBeDefined(); });
TypeScript
복사
auth.service.spec.ts

2) 서비스 변수의 범위 조정

beforeEach 안에서 선언된 service 변수는 지역 범위에 있어 it 블록과 범위가 다르다.
이를 해결하기 위해 최상위 범위에서 let service: AuthService; 를 선언한다.
이렇게 하면 모든 테스트에서 service 변수를 안전하게 참조할 수 있다.
let service: AuthService; beforeEach(async () => { // Create a fake copy of the users service const fakeUsersService: Partial<UsersService> = { find: () => Promise.resolve([]), create: (email: string, password: string) => Promise.resolve({ id: 1, email, password } as User), }; const module = await Test.createTestingModule({ providers: [ AuthService, { provide: UsersService, useValue: fakeUsersService, }, ], }).compile(); service = module.get(AuthService); }); it('can create an instance of auth service', async () => { expect(service).toBeDefined(); });
TypeScript
복사
auth.service.spec.ts

3) 테스트 구조 정리

테스트 파일의 구조를 더 깔끔하게 하기 위해, 모든 테스트 코드를 describe 블록으로 감싼다.
describe 블록은 논리적 그룹화와 설명을 추가하는 데 유용하여 가독성을 높인다.
import { Test } from '@nestjs/testing'; import { AuthService } from './auth.service'; import { UsersService } from './users.service'; import { User } from './users.entity'; describe('AuthService', () => { let service: AuthService; beforeEach(async () => { // Create a fake copy of the users service const fakeUsersService: Partial<UsersService> = { find: () => Promise.resolve([]), create: (email: string, password: string) => Promise.resolve({ id: 1, email, password } as User), }; const module = await Test.createTestingModule({ providers: [ AuthService, { provide: UsersService, useValue: fakeUsersService, }, ], }).compile(); service = module.get(AuthService); }); it('can create an instance of auth service', async () => { expect(service).toBeDefined(); }); });
TypeScript
복사
auth.service.spec.ts

3. AuthService Signup 메서드 테스트

기존 signup 메서드는 다음과 같은 논리로 동작한다.
1.
이메일과 비밀번호를 입력받는다.
2.
비밀번호에 해시와 솔트를 적용하여 안전하게 저장한다.
3.
새로운 사용자 엔터티를 생성하고 반환한다.
이 테스트에서 확인하고자 하는 것은 회원 가입 시 비밀번호가 평문으로 저장되지 않고 해시와 솔트가 적용되는지 여부이다.
테스트를 위해 다음 사항을 고려해야 한다.
find 메서드는 존재하지 않는 사용자를 찾을 때 빈 배열을 반환해야 한다.
create 메서드는 입력된 이메일과 비밀번호를 기반으로 사용자 객체를 반환해야 한다.
const fakeUsersService: Partial<UsersService> = { find: () => Promise.resolve([]), create: (email: string, password: string) => Promise.resolve({ id: 1, email, password } as User), };
TypeScript
복사
auth.service.spec.ts

3.1. signup 메서드가 성공적으로 새로운 사용자를 생성하는지 확인

이제 signup 메서드를 테스트한다. 중요한 점은 비밀번호가 평문으로 저장되지 않고, 해시와 솔트 처리되어야 한다는 것이다.
it('creates a new user with a salted and hashed password', async () => { const user = await service.signup('test@example.com', 'password123'); expect(user.password).not.toEqual('password123'); const [salt, hash] = user.password.split('.'); expect(salt).toBeDefined(); expect(hash).toBeDefined(); });
TypeScript
복사
테스트를 실행한 결과, 비밀번호가 평문으로 저장되지 않고 올바르게 해시와 솔트 처리되어 통과했다.
이제 의도적으로 실패 조건을 만들었을 때도 실패가 발생하는지 확인해보자.
1.
평문 비밀번호와 비교
expect(user.password).toEqual('password123');
TypeScript
복사
비밀번호가 해시화되고 솔트가 적용되었다면, 절대 평문 비밀번호와 일치할 수 없다.
2.
비밀번호가 솔트와 해시로 분리되는지 확인
const [salt, hash] = user.password.split('$'); expect(salt).toBeDefined(); expect(hash).toBeDefined();
TypeScript
복사
salthash 중 하나라도 undefined가 나올 경우 실패하게 된다.
이 경우 $ 기호를 기준으로 두 부분으로 분리되는지 확인하고 있는데, 우리는 .으로 hash와 salt를 구분하고 있기 때문에 테스트에 실패하게 된다.

3.2. 이미 사용 중인 이메일로 signup 호출 시 오류가 발생하는지 확인

다음으로 확인할 테스트는 이미 사용 중인 이메일로 가입 시 오류가 발생하는지 확인하는 것이다.
테스트를 작성하면서 가장 먼저 직면하는 문제는 usersService.find 메서드의 모킹이다.
두 가지 테스트 시나리오에서 이 메서드의 동작은 서로 충돌하는 경우가 있다.
1.
첫 번째 테스트: 새로운 사용자가 정상적으로 생성되는지 확인 → find 메서드는 빈 배열을 반환해야 함
2.
두 번째 테스트: 이미 사용중인 이메일로 signup을 호출할 때 오류 발생 → find 메서드는 사용자 객체를 포함한 배열을 반환해야 함
테스트 간 충돌을 방지하기 위해 fakeUsersService 객체를 각 테스트 케이스에 맞게 동적으로 수정해야 한다.
해결 방법은 다음과 같다.
먼저, fakeUsersService 객체를 전역으로 선언하여 여러 테스트 케이스에서 공유할 수 있도록 설정한다.
그리고 각 테스트에서 find 메서드를 직접 수정하여 요구 사항에 맞는 모킹을 제공한다.
it('throws an error if user signs up with email that is in use', async () => { fakeUsersService.find = () => Promise.resolve([ { id: 1, email: 'a', password: '1', } as User, ]); await expect( service.signup('test@example.com', 'password123'), ).rejects.toThrow(BadRequestException); });
TypeScript
복사
auth.service.spec.ts
비동기 함수에서 오류를 테스트하려면 await expect(...).rejects.toThrow(...)와 같이 비동기 에러를 명시적으로 처리해주어야 한다.

4. AuthService Signin 메서드 테스트

4.1. 사용자를 찾을 수 없는 경우 테스트

이메일이 존재하지 않을 때 signin 메서드가 오류를 던지는지 확인하려고 한다.
이 경우, find 메서드가 빈 배열을 반환해야 한다.
it('throws if signin is called with an unused email', async () => { await expect( service.signin('test@example.com', 'password123'), ).rejects.toThrow(NotFoundException); });
TypeScript
복사
auth.service.spec.ts
참고로, find 메서드의 기본 구현이 빈 배열을 반환하도록 설정되어 있어 별도 수정이 필요 없다.

4.2. 비밀번호가 일치하지 않는 경우 테스트

다음으로, 제공된 비밀번호가 데이터베이스에 저장된 비밀번호와 일치하지 않는 경우를 테스트한다.
it('throws if an invalid password is provided', async () => { fakeUsersService.find = () => Promise.resolve([ { id: 1, email: 'a', password: '1', } as User, ]); await expect( service.signin('test@example.com', 'password123'), ).rejects.toThrow(BadRequestException); });
TypeScript
복사

4.3. 올바른 비밀번호를 제공했을 때 테스트

마지막으로, 올바른 비밀번호를 제공했을 때 성공적으로 반환되는지 확인한다.
단순히, 다음과 같이 접근하려고 한다면 비밀번호 비교 과정이 올바르게 이루어지지 않는다.
// 잘못된 예제 it('returns a user if correct password is provided', async () => { fakeUsersService.find = () => Promise.resolve([ { id: 1, email: 'test@example.com', password: 'password123', } as User, ]); const user = await service.signin('test@example.com', 'password123'); });
TypeScript
복사
이는 실제 서비스에서 사용하는 비밀번호는 해시화되어 저장되기 때문이다.
따라서 테스트에서도 실제 서비스에서 사용하는 것과 동일한 방식으로 해싱된 비밀번호를 사용해야 한다.
그렇지 않으면 테스트가 항상 실패하게 된다.

 하드 코딩된 방식으로 해결

이 문제를 해결하는 가장 간단한 접근법은 서비스에서 사용하는 해싱 알고리즘을 사용하여 미리 해싱된 비밀번호를 생성하고, 이를 테스트 코드에 직접 작성하는 것이다.
it('returns a user if correct password is provided', async () => { fakeUsersService.find = () => Promise.resolve([ { id: 1, email: 'test@example.com', password: '1f3f1f78b727fa56.af7186ab0d0a...', } as User, ]); const user = await service.signin('test@example.com', 'password123'); expect(user).toBeDefined(); });
TypeScript
복사
위와 같이 하드 코딩된 해싱 비밀번호를 사용하면 테스트는 정상적으로 통과한다. 하지만 이 방법에는 다음과 같은 문제점이 있다.
1.
유연성 부족: 비밀번호 해싱 알고리즘이나 솔트가 변경될 경우, 테스트 코드를 모두 수정해야 한다.
2.
보안 문제: 해싱된 비밀번호 자체를 코드에 직접 노출하기 때문에 보안상 취약하다.
3.
신뢰성 문제: 코드 유지보수 중 실수로 해싱 비밀번호를 수정하면, 해당 테스트가 실패할 수 있다.

 동적으로 해싱 비밀번호 생성

이 문제를 해결하기 위해, 테스트 코드 내에서 해싱된 비밀번호를 동적으로 생성하여 사용하면 보다 안전하고 유연하다.
const users: User[] = []; fakeUsersService = { find: (email: string): Promise<User[]> => { const filteredUsers = users.filter((user) => user.email === email); return Promise.resolve(filteredUsers); }, create: (email: string, password: string): Promise<User> => { const user = { id: Math.floor(Math.random() * 999999), email, password, } as User; users.push(user); return Promise.resolve(user); }, };
TypeScript
복사
우선 사용자를 메모리에 저장하기 위해 배열을 만들고, 이를 User[] 타입으로 선언한다.
find 메서드는 주어진 이메일로 사용자를 찾는 역할을 한다. 이때, 실제로 이메일을 기반으로 사용자 목록을 필터링하여 반환한다.
create 메서드는 새 사용자를 생성하여 users 배열에 추가하는 역할을 한다. 사용자 객체를 생성하고 배열에 삽입한다. 이때, idMath.random()을 활용하여 무작위로 생성한다.
이제 signupsignin 테스트를 쉽게 작성할 수 있다. fakeUsersService가 실제 사용자 목록을 저장하고 검색하는 방식으로 작동하므로, 사용자 추가와 인증 과정이 더욱 직관적으로 이루어진다.
it('throws an error if user signs up with email that is in use', async () => { await service.signup('test@example.com', 'password123'); await expect( service.signup('test@example.com', 'password123'), ).rejects.toThrow(BadRequestException); }); it('throws if an invalid password is provided', async () => { await service.signup('test@example.com', 'foo'); await expect( service.signin('test@example.com', 'password123'), ).rejects.toThrow(BadRequestException); }); it('returns a user if correct password is provided', async () => { await service.signup('test@example.com', 'password123'); const user = await service.signin('test@example.com', 'password123'); expect(user).toBeDefined(); });
TypeScript
복사

5. UserController 테스트

인증 서비스 테스트는 충분히 진행했으므로 이제 애플리케이션 내부, 특히 사용자 컨트롤러에 대한 테스트를 진행해보자. 사용자 컨트롤러 테스트는 두 가지 이유로 흥미롭다.
첫 번째 이유는 사용자 컨트롤러가 두 가지 의존성(UsersServiceAuthServices)을 가지기 때문에 두 서비스의 가짜 사본을 만들어야 한다는 점이다. 앞서 인증 서비스 테스트에서 fakeUsersService를 사용했던 것처럼 두 배로 모의 구현이 필요하다.
export class UsersController { constructor( private usersService: UsersService, private authService: AuthService, ) {} }
TypeScript
복사
users.controller.ts
두 번째 이유는 테스트할 내용이 상대적으로 적다는 점이다. 예를 들어, whoAmI 라우트 핸들러는 사용자 인수를 받아 그대로 반환하는 단순한 구조이기 때문에 테스트할 부분이 많지 않다. 메서드에 연결된 데코레이터를 테스트하는 것은 Nest 환경에서는 상당히 복잡하여 일반적으로 데코레이터를 제외한 부분만 테스트한다.
@Get('/whoami') @UseGuards(AuthGuard) whoAmI(@CurrentUser() user: User) { return user; }
TypeScript
복사
users.controller.ts

5.1. 테스트 파일 설정

사용자 컨트롤러 테스트를 시작하려면 users.controller.spec 파일을 열어야 한다. 이 파일은 기본적으로 생성되어 있으며, 이전에 작성한 인증 테스트 파일과 구조가 유사하다.
먼저 다음과 같은 import 문을 추가한다.
import { UsersService } from './users.service'; import { AuthService } from './auth.service'; import { User } from './users.entity';
TypeScript
복사
users.controller.spec
생성된 테스트 모듈에서, 격리된 DI 컨테이너를 사용하여 UsersController 인스턴스를 만든다. DI 컨테이너 안에 필요한 의존성을 제공하지 않으면 오류가 발생하므로, 두 서비스의 가짜 사본을 만들어야 한다.
따라서, 다음과 같이 두 가지 변수를 선언한다.
describe('UsersController', () => { let controller: UsersController; let fakeUsersService: Partial<UsersService>; let fakeAuthService: Partial<AuthService>; ...
TypeScript
복사
그리고 사용자 컨트롤러에서 실제 사용하는 메서드를 파악하여, 테스트 파일의 beforeEach 블록에서 다음과 같이 구현한다.
beforeEach(async () => { fakeUsersService = { findOne: () => {}, find: () => {}, remove: () => {}, update: () => {}, }; fakeAuthService = { signup: () => {}, signin: () => {}, }; ...
TypeScript
복사

1) findOne 메서드 모의 구현

findOne 메서드는 숫자 타입의 id를 받아서 User 엔터티를 반환하는 Promise를 반환해야 한다.
findOne: (id: number) => { return Promise.resolve({ id, email: 'test@example.com', password: 'password123', } as User); },
TypeScript
복사

2) find 메서드 모의 구현

find 메서드는 이메일을 받아서 사용자 목록을 배열 형태를 반환하는 Promise를 반환해야 한다.
find(email: string): Promise<User[]> { return Promise.resolve([{ id: 1, email, password: 'randomPassword', } as User]); }
TypeScript
복사

3) 모의 서비스 등록하기

NestJS의 DI 컨테이너에 모의 서비스를 주입하여 테스트 컨트롤러가 이를 사용하도록 설정한다. provide를 이용하여 실제 서비스 대신 모의 서비스를 주입한다.
const module: TestingModule = await Test.createTestingModule({ controllers: [UsersController], providers: [ { provide: UsersService, useValue: fakeUsersService, }, { provide: AuthService, useValue: fakeAuthService, }, ], }).compile();
TypeScript
복사

4) 테스트 코드 작성하기

NestJS에서 컨트롤러 메서드를 테스트할 때 가장 중요한 점은 컨트롤러 자체의 로직이 단순해야 한다는 것이다. 일반적으로 컨트롤러는 서비스의 메서드를 호출하고, 받은 결과를 반환하는 역할만을 수행해야 한다.
findAllUsers 메서드 (이메일이 올바르게 반환되는지 확인)
it('findAllUsers returns a list of users with the given email', async () => { const users = await controller.findAllUsers('asdf@asdf.com'); expect(users.length).toEqual(1); expect(users[0].email).toEqual('asdf@asdf.com'); });
TypeScript
복사
findUser 메서드 (올바른 id값을 넘겼을 때 유저가 반환되는지 확인)
it('findUser returns a single user with the given id', async () => { const user = await controller.findUser('1'); expect(user).toBeDefined; });
TypeScript
복사
findUser 메서드 (잘못된 id값을 넘겼을 때 예외처리 되는지 확인)
it('findUser throws an error if user with given id is not found', async () => { fakeUsersService.findOne = async () => null; await expect(controller.findUser('1')).rejects.toThrow(NotFoundException); });
TypeScript
복사
사용자를 찾을 수 없을 때를 테스트하려면 모의 서비스의 findOne 메서드를 null로 설정하여 예외를 발생시킬 수 있다.
signin 메서드 (세션 객체를 업데이트하고 사용자를 반환하는지 확인)
it('signin updates session object and returns user', async () => { const session = { userId: -10 }; const user = await controller.signin( { email: 'asdf@asdf.com', password: 'asdf', }, session, ); expect(user.id).toEqual(1); expect(session.userId).toEqual(1); });
TypeScript
복사
참고로 테스트에서 메서드 호출을 직접 확인할 때 데코레이터를 실행하거나 사용할 수 없다. 따라서 GET 요청이나 쿼리 문자열로 직접 테스트할 수 없으며, 단지 메서드 자체의 논리만을 확인할 수 있다. 데코레이터를 테스트하려면 엔드투엔드(E2E) 테스트가 필요하다.