TIL | WIL

3/11 토 (모듈 하나 e2e테스트 성공, Date타입의 toString 종류 총정리) TIL, TIT

깊은바다거북 2023. 4. 6. 20:03

(최종 프로젝트 진행중)


※ 이하는 스스로 공부하며 적어둔 노트이며 불확실한 내용이 있을 수 있습니다. 학습용으로 적합하지 않음을 유념해주세요. ※

[Nest.js][Jest] 에러 TypeError: this.repository.create is not a function

발생 상황: messages.service.spec.ts를 실행하는데 messages.service.ts 소스코드에 정의한 this.repository.create()이라는 TypeORM Repository의 기본 메소드가 인식되지 않는다고 함.

에러 메세지 전문:

FAIL  src/messages/messages.service.spec.ts (7.845 s)
  MessagesService
    getMessageById
      √ should be defined (14 ms)
      × should create a new message and return that (6 ms)

  ● MessagesService › getMessageById › should create a new message and return that

    TypeError: this.repository.create is not a function

      44 |
      45 |   async create(createMessageDto: CreateMessageDto) {
    > 46 |     const newMessage = this.repository.create(createMessageDto);
         |                                        ^
      47 |
      48 |     return this.repository.save(newMessage);
      49 |   }

      at MessagesService.create (messages/messages.service.ts:46:40)
      at Object.<anonymous> (messages/messages.service.spec.ts:94:28)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        8.454 s
Ran all test suites matching /messages.service/i.

시도:

시도1: 일단 @InjectRepository를 서비스 의 생성자에 붙이면 Repository<Message>의 모든 메소드를 이용할 수 있게 된다고 ㅎ나다. 이 때 짝으로 설정해줘야 하는 것은 TypeOrmModule.forFeature에 Entity를 넣어서 module에 import해줘야 한다는 것. 이런 것처럼:

@Injectable()
export class ProductService {
 private logger = new Logger('ProductService');
 constructor(
  @InjectRepository(Product)
  private productRepository: Repository<Product>
 ) { }

 async getAllAsync(): Promise<Product[]> {
  return await this.productRepository.find();
 }

 async getCountAsync(): Promise<number> {
  return await this.productRepository.count();
 }
}

⇒ 나는 이미 이렇게 하고 있었음.

시도2: service.spec.ts에 mock method를 제대로 안 넣어줘서 이 에러가 났다는 사람도 있었다. 그래서 인식되지 않는다는 해당 메소드를 제대로 mocking해주니 해결됐다고.

// 답변자의 예시:
const mockUserRepository = {
  register: jest.fn().mockResolvedValue('sampleResolvedValue')
};

// 내 코드엔 원래 제대로 되어 있었음:
const mockMessagesRepository = {
  create: jest.fn().mockImplementation((dto) => dto),
  save: jest
    .fn()
    .mockImplementation((message) => Promise.resolve({ id: 1, ...message })),
};

⇒ 나는 이미 잘 mocking 해준 상태였음.

시도3: 헐 보다보니 beforeEach에서 mockTestingModule을 초기화할 때 정작 mockMEssagesRepository를 안 넣어준 상태였다..! ⇒ 해결됨

원인: 테스트 코드에서 mock repository를 제대로 주입해주지 않아서 생긴 에러였다. mockMessageRepository를 다 만들어 뒀어도 정작 testingModule을 초기화할 때 주입할 자리에 그냥 빈 객체 {}를 지정해 둔 상태였던 것이다. 그러니 create이라는 메소드가 정의되지 않은 상태가 맞았지...

해결: createTestingModule()의 provider 파트에 repository를 커스텀 mockMessagesRepository로 제대로 지정해주니 해결됨.

// (원래 만들어 둔) mock repository:
const mockMessagesRepository = {
  create: jest.fn().mockImplementation((dto) => dto),
  save: jest
    .fn()
    .mockImplementation((message) => Promise.resolve({ id: 1, ...message })),
};

// 이렇던 것을: 
beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        MessagesService,
        {
          provide: getRepositoryToken(Message),
          useValue: {},
        },
      ],
    }).compile();
})

