Node.js/Nest.js

[Nest.js] Custom Repository Unit Testing (TypeORM)

후뿡이 2023. 6. 10. 18:56

Nest.js에서는 기본적으로 Jest를 이용한 Unit Testing과 e2e Testing을 지원한다.

기본으로 지원하는 기능을 통해 오늘부터 운동 기록 서비스의 TestCode 작성기 시작합니다 !!

 

🐳 첫 코드 - 실패 


Nest.js의 공식 홈페이지와 Jest의 공식 홈페이지를 참고하여 

가장 먼저 운동기록 테이블의 service 레이어를 테스트하는 코드를 작성하였다.

 

import { DataSource } from "typeorm";
import { RecordsRepository } from "./records.repository";
import { RecordsService } from "./records.service";
import { Record } from "./entity/records.entity";

describe('RecordsRepository', () => {
	
    // 추후에 설명하겠지만 아래의 코드는 Nest.js의 특징을 살리지 못하는 코드이다 !
    // createTestModule 생성을 통해 Nest.js의 강점을 살려 Testing 하자 !!
    
    let recordsRepository: RecordsRepository;
    let recordsService:RecordsService;
    let datasource:DataSource;

    beforeEach(async () => {
        recordsRepository = new RecordsRepository(datasource);
        recordsService = new RecordsService(recordsRepository)
    })  
})

먼저 테스팅에 사용할 recordsService 객체와 recordsRepository 객체를 선언해 주고

모든 테스트 전에 recordsService 객체와 recordsRepository 객체를 생성한다.

이때 의존성 주입을 ( ~~ ) 을 통해 해 준다!!

( 추후에 설명하겠지만 이는 Nest.js의 강점을 살리지 못한다. Nest.js의 강점을 살리기 위해서는 createTestingModule을 활용하자 )

 

그런 후에 테스팅할 코드를 작성해 준다.

가장 먼저 한 개의 운동 기록을 가지고 오는 getRecordById를 테스팅해보자!

    describe('getRecordById', () => {
        it('should return found record', async () => {
            const id = 28;

            const record = {
                "id": 28,
                "userId": 1,
                "exercise": 1,
                "setNum": 2,
                "weights": [30,20],
                "reps": [10,20],
                "start_time": "2023-06-01 00:50:59",
                "end_time": "2023-06-01 01:30:59",
                "created_at": "2023-06-10 02:45:46",
                "updated_at": "2023-06-10 02:45:46"
            };
            
            // recordService 에서 getRecordById 메소드를 호출하면 
            // recordRepository의 findOneBy를 호출하므로
            // findOneBy를 spyOn 해주고
            // 그 결과가 record 값이 나오도록 한다.
            const findOneSpy = jest
            .spyOn(recordsRepository, 'findOneBy')
            .mockResolvedValue(record as Record);

            const foundRecord = await recordsService.getRecordById(id);
            expect(foundRecord).toEqual(record)
            expect(findOneSpy).toHaveBeenCalledWith({id})
        })

    })
    
})

 

 

실행결과

 

읭 .. ? 

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

 

RecordRepository 에서 생성자로 datasource.createEntityManager() 가 실행되는데

이 부분에서 오류가 발생하는 것을 알 수 있다.

저 메소드가 정의되지 않았다고 한다.

 

datasource = new DataSource() ; 를 통해 datasource 객체도 생성해 주려고 했으나 이 부분에서 계속 오류가 발생했다.

 

테스팅 영역을 바꿔보자 !

 

🐳 Repository 영역을 Testing 해보자 !


결국 Service 레이어에서 Repository 영역을 호출하므로 Service 영역이 아닌 Repository 영역을 테스팅하자 !

 

import { RecordsRepository } from "./records.repository"
import { Test, TestingModule } from "@nestjs/testing";
import { DataSource } from "typeorm";
import { Record } from "./entity/records.entity";

