TDD에 대한 정리
TDD(Test-Driven Development)는 개발자가 코드를 작성하기 전에 테스트를 먼저 작성하는 방법론입니다.
이 방법론의 핵심은 “테스트가 통과하는 코드를 최소한으로 작성하고, 그 후에 리팩토링을 진행하면서 기능을 확장해 나간다”는 점입니다.
TDD의 기본 사이클은 Red - Green - Refactor로 나뉩니다.
• Red: 실패하는 테스트를 먼저 작성합니다.
• Green: 테스트를 통과할 수 있는 최소한의 코드를 작성합니다.
• Refactor: 테스트가 통과한 후 코드를 개선하고 최적화 합니다.
이 방법론을 사용하면 코드의 안정성을 유지할 수 있으며, 리팩토링 시 테스트를 통해 기능이 올바르게 동작하는지 확인할 수 있습니다.
TDD를 사용하는 이유
장점
• 높은 코드 품질: 테스트를 먼저 작성하므로, 코드를 작성할 때 예상치 못한 에러나 논리적 오류를 미리 방지할 수 있습니다.
• 리팩토링의 용이성: 기능이 변경될 때마다 테스트를 통해 시스템 전체의 동작을 확인할 수 있으므로, 리팩토링 시에도 큰 문제가 발생하지 않습니다.
• 자동화된 테스트: 반복적인 수동 테스트 없이도 코드를 변경할 때마다 테스트를 자동으로 수행할 수 있습니다.
• 자신감 있는 개발: 기능 구현 중에도 테스트가 코드의 동작을 보증하기 때문에, 개발자는 자신 있게 코드를 수정하거나 기능을 추가할 수 있습니다.
단점
• 초기 개발 속도가 느릴 수 있음: TDD를 처음 도입하면 테스트 작성에 추가적인 시간이 필요하기 때문에 개발 속도가 느려질 수 있습니다.
• 테스트 유지보수 비용: 코드가 변경되면 테스트 코드도 수정해야 하므로, 기능이 자주 바뀌는 프로젝트에서는 테스트 유지보수가 부담이 될 수 있습니다.
NestJS에서 TDD의 장점
NestJS는 테스트 작성에 매우 적합한 프레임워크입니다.
• Jest 내장: NestJS는 기본적으로 Jest를 내장하고 있어, 별도의 추가 설정 없이도 테스트 환경을 쉽게 구축할 수 있습니다.
• 모듈화된 구조: NestJS의 모듈화 구조 덕분에 각 모듈과 서비스가 독립적으로 테스트하기 쉬워집니다.
• DI(Dependency Injection) 지원: 테스트를 작성할 때 의존성을 쉽게 주입하여, 테스트의 복잡도를 줄이고 Mock 객체를 사용한 유연한 테스트를 가능하게 합니다.
TDD 테스트 전략
테스트 전략은 프로젝트의 크기나 목표에 따라 다양하게 선택할 수 있습니다.
일반적으로 사용하는 주요 테스트 전략은 다음과 같습니다.
1. 유닛 테스트 (Unit Test)
• 정의: 개별 함수나 메소드 단위로 동작을 확인하는 테스트입니다. 외부 종속성을 Mocking하여 독립적으로 테스트할 수 있습니다.
• 목표: 가장 작은 단위의 코드가 예상대로 동작하는지 확인합니다.
• 예시: 회원가입 함수에서 이메일 유효성 검사나 비밀번호 정책을 테스트합니다.
2. 통합 테스트 (Integration Test)
• 정의: 여러 모듈이 서로 올바르게 작동하는지 테스트하는 방법입니다. 주로 데이터베이스나 외부 API와의 통합 작업을 테스트합니다.
• 목표: 여러 시스템 간의 상호작용이 의도한 대로 동작하는지 확인합니다.
• 예시: 회원가입 API에서 사용자 데이터를 데이터베이스에 저장하는 과정까지 테스트합니다.
3. 기능 테스트 (Functional Test)
• 정의: 사용자의 행동과 가장 가까운 수준에서 테스트를 수행하는 방법입니다. 시스템의 기능이 사용자 관점에서 올바르게 작동하는지 확인합니다.
• 목표: 사용자가 기대하는 동작을 실제로 수행하는지 확인합니다.
• 예시: 회원가입 폼을 제출했을 때 모든 유효성 검사와 데이터베이스 처리가 올바르게 완료되는지 테스트합니다.
4. 엔드투엔드 테스트 (End-to-End Test, E2E)
• 정의: 시스템의 시작부터 끝까지 모든 흐름을 테스트하는 방식입니다.
• 목표: 전체 시스템이 통합되어 있을 때 모든 흐름이 제대로 작동하는지 확인합니다.
• 예시: 사용자가 회원가입을 완료한 후 로그인하여 마이페이지에 접근할 수 있는지 테스트합니다.
NestJS에서 TDD를 위한 설치
NestJS는 TDD 환경을 쉽게 구축할 수 있도록 기본적으로 Jest를 내장하고 있습니다.
설치 과정은 간단하며 프로젝트 생성 시 자동으로 설정되기도 합니다.
npm install --save-dev jest @types/jest ts-jest
npm install --save-dev @nestjs/testing
NestJS는 기본적으로 @nestjs/testing 모듈을 제공하여 테스트 환경 구성이 간단합니다.
이 모듈을 사용하면 DI 컨테이너를 설정하고, 서비스를 쉽게 모킹할 수 있습니다.
TDD 사용 방법
NestJS에서 TDD를 적용할 때 사용하는 주요 메소드와 그 용도를 이해하는 것이 중요합니다.
아래는 TDD를 위한 Jest와 NestJS Testing Module의 주요 메소드들과 그 사용법을 정리한 것입니다.
1. describe
테스트를 그룹으로 묶어주는 메소드.
테스트 그룹을 만들고, 테스트의 전반적인 주제를 설명하는 데 사용됩니다.
describe('AuthService', () => {
// 해당 테스트 그룹에 속하는 테스트 케이스 작성
});
2. it
실제로 테스트할 개별 케이스를 정의하는 메소드.
각 테스트 케이스의 구체적인 동작을 설명합니다.
it('이메일이 없을 경우 에러를 반환한다', () => {
// 테스트 케이스 작성
});
3. beforeEach
각 테스트 케이스가 실행되기 전에 실행할 초기화 코드를 정의하는 메소드.
Mock 객체 생성, 모듈 컴파일 등을 이곳에서 설정할 수 있습니다.
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [AuthService],
}).compile();
authService = moduleRef.get<AuthService>(AuthService);
});
// 이 코드는 테스트 모듈을 설정하고, 각 테스트 전에 해당 모듈에서 서비스를 가져오는 작업을 합니다.
4. expect
테스트의 기대 결과를 설정하는 메소드.
특정 값이 예상되는지, 예외가 발생하는지 등을 정의합니다.
// result가 10인지 확인
expect(result).toBe(10);
// 예외 발생 여부 확인
expect(service.createUser(user)).rejects.toThrowError('이메일이 필요합니다');
5. jest.fn
Jest에서 Mock 함수를 생성하는 메소드.
Mocking을 통해 실제 동작 대신 예상되는 동작을 정의하고, 테스트에서 특정 동작을 대체할 수 있습니다.
const mockFunction = jest.fn().mockReturnValue('Hello');
expect(mockFunction()).toBe('Hello');
6. spyOn
클래스나 객체의 특정 메소드를 감시하는 기능을 제공합니다.
메소드가 호출되었는지, 어떤 값으로 호출되었는지 등을 확인할 수 있습니다.
const spy = jest.spyOn(authService, 'signUp');
authService.signUp(user);
expect(spy).toHaveBeenCalledWith(user);
7. rejects / resolves
비동기 함수에서의 결과나 에러를 테스트할 때 사용됩니다.
resolves는 비동기 함수가 성공적으로 완료될 때, rejects는 에러가 발생했을 때 테스트를 작성합니다.
await expect(authService.signUp(user)).resolves.not.toThrow();
await expect(authService.signUp(invalidUser)).rejects.toThrowError('유효하지 않은 이메일입니다');
8. toThrowError
특정 함수가 에러를 발생시키는지 테스트할 때 사용합니다.
특히 예외 처리가 중요한 로직에서 사용됩니다.
expect(() => authService.signUp(invalidUser)).toThrowError('유효하지 않은 이메일입니다');
9. toBe / toEqual
두 값이 정확히 일치하는지 확인할 때 사용합니다.
toBe()는 기본 자료형의 동등성을 검사하고
toEqual()는 객체나 배열의 값을 비교할 때 사용됩니다.
expect(result).toBe(5); // 기본 자료형 비교
expect(user).toEqual({ email: 'test@test.com', password: 'password' }); // 객체 비교
10. jest.clearAllMocks
테스트 실행 중에 생성된 모든 Mock 함수나 스파이를 초기화하는 메소드.
Mock 데이터가 여러 테스트에 영향을 미치지 않도록 클리어할 때 유용합니다.
afterEach(() => {
jest.clearAllMocks(); // 모든 Mock 함수 초기화
});
마무리
TDD는 코드의 품질과 안정성을 높이는 매우 유용한 개발 방법론이지만, 현실적으로 모든 팀에서 바로 적용하기는 쉽지 않습니다.
특히, 제가 근무하고 있는 스타트업 환경에서는 빠른 기능 개발이 우선시되기 때문에 TDD를 전면적으로 도입하기 어려운 상황이 많았습니다.
스타트업은 자원이 한정되어 있고, 개발 속도가 중요한 요소로 작용합니다. 이러한 이유로 인해 TDD를 적용하는 데에 많은 어려움이 있었고, 주로 “빠른 해결”을 요구하는 상황이 빈번했습니다. 테스트를 먼저 작성하고, 이를 기반으로 코드를 작성하는 TDD 프로세스는 단기적으로는 개발 속도가 느려 보일 수 있기 때문에, 현실적으로 TDD를 도입하지 못한 경험이 있습니다.
하지만 개인적으로 TDD에 대한 필요성을 느끼고, 더 나은 코드 품질과 유지보수성을 위해 공부하고 있습니다.
이 블로그 역시 TDD를 학습하며 정리한 개인적인 기록입니다.
'Javascript > Nest.js' 카테고리의 다른 글
Nest.Js 란 ? (0) | 2022.12.02 |
---|