NestJS 테스트 환경 구성

HyoKwangRyu·2023년 3월 5일
1

서론

database

테스트 코드 작성시, 외부 API 서비스나 레포지토리 메소드를 모킹해서 사용하곤 한다.
orm 기능을 매번 모킹해서 테스트 코드를 짜는것은 로직이 커질 수록 힘들어 진다.
sqlite 타입의 in memory db 를 이용하여 모킹을 대신할 방법이 있지만, postgreql을 사용할 경우, 위 같은 방법을 사용할 수 없었다.
이때 pg-mem 패키지를 이용할 수 있다.

ConfigService

테스트 코드에 환경변수 관련 로직이 포함 될때, 테스트 모듈에 테스트 환경의 env들을 상수로 직접 주입하여 사용 하는 예제가 많더라.
이 방법은 테스트 코드 작성시 config 관련 로직이 있는지 확인해, 각 env 값을 직접 주입해야하는데 굉장히 불편하다. test env 도 다른 prd, stg, local 등의 환경 처럼 한 파일에 편하게 관리하고 싶었다.

const module: TestingModule = await Test.createTestingModule({
      providers: [
        {
          provide: ConfigService,
          useValue: {
            get: jest.fn((key: string) => {
              if (key === 'ENV_FOO') {
                return 111;
              }
              return null;
            }),
          },
        },
      ],
    }).compile();

이런 모킹 코드가 싫었단 이야기.

본론

pg-mem setup, .test.env

export const setupTestConnection = async () => {
  const db = newDb({
    autoCreateForeignKeyIndices: true,
  });
  db.public.registerFunction({
    name: 'current_database',
    implementation: () => 'test',
  });

  db.public.registerFunction({
    name: 'version',
    implementation: () => 'test',
  });

  db.registerExtension('uuid-ossp', (schema) => {
    schema.registerFunction({
      name: 'uuid_generate_v4',
      returns: DataType.uuid,
      implementation: randomUUID,
      impure: true,
    });
  });

  db.public.interceptQueries((sql) => {
    const newSql = sql.replace(/\bnumeric\s*\(\s*\d+\s*,\s*\d+\s*\)/g, 'float');
    if (sql !== newSql) {
      // eslint-disable-next-line @typescript-eslint/no-unsafe-return
      return db.public.many(newSql);
    }

    return null;
  });

  db.public.interceptQueries((queryText) => {
    if (queryText.search(/(pg_views|pg_matviews|pg_tables|pg_enum)/g) > -1) {
      return [];
    }
    return null;
  });

  const dataSource = (await db.adapters.createTypeormDataSource({
    type: 'postgres',
    entities: [`${__dirname}/../**/*.entity{.ts,.js}`],
    migrationsRun: false,
    host: 'localhost',
    port: 5432,
    username: 'test',
    password: 'test',
    database: 'test',
    autoLoadEntities: true,
    synchronize: true,
    dropSchema: true,
  })) as DataSource;

  await dataSource.initialize();

  return dataSource;
};
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: `${__dirname}/env/.${process.env.NODE_ENV === 'test' ? 'test' : process.env.AWS_ENV ?? 'local'}.env`,
    }),
    process.env.NODE_ENV === 'test'
      ? TypeOrmModule.forRootAsync({
          useFactory: () => ({
            type: 'postgres',
            migrationsRun: false,
          }),
          dataSourceFactory: async () => {
            return setupTestConnection();
          },
        })
      : TypeOrmModule.forRootAsync({ useClass: DatabaseService }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

AppModule.ts

기존 typeorm 세팅은 DatabaseService 를 통해 관리하고 있었다.
노드 환경에 따라, test환경의 경우 pg-mem 을통해 인메모리 DB를 사용하도록 한다.

ConfigModule도 함께 보자.
dotenv를 통해 환경변수를 관리 하고 있었다.
.test.env 파일에 테스트 환경에서 쓰일 환경 변수들을 정리하자.

테스트 코드

describe('MyService', () => {
  let service: MyService;
  let postsRepository: Repository<Post>;

  beforeAll(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    service = module.get<PostsService>(PostsService);
    postsRepository = module.get<Repository<Post>>(getRepositoryToken(Post));
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });
  
  describe('findAll', () => {
    beforeAll(async () => {
      const mockPosts = [
        new Post('title1', 'content1'),
        new Post('title2', 'content2'),
      ];
      await postsRepository.save(mockPosts);
    });

    afterAll(async () => {
      await postsRepository.clear();
    });
})

테스트 모듈 생성시, 테스트에 필요한 모듈들을 하나하나 주입할 필요가 없다.
AppModule에서 이미 환경별 DB, ConfigService 세팅을 해놨기 때문에, 테스트 모듈 생성시에 AppModule 만 주입해주면 된다.

테스트 케이스에 필요한 데이터를 미리 저장해 주고, 테스트 완료 후 db를 비워 준다.
테스트간 간섭이 일어난다면, beforeEach, afterEach로 대신 사용해도 된다.

결론

테스트코드 작성의 피로는 반복되는 셋업코드와 모킹 코드 때문이라고 생각한다.
환경에 따라 DB, ENV를 설정해 둠으로써, 테스트 코드 작성시 모킹 등의 셋업코드가 현저하게 줄어들게 된다.
이제 편하게 테스트 로직에만 집중할 수 있게 될 것이다.

profile
Backend Developer

0개의 댓글