백엔드에서 구현한 API를 iOS에서 이용하는 과정에서 오류를 찾거나, 변동 사항이 생기면서 API를 수정해야 할 일이 잦아졌습니다. 저희는 서비스 로직을 여러 곳에서 재활용 할 수 있도록 해 둔 상태였는데, 수정을 거치면서 예상치 못한 곳에서 다른 이슈가 발생해 다시 해당 API를 수정해야 했습니다.
그리고 로직을 수정 할 때마다 로직이 의도에 맞게 잘 동작하고 있는지 확인을 해야 했고, 이를 위해서 Swagger나 Postman으로 요청을 보내면서 결과를 확인했습니다. 하지만 매 번 모든 경우를 확인하지는 못했기 때문에 놓쳤던 곳에서 에러가 발생해 다시 코드를 수정하기도 했습니다.
이런 과정을 거치면서 테스트 코드의 필요성에 대해 실감하게 되었고, 테스트 코드를 작성하기로 했습니다.
describe('CategoriesService', () => {
let service: CategoriesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CategoriesService],
}).compile();
service = module.get<CategoriesService>(CategoriesService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
테스트 코드 작성에 익숙하지 않았기 때문에, 비교적 간단했던 CategoriesService
에 대한 테스트 코드를 먼저 작성해보면서 테스트 코드에 대한 감을 잡으려고 했습니다. nest g categories
명령어를 통해 컴포넌트를 생성하면 위와 같이 서비스에 대한 테스트 코드가 자동으로 생성됩니다. 그래서 위 테스트를 실행하기 위해 npm run test
명령을 하면 의존성과 관련된 에러가 발생합니다. 테스트 코드에 특별한 로직이 포함되어 있지도 않은데, 단순히 CategoriesService
를 불러오는 것 만으로도 에러가 발생합니다. 에러 메세지에는 의존성과 관련된 문제가 발생했으며, 첫 번째 파라미터가 문제라는 내용이 담겨 있습니다.
export class CategoriesService {
constructor(
@InjectRepository(Categories)
private categoriesRepository: Repository<Categories>,
private usersService: UsersService,
) {}
CategoriesService
의 생성자에 있는 첫 파라미터는 Categories
entity를 이용하는 TypeORM Repository
였습니다. 테스트 코드의 모듈 부분에서 필요한 것들에 대해 import 해주지 않아서 에러가 발생한 것으로 판단할 수 있습니다. 그러면 다른 로직을 작성했던 것 처럼 test 모듈에도 똑같이 import를 해주면 되는 걸까요?
테스트 코드를 작성할 때에는 외부에 의존하는 부분을 분리하고 테스트 할 부분에 집중합니다. DB에 직접 접근하는 경우 데이터베이스의 내용이 계속해서 바뀌기 때문에 테스트가 항상 같은 결과가 나오는 것을 보장할 수 없습니다. 그리고 DB 외에 외부 서비스 로직에 의존성이 있는 경우에도 해당 서비스 코드의 변화에 따라서 테스트의 결과가 바뀌게 됩니다. 그래서 테스트 코드를 작성할 때는 외부에 의존하는 부분을 Mocking해서 사용합니다. Mocking에도 여러 방법이 있지만, 저희는 Repository
, UsersService
와 같은 동작을 하도록 Mock Class를 작성해서 활용했습니다.
class MockCategoriesRepository {
private data = categoriesData;
create(entity: object): object {
return {
id: this.data.length + 1,
...entity,
};
}
save(entity: object): Promise<object> {
return Promise.resolve(entity);
}
find({ where: { user_id } }): Promise<object> {
const categories = this.data.filter(
(category) => category.user_id === user_id.id,
);
return Promise.resolve(categories);
}
}
class MockUsersService {
private data: UsersModel[] = usersData as UsersModel[];
findUserById(user_id: number): Promise<object> {
const user = this.data.find((user) => user.id === user_id);
return Promise.resolve(user);
}
}
CategoriesService
에서 사용하는 메서드를 갖고 있으며, 해당 메서드와 같은 동작을 해서 대신 사용할 수 있도록 일종의 꼭두각시를 만든 셈입니다. 이렇게 Mocking한 클래스를 TestingModule에 import 해줍니다.
describe('CategoriesService', () => {
let service: CategoriesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CategoriesService,
{
provide: getRepositoryToken(Categories),
useClass: MockCategoriesRepository,
},
{
provide: UsersService,
useClass: MockUsersService,
},
],
}).compile();
service = module.get<CategoriesService>(CategoriesService);
});
A
라는 객체를 Mocking한 B
로 대체하고 싶다고 하면 provide
에는 대체할 객체(A
)를, useClass
에는 B
를 작성하면 됩니다. 이렇게 하면 서비스 로직에서 외부에 의존해야 할 때 (provide
에 적힌 객체를 사용해야 할 때), useClass
에 적힌 객체를 대신 사용하게 됩니다.