목록으로
Testing

Jest - TDD를 Jest로 직접 해보자 (6)

Dev Park
3/4/2023
24 views

서론

저번 게시글에 이어서 날씨 정보를 받아오는 추가적인 기능을 만들어보자.

1단계 : 기능의 요구사항 및 스팩

저번 게시글에서 만든 기능은 지역코드, 시간, 날짜 를 보내면 그에 맞는 데이터를 반환하는 것이었다.
오늘은 시간과 날짜만 받아서 그것에 해당하는 전체 지역 데이터를 보내는 코드를 테스트를 이용해 만들어보자.

이 기능의 요구사항 및 스팩은 아래와 같다.

  1. 클라이언트로부터 시간과 날짜 데이터를 요청받으면 해당 시간과 날짜에 해당하는 전체지역의 데이터를 반환하는 'getAllWeatherInfo' 를 구현한다.

  2. 클라이언트로부터 받은 데이터는 {date:string, time:number} 의 형태이다.

  3. 반환하는 데이터형태는 [temperature:number, precipitation:number, precipitationPattern:number, windSpeed:number, windDirection:number, humidity:number] 와 같아야한다.

2단계 : 테스트 코드 작성

기능의 요구사항 및 스팩을 만족하는 테스트 코드를 작성하려면 아래와 같은 점을 고려해야한다.

  1. 클라이언트로부터 받을 데이터를 예제 데이터로 만든다.

  2. 'getAllWeatherInfo'가 반환하는 값을 예제 데이터로 만든다.

클라이언트로 부터 받은 데이터는 Dto를 이용해 옮길 것임으로 Dto를 추가로 만들어준다.

typescript
1import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; 2 3export class SelectWeatherDto { 4 @IsNotEmpty() 5 @IsNumber() 6 area: number; 7 8 @IsNotEmpty() 9 @IsNumber() 10 time: number; 11 12 @IsNotEmpty() 13 @IsString() 14 date: string; 15} 16 17export class SelectAllWeatherDto { 18 @IsNotEmpty() 19 @IsNumber() 20 time: number; 21 22 @IsNotEmpty() 23 @IsString() 24 date: string; 25}

저번 게시글에서 사용했던 DTO도 추가로 작성해뒀으니 참고바란다.

우선 해당 DTO를 모듈로 Import 한다.

typescript
1import { SelectWeatherDto, SelectAllWeatherDto } from './dto/weather.dto';

각자 DTO위치에 맞게 경로를 수정하자.
그리고 테스트 코드를 작성해보자.
모듈에 사용이 예상되는 weatherRepository의 메소드인 find를 추가해준다.

typescript
1beforeEach(async () => { 2 const module: TestingModule = await Test.createTestingModule({ 3 providers: [ 4 WeatherService, 5 { 6 provide: getRepositoryToken(localEntity), 7 useValue: { 8 find: jest.fn(), 9 createQueryBuilder: jest.fn(), 10 }, 11 }, 12 { 13 provide: getRepositoryToken(weatherEntity), 14 useValue: { 15 findOne: jest.fn(), 16 find: jest.fn(), 17 }, 18 }, 19 ], 20 }).compile(); 21 22 service = module.get<WeatherService>(WeatherService); 23 localRepository = module.get<Repository<localEntity>>( 24 getRepositoryToken(localEntity), 25 ); 26 weatherRepository = module.get<Repository<weatherEntity>>( 27 getRepositoryToken(weatherEntity), 28 ); 29 });

정상적인 작동을 테스트하는 테스트코드를 작성해보겠다.

typescript
1 describe('getWeatherAllInfo', () => { 2 it('should return weather information', async () => { 3 const clientData: SelectAllWeatherDto = { 4 date: '20230302', 5 time: 15, 6 }; 7 const returnWeatherInfo: weatherEntity[] = [ 8 { 9 id: 1, 10 area: 1, 11 time: clientData.time, 12 date: clientData.date, 13 temperature: 12, 14 precipitation: 0, 15 precipitationPattern: 0, 16 windSpeed: 0.2, 17 windDirection: 340, 18 humidity: 40, 19 }, 20 { 21 id: 2, 22 area: 2, 23 time: clientData.time, 24 date: clientData.date, 25 temperature: 15, 26 precipitation: 0, 27 precipitationPattern: 0, 28 windSpeed: 1.4, 29 windDirection: 150, 30 humidity: 35, 31 }, 32 { 33 id: 3, 34 area: 3, 35 time: clientData.time, 36 date: clientData.date, 37 temperature: 7, 38 precipitation: 50, 39 precipitationPattern: 3, 40 windSpeed: 2.3, 41 windDirection: 280, 42 humidity: 60, 43 } 44 ] 45 jest 46 .spyOn(weatherRepository, 'find') 47 .mockResolvedValue(returnWeatherInfo); 48 49 const weatherInfo = await service.getAllWeatherInfo(clientData); 50 51 expect(weatherInfo).toEqual(returnWeatherInfo); 52 expect(weatherRepository.find).toHaveBeenCalled(); 53 }); 54 });

우선 'selectAllWeatherDto' 형태의 Dto를 클라이언트로 요청받은 예시데이터를 작성한다.
그리고 반환할 예시 데이터를 weatherEntity[] 의 객체 배열 형태로 작성한다.
실제코드에서 사용이 예상되는 find 메소드를 mock 하고 'returnWeatherInfo' 가 반환되도록 설정했다.
그 후 'getAllWeatherInfo' 를 호출해 'clientData' 를 매개변수로 받고 반환되는 값이 'returnWeatherInfo' 와 같은지를 테스트한다.
또한 weatherRepository의 find 메소드가 제대로 호출되었는지 테스트한다.

이제 데이터베이스 오류를 테스트하는 코드를 작성해보자.

typescript
1 it('should throw error when database error', async () => { 2 const clientData: SelectAllWeatherDto = { 3 date: '20230302', 4 time: 15, 5 }; 6 7 jest.spyOn(weatherRepository, 'find').mockRejectedValue(new Error('Database error')), 8 9 await expect(service.getAllWeatherInfo(clientData)).rejects.toThrow( 10 new Error('Database error'), 11 ); 12 });

똑같이 clientData 를 매개변수로 받는다.
그리고 find 메소드가 호출되었을때 'Database error' 라는 예외가 발생하도록한다.
'getAllWeatherInfo' 메소드를 호출하여 예외가 제대로 발생하고 err 코드를 제대로 반환했는지 테스트한다.

3단계 : 테스트 실패 확인

당연히 실제 코드가 없으니 테스트는 실패한다.

4단계 : 코드 작성

이제 테스트 코드를 바탕으로 실제 코드를 작성해보자.

typescript
1 async getAllWeatherInfo( 2 selectWeatherDto: SelectAllWeatherDto, 3 ): Promise<weatherEntity[]> { 4 try { 5 return this.weatherRepository.find(selectWeatherDto); 6 } catch (err) { 7 throw err; 8 } 9 }

typeOrm의 find 메소드를 이용해 클라이언트로 부터 요청받은 데이터에 맞는 데이터를 찾아 반환하는 코드이다.
예외가 발생하면 에러코드를 throw 한다.
weatherEntity[] 형태의 객체 배열을 반환하고 매개변수로는 SelectAllWeatherDto 의 Dto를 받는다.

5단계 : 테스트 통과 확인

imageimage

테스트를 통과했다.

6단계 : 리팩토링

리팩토링할 부분이 발견되지 않아 6단계는 패스한다.

7단계 : 2~6단계 반복

기능의 요구사항 및 스팩을 모두 만족했기때문에 7단계는 패스한다.