// 이렇게 바꿔줌:
 beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        MessagesService,
        {
          provide: getRepositoryToken(Message),
          useValue: mockMessagesRepository,
        },
      ],
    }).compile();
})

알게 된 점: 내가 제대로 된 mock repository를 제공하지 않아서 ‘그런 repository.문제메소드 는 찾을 수가 없다’고 에러가 뜨는 상황이라도, 에러가 가리키는 곳은 실제 service 소스 코드this.repository.문제메소드였다. 앞으로는 단위 테스트 때 이와 비슷하게 ‘mock이 주입되어야 하는 provider의 실제 소스’를 가리키는 것 같은 에러가 나도 “이 provider는 mocking해주는 생황이니 문제는 테스트 코드에 있다” 하고 알아서 판단해 수정할 부분을 찾을 수 있을 것 같다.

(참고: https://stackoverflow.com/questions/60777204/typeerror-repository-method-is-not-a-function-nestjs-typeorm)

[Nest.js][Jest] 모듈 별 e2e 테스트

  1. test/messages.e2e-spec.ts 파일 생성
  1. app.e2e-spec.ts 파일 복사 붙여넣기
  1. import 부분을 appModule 대신 messagesModule로 대체
  1. mockMessagesRepository를 (messages.service.spec.ts에서와 똑같이) override해준다.
  1. validationPipe app에 지정해주기.
  1. 이제 'GET /messages’처럼 테스트를 원하는 api 경로로 테스트하면 된다.

supertest Docs 참고

npm: supertest
SuperAgent driven library for testing HTTP servers. Latest version: 6.3.3, last published: 3 months ago. Start using supertest in your project by running `npm i supertest`. There are 1138 other projects in the npm registry using supertest.
https://www.npmjs.com/package/supertest

supertest Test.set 예시 모음집

supertest.Test.set JavaScript and Node.js code examples | Tabnine
.get('/cubejs-api/v1/load?query={}') .set('Authorization', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcSemACt8x4iTMCda8Yhe3iZaWbvV5XKSTbuAn0M')
https://www.tabnine.com/code/javascript/functions/supertest/Test/set

e2e 내 각 it() 마다 request(app.getHttpServer()) 를 리턴해줘야 실제로 테스트가 된다.

return을 안 넣어도 테스트가 되길래 빼버렸더니, 뭘 넣어도 그냥 계속 통과되기만 한다는 것을 알게 되었다.

return 을 빼먹었을 때:

// test/requests.e2e-spec.ts
it('GET /requests', () => {
  request(app.getHttpServer())
    .get('/requests')
    .expect(400) // 200이어야 함. 
    .expect(getRequestsSample);
});

=> 통과!?

return을 넣어줬을 때: ‘200이어야 하는데 400이 들어왔다’고 제대로 검증할 수 있음.

it('GET /requests', () => {
  return request(app.getHttpServer())
    .get('/requests')
    .expect(400)  
    .expect(getRequestsSample);
});

=> expected 400 "Bad Request", got 200 "OK"

실제로 ‘잘못된 타입’을 body로 줬을 때 테스트가 실패하게 하고 싶다면

main.ts에 ValidationPipe를 써준 것처럼

// src/main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

테스트하려는 e2e-spec.ts 내의 mock 모듈에도 똑같이 지정해줘야 들어오는 인자값들의 타입을 실제로 검증할 수 있게 된다:

// test.requests.e2e-spec.ts
beforeAll(async () => {
  const module = await Test.createTestingModule({
    imports: [RequestsModule],
  })
    .overrideProvider(getRepositoryToken(Request))
    .useValue(mockRequestsRepository)
    .compile();

  app = module.createNestApplication();
  app.useGlobalPipes(new ValidationPipe()); // <- new!

  await app.init();
});

app.useGlobalPipes(new ValidationPipe());을 지정해주지 않았을 때:

// 
it('fails when "detail" is not a string', () => {
  return request(app.getHttpServer())
    .post('/requests')
    .send({
      reserved_time: new Date('2023-05-05'),
      detail: 123123,
    })
    .expect('Content-Type', /json/)
    .expect(201); // 400이어야 함. 
});

=> 통과!?

app.useGlobalPipes(new ValidationPipe());을 지정해준 후: ‘201을 예상했지만 400이 발생했다’고 제대로 검증할 수 있게 됨:

=> 
expected 201 "Created", got 400 "Bad Request"

(참고 - 거의 이것 보고 작성하고 에러 잡아낸 유튜브: )

NestJS Testing Tutorial | Unit and Integration Testing
In this video we go over the fundamentals of writing unit, integration, e2e tests for a NestJS API application.
https://www.youtube.com/watch?v=dXOfOgFFKuY

[Nest.js] 에러 Nest can't resolve dependencies of the RequestRepository (?). Please make sure that the argument DataSource at index [0] is available in the TypeOrmModule context.

발생 상황: e2e 테스트 처음 실행시 발생.

// test/requests.e2e-spec.ts
import request from 'supertest';
import { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { RequestsService } from '../src/requests/requests.service';
import { RequestsModule } from '../src/requests/requests.module';

describe('Requests (e2e)', () => {
  let app: INestApplication;
  
  let requestsService = {
    getRequests: () => getRequestsResult,
    getRequestById: (id) => getRequestByIdResult, // ? params로 받는 값을 어떻게 넣어주지.
    createRequest: () => createRequestResult,
    updateRequestById: () => updateRequestByIdResult,
    deleteRequestById: () => deleteRequestByIdResult,
  };

  beforeAll(async () => {
    const module = await Test.createTestingModule({
      imports: [RequestsModule],
    })
      .overrideProvider(RequestsService)
      .useValue(requestsService)
      .compile();

    app = module.createNestApplication();
    await app.init();
  });

  it('/GET requests', () => {
    return request(app.getHttpServer())
      .get('/requests')
      .expect(200)
      .expect({ data: requestsService.getRequests() });
  });

  afterAll(async () => {
    await app.close();
  });
});

에러 메세지 전문:

Watch Usage: Press w to show more.
 FAIL  test/requests.e2e-spec.ts (5.709 s)
  ● Requests (e2e) › /GET requests

    Nest can't resolve dependencies of the RequestRepository (?). Please make sure that the argument DataSource at index [0] is available in the TypeOrmModule context.

    Potential solutions:
    - Is TypeOrmModule a valid NestJS module?
    - If DataSource is a provider, is it part of the current TypeOrmModule?
    - If DataSource is exported from a separate @Module, is that module imported within TypeOrmModule?
      @Module({
        imports: [ /* the Module containing DataSource */ ]
      })

      47 |
      48 |   beforeAll(async () => {
    > 49 |     const module = await Test.createTestingModule({
         |                    ^
      50 |       imports: [RequestsModule],
      51 |     })
      52 |       .overrideProvider(RequestsService)

      at TestingInjector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:247:19)
      at TestingInjector.resolveComponentInstance (../node_modules/@nestjs/core/injector/injector.js:200:33)
      at TestingInjector.resolveComponentInstance (../node_modules/@nestjs/testing/testing-injector.js:19:45)
      at resolveParam (../node_modules/@nestjs/core/injector/injector.js:120:38)
          at async Promise.all (index 0)
      at TestingInjector.resolveConstructorParams (../node_modules/@nestjs/core/injector/injector.js:135:27)
      at TestingInjector.loadInstance (../node_modules/@nestjs/core/injector/injector.js:61:13)
      at TestingInjector.loadProvider (../node_modules/@nestjs/core/injector/injector.js:88:9)
      at ../node_modules/@nestjs/core/injector/instance-loader.js:49:13
          at async Promise.all (index 3)
      at TestingInstanceLoader.createInstancesOfProviders (../node_modules/@nestjs/core/injector/instance-loader.js:48:9)
      at ../node_modules/@nestjs/core/injector/instance-loader.js:33:13
          at async Promise.all (index 3)
      at TestingInstanceLoader.createInstances (../node_modules/@nestjs/core/injector/instance-loader.js:32:9)
      at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/core/injector/instance-loader.js:21:9)
      at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-instance-loader.js:9:9)
      at TestingModuleBuilder.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-module.builder.js:97:9)
      at TestingModuleBuilder.compile (../node_modules/@nestjs/testing/testing-module.builder.js:63:9)
      at Object.<anonymous> (requests.e2e-spec.ts:49:20)


  ● Test suite failed to run

    TypeError: Cannot read properties of undefined (reading 'close')

      94 |
      95 |   afterAll(async () => {
    > 96 |     await app.close();
         |               ^
      97 |   });
      98 | });
      99 |

      at Object.<anonymous> (requests.e2e-spec.ts:96:15)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        5.762 s