describe('RecordsRepository', () => {
    let recordsRepository: RecordsRepository;
    
    // Repository 생성 과정에 보면 datasource.createEntityManager();를 통해 Repository를 만든다.
    // 그러므로 repository 생성 과정에서 datasource가 없으면 오류가 발생한다 !!!
    // 그렇기 때문에 datasource를 jest.fn()를 통해 Mocking 함수로 가로채주자 !
    const datasource = {
        createEntityManager:jest.fn()
    };

    beforeEach(async () => {
        const module: TestingModule = await Test.createTestingModule({
            providers: [
                RecordsRepository,
                {
                    provide:DataSource,
                    useValue:datasource
                }
            ]
        }).compile();

        recordsRepository = module.get<RecordsRepository>(RecordsRepository);
    })

 

이번에는 beforeEach 부분을 변경해 주었다.

실제 repository를 생성하는 것이 아니라 Testing용 모듈을 생성해 보자 !

 

이 테스팅 모듈의 provider로는 RecordsRepository와 Datasource가 있는데 이 Datasource를 미리 만들어 둔 datasource로 사용해 제공하자.

    const datasource = {
        createEntityManager:jest.fn()
    };

위의 코드는 repository 생성 시에 실행 되는 datasource.createEntityManager(); 를 Mocking하기 위해 선언한다.

Repository 생성 시에 datasource.createEntityManager()가 실행되면 실제 createEntityManager 대신 실행된다.

그 결과 우리가 원하는 값을 리턴하게 하거나, 함수를 재정의 할 수 있다.

 

이 후에 module을 compile 해주면 testing module인 "module"이 생성된다.

( console.log(module)을 해보니 굉장히 많은 정보들이 담겨 있는 객체인 것을 알 수 있었다. )

( 이해할 수는 없음... 진짜 짱 많음 ... )

 

그리고 생성한 module에서 recordsRepository 인스턴스를 .get<>()을 통해 가지고 오자 !

 

그리고 테스팅을 진행해 주자

    describe('findOneBy', () => {
        it('should return found record', async () => {
            const id = 28;

            const record = {
                "id": 28,
                "userId": 1,
                "exercise": 1,
                "setNum": 2,
                "weights": [30,20],
                "reps": [10,20],
                "start_time": "2023-06-01 00:50:59",
                "end_time": "2023-06-01 01:30:59",
                "created_at": "2023-06-10 02:45:46",
                "updated_at": "2023-06-10 02:45:46"
            };
            
            // repository의 method findOneBy가 실행되면
            // 비동기결과값인 record를 return 한다.
            const findOneSpy = jest
            .spyOn(recordsRepository, 'findOneBy')
            .mockResolvedValue(record as Record);
			
            // 실제 메소드를 수행하고 그 결과를 foundRecord에 저장한다.
            const foundRecord = await recordsRepository.findOneBy({id});
            // 실제 값인 foundRecord와 우리가 예상한 결과 값인 record가 일치하는지 확인한다.
            expect(foundRecord).toEqual(record)
            // 그리고 우리가 입력한 id에 의해 호출 됐는지 확인한다.
            expect(findOneSpy).toHaveBeenCalledWith({id})
        })

    })
    
})

 

실행 결과 !!

 

테스트 성공 !!!!

 

 

🐳 회고


repository 테스트 하나를 위해 정말 많은 시간을 소요했다 ...

 

먼저 테스트해야 할 부분을 잘 파악하는 것이 중요한 것 같고.

Jest의 동작 구조에 대해 더 이해할 필요가 있는 것 같다.

 

처음 테스트코드를 작성해 보았는데

기능 하나가 완성될 때마다 즉시 테스트 코드를 작성하는 습관을 길러야겠다.

 

그리고 이번에 테스트 코드를 작성해 보니

테스터블 한 코드를 작성하는 게 정말 쉬운 일이 아니구나 싶었다... 

 

Nest.js를 더 이해하는 그날까지 화이팅 !