레포지토리에 대한 테스트를 작성했다.
OutboundPortAdopter 에서 실제로 DB와 연결되는 Repository의 메서드를 테스트 하기 위해 테스트용 DB를 연결하고 beforeAll
, beforeEach
, AfterEach
, AfterAll
만 잘 설정하고 각 메서드를 실행시켜, 결과값을 적절하게 예상하면 쉽게 끝날 것이라 생각했다.
그러나 예상과는 다르게 3일에 걸쳐 설정을 완료했으며, 그 과정에서 헤맸던 우여곡절을 적어본다.
나는 현재 서버에서 repository 메서드를 mikroORM 을 통해 entityManager로 구현하고 있었다.
entityManager로 작성하면 entityRepository 처럼 일일히 엔티티에 대한 리포지토리를 생성할 필요없이 하나의 매니저로 관리할 수 있다.
describe('Todo Repository', () => {
let todoRepository: TodoRepository;
let orm: MikroORM;
let em: EntityManager;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [ConfigModule.forRoot(), MikroOrmModule.forRoot(testConfig)],
providers: [TodoRepository],
}).compile();
todoRepository = module.get<TodoRepository>(TodoRepository);
orm = module.get<MikroORM>(MikroORM); //<---------------- undefined !!
em = module.get<EntityManager>(EntityManager);
});
...
처음에는 이렇게 모듈을 가지고 인스턴스를 생성하여 테스트를 진행하려고 했다.
그러나 생성된 module 인스턴스에서 orm을 불러올 수 가 없었다. (이건 지금도 왜 안되는지 모르겠다)
또한 테스트 실행 이후에 정상적인 종료를 위해서는, entitymanager가 orm으로부터 추출된 것이어야 했다.
만약 entitymanager가 여러 개 생성된다면, orm 을 종료시켜도 em는 작업하느라 정상종료가 안 될 수도 있다고 판단했다.
따라서, 나는 orm을 인스턴스로 초기화 시키고, 해당 orm에서 em 를 가져와 그것을 repository에 주입하기로 했다.
이러면 하나의 orm에 포함된 em 를 가지고 테스트하므로 afterAll에서 정상적으로 close()를 통해 데이터베이스와 연결을 종료할 수 있을 것이다.
describe('Todo Repository', () => {
let todoRepository: TodoRepository;
let em: EntityManager;
let orm: MikroORM;
beforeAll(async () => {
orm = await MikroORM.init({ ...testConfig, driver: PostgreSqlDriver });
em = orm.em;
todoRepository = new TodoRepository(em);
...
}
...
afterAll(async () => {
await em.nativeDelete(Users, {});
await orm.close();
});
테스트케이스를 작성하기 위해서는 해당 메서드의 실행결과를 직접 하드코딩해 두어야 한다.
하지만 데이터베이스의 테이블을 인스턴스화한 엔티티를 보면, 외래키로 연관된 속성들은 해당 테이블의 엔티티 타입으로 설정되어 있다.
import { Entity, ManyToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
import { Users } from './Users';
@Entity()
export class Todos {
[OptionalProps]?: 'completed';
@PrimaryKey({ columnType: 'int8' })
TodoId!: string;
@ManyToOne({ entity: () => Users, fieldName: 'user', onUpdateIntegrity: 'cascade' })
user!: Users;
@Property({ length: 50 })
name!: string;
@Property({ length: 6 })
startTime!: Date;
@Property({ length: 6 })
endTime!: Date;
@Property({ default: false })
completed: boolean = false;
@Property({ length: 1000, nullable: true })
description?: string;
}
Todos 의 경우, 해당 Todo가 누구의 것인지 적는 user가
단순히 userId : string(=bigInt) 이 아닌, User 라는 엔티티 타입으로 설정되어 있다.
이것은 데이터베이스에서 테이블의 연관관계를 인스턴스에서 표시하기 위함이다.
그렇다면 테스트 케이스를 작성할 때, 어떻게 될까?
expect(result).toEqual({
id: '50',
name: '8월 8일에 추가한 투두 테스트1',
startTime: new Date('2023-08-08 16:00:00'),
endTime: new Date('2023-08-21 18:00:00'),
description: 'test로 생성된 투두입니다.',
completed: false,
user: {
userId : '1',
password : 'asdfasdfasdfa',
nickname: 'testuser',
loginMethod : 'LOCAL'
...
});
이 테스트 코드처럼 저기 user에 대한 모든 정보를 일일히 쳐주어야 한다.
그럴경우, 메인키가 아닌 다른 컬럼의 값이 바뀌게 되면 일일히 모두 수정해야하고,
단순히 Todo와 User 관계뿐만아 아니라 다른 엔티티가 포함된 테스트에서도 이 하드 코딩을 해줘야 한다는 말이다.
차라리 findOne을 통해서 유저를 받아오고 그것을 넣어주는 것도 시도해 보았지만, 중요하지 않은 쿼리를 날리는 과정이 추가되기에, 최적의 방법은 아닌 것 같았다.
Users 엔티티의 기본 키인 userId 만을 가지고 넣을 수 있는 방법을 찾아보았고,
엔티티를 기본키로만 참조할 수 있는entityManager.getReference()
를 사용하기로 했다.
이 메서드는 엔티티를 데이터베이스에서 실제로 찾아오지는 않고, 해당 엔티티 타입으로 엔티티를 '찾았다 치고' 사용할 수 있다.
일종의 연산지연이니 더 경제적으로 사용할 수 있을 것 같다.
그런데 문제가 생겼다. em.getReference(Users, '1') 이런식으로 작성하면, never 값에 string을 할당할 수 없다고 나온다.
never 값이 뜨는 걸 보니, 엔티티의 기본 키에대한 타입 추론이 잘못된 것 같았다.
여러 실험을 통해 알아 낸 것은, getReference 메서드가 PrimaryKey를 데코레이터나 옵션값으로 찾아내는 것이 아닌,
엔티티의 'id' 속성을 찾아 인식하더라.
나는 User의 기본키를 userId 와 같이 작성하고 있어서, 기본키를 찾지못해 never 타입으로 추론하고 있는 것이었다.
이를 위해 모든 엔티티의 기본키를 id 값으로 바꾸고 데이터베이스에 generate로 테이블 속성을 수정하고자 했다.
const orm = await MikroORM.init(testConfig); // MikroORM 설정을 사용하여 초기화
const generator = orm.getSchemaGenerator(); // 스키마 생성기
await generator.updateSchema();
그런데 제약조건때문에 한 번에 다 수정하면 잘 안먹히더라.. 제약조건이나 순서까지 따져서 수정하면 배보다 배꼽이 더 큰 격이라
결국 테이블이 많지 않아서 노가다로 pgAdmin 에서 오리지널 데이터베이스를 일일히 수정했다.
그 후 위의 코드로 테스트 데이터베이스를 생성했다.
이제 모든 준비는 끝났다.
테스트 데이터베이스도 만들어 두었겠다, 테스트 코드만 작성하면 된다.
먼저 테스트를 하기전에 필요한 데이터들을 생성하고, 각 테스트 별로 초기화가 필요한 것들을 작성해 두었다.
describe('Todo Repository', () => {
let todoRepository: TodoRepository;
let em: EntityManager;
let orm: MikroORM;
beforeAll(async () => {
orm = await MikroORM.init({ ...testConfig, driver: PostgreSqlDriver });
em = orm.em;
todoRepository = new TodoRepository(em);
const newUser1 = em.create(Users, {
id: '1',
email: 'oboTestUser1@obo.com',
nickname: 'whiteOBO',
password: '123123',
});
const newUser2 = em.create(Users, {
id: '2',
email: 'oboTestUser2@obo.com',
nickname: 'blackOBO',
password: '123123',
});
await em.persistAndFlush(newUser1);
await em.persistAndFlush(newUser2);
});
beforeEach(async () => {
const newTodos = [];
newTodos.push(
em.create(Todos, {
id: '15',
name: '4월 15일에 추가한 투두 테스트2',
startTime: new Date('2023-04-15 16:00:00'),
endTime: new Date('2023-04-15 18:00:00'),
description: 'beforeEach로 생성된 투두입니다.',
completed: false,
user: em.getReference(Users, '2'),
}),
);
...
newTodos.push(
em.create(Todos, {
id: '45',
name: '7월 21일에 추가한 투두 테스트2',
startTime: new Date('2023-07-21 16:00:00'),
endTime: new Date('2023-07-23 18:00:00'),
description: 'beforeEach로 생성된 투두입니다.',
completed: false,
user: em.getReference(Users, '2'),
}),
);
for (const newTodo of newTodos) {
await em.persistAndFlush(newTodo);
}
});
afterEach(async () => {
await em.nativeDelete(Todos, {});
em.clear();
});
afterAll(async () => {
await em.nativeDelete(Users, {});
await orm.close();
});
그리고 테스트케이스는 getReference 메서드를 활용하여 컴팩트하게 예상 출력값을 작성했다.
test('create', async () => {
const params = {
id: '50',
name: '8월 8일에 추가한 투두 테스트1',
startTime: new Date('2023-08-08 16:00:00'),
endTime: new Date('2023-08-21 18:00:00'),
description: 'test로 생성된 투두입니다.',
completed: false,
userId: '1',
};
const result: TodoCreateOutboundPortOutputDto = await todoRepository.create(
params,
);
expect(result).toEqual({
id: '50',
name: '8월 8일에 추가한 투두 테스트1',
startTime: new Date('2023-08-08 16:00:00'),
endTime: new Date('2023-08-21 18:00:00'),
description: 'test로 생성된 투두입니다.',
completed: false,
user: em.getReference(Users, '1'),
});
});
성공했다!
이제 위와 같이 나머지 메서드들도 테스트하면 되겠다.
테스트코드 완성버전 :
https://github.com/ObO314/ObO-back/blob/main/src/todo/outbound-adapter/todo.repository.spec.ts