Ran all test suites matching /requests.e2e/i.

시도:

원인:

  • requests.module.ts에서 TypeOrmModule을 주입해주는데 왜 이런 오류가 발생하는 걸까
// src/requests/requests.module.ts
@Module({
  imports: [
    TypeOrmModule.forFeature([Request]),
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useClass: JwtConfigService,
      inject: [ConfigService],
    }),
  ],
  controllers: [RequestsController],
  providers: [RequestsService],
})
export class RequestsModule {}

해결: beforeEach 파트를 다음과 같이 수정 시 해결됨

const mockRequestsRepository = {};

beforeAll(async () => {
  const module = await Test.createTestingModule({
    imports: [RequestsModule],
  })
    .overrideProvider(getRepositoryToken(Request))
    .useValue(mockRequestsRepository)
    .compile();

  app = module.createNestApplication();
  await app.init();
});

커스텀 repository를 만들었을 때

이렇게 하는 게 맞을까

// messages.module.ts
@Module({
  imports: [
    TypeOrmModule.forFeature([Message]),
  ],
  controllers: [MessagesController],
  providers: [MessagesService, MessagesRepository],
})
export class MessagesModule {}

이렇게 하는 게 맞을까

// messages.module.ts
@Module({
  imports: [
    TypeOrmModule.forFeature([MessagesRepository]),
  ],
  controllers: [MessagesController],
  providers: [MessagesService, ],
})
export class MessagesModule {}

일단 service.ts 에는 @InjectRepository를 지우고 이렇게 써주는 게 맞다:

// 기본 Repository<Message>가 아니라 커스텀 MessagesRepository를 넣어줄 때: 
@Injectable()
export class RequestsService {
  constructor(
    @InjectRepository(Message) private repository: Repository<Message>
		private readonly repository: MessagesRepository
  ) {}
	...
}

node-mocks-http 모듈 도입

node-mocks-http: Express 애플리케이션 라우팅 함수를 테스트하기 위한 Mock 객체를 제공해주는 모듈.

create 계열 메소드를 테스트 할 때 인수 req.user를 구현하려고 도입함.

그리고 나중에 Authorization header도 사용할 수 있을까 해서…

mockRequest = httpMocks.createRequest();
mockResponse = httpMocks.createResponse();

const body = { name: 'John', age: 39 }
mockRequest.body = body;

(참고: https://libertegrace.tistory.com/entry/TDD-node-mocks-http-모듈)

(https://www.youtube.com/watch?v=43iQzPJvZDw)

⇒ 그러나 테스트 때 과한 3rd-party 모듈을 도입하는 것을 지양해야 한다고 하기에 도입하지 않기로 함.

Date의 toString() 종류 총정리:

reserved_time = new Date('2023-05-05’)

⇒이것을 "2023-05-05", 처럼 출력하고 싶다.

  • "디폴트(reserved_time 그대로)": 2023-05-05T00:00:00.000Z,
  • “toLocaleString": "2023. 5. 5. 오전 9:00:00",
  • "toDateString": "Fri May 05 2023",
  • "toISOString": "2023-05-05T00:00:00.000Z",
  • "toJSON": "2023-05-05T00:00:00.000Z",
  • "toLocaleDateString": "2023. 5. 5.",
  • "toLocaleTimeString": "오전 9:00:00",
  • "toString": "Fri May 05 2023 09:00:00 GMT+0900 (대한민국 표준시)",
  • "toTimeString": "09:00:00 GMT+0900 (대한민국 표준시)",

일단 디폴트는 toJSON인 것 같다.

reserved_time.toJSON 혹은 reserved_time에서 .split(’T’)[0]를 고르면 되겠다. 아 잠깐만 그러면 다시 타입이 Date이 아니게 되는데…


Uploaded by N2T