작성자 : 김준혁
사내세미나로 진행한 영상 중 일부입니다. 해당 내용은 실사용 코드가 어느정도 섞여있기 때문에 발표중 일부만 발췌하였습니다.
목차
시작하기 전에,,,
사례 공유를 총 N개로 나누어서 진행할 예정입니다.
하나를 전달하더라도, 확실히 전달하고 싶습니다.
•
1부 : 외부 라이브러리 테스트 (self constuct mock)
•
2부 : D.CODE Architecture TDD (jest mock function) : Data & Domain
•
3부 : D.CODE Architecture TDD : VM (
현재 보시는 문서 위치)
바쁜이들을 위한 2부 요약 : D.CODE Architecture TDD (jest mock function) : Data & Domain
2부의 핵심 :
•
jest에서 제공하는 mock을 이용하여 테스트를 진행한다.
•
D.CODE Architecture에서 Data와 Domain 영역을 테스트한다.
2부에서는 mock을 직접 만들지 않고 테스트 했던 방법.
1.
jest 에서 제공하는 mock 모듈화 기능을 이용한다.
jest.mock('../../../APPLICATION/dependencies/IO/AxiosNetworkingForSanta');
TypeScript
복사
2부 코드 일부
2.
화살표함수의 경우 위 방법으로 해결이 안되므로 강제적으로 returnValue 혹은 resolveValue를 이용한다.
describe('BrandRepository', () => {
test('브랜드 목록 조회 요청시 Get /api/products/brands 를 호출해야한다. ', async () => {
const networking = new (AxiosNetworkingForSanta as jest.Mock)();
networking.execute.mockReturnValue(Promise.resolve(new NCNetworkResponse(200, '', undefined)));
const context = NCApplicationContext.createContext(new AdminApplicationConfiguration());
context.serviceLocator.registSingle(NCNetworkingForSantaClassName, () => networking);
// ...
TypeScript
복사
2부 코드 일부
2부에서는 위와 같이 jest.mock을 이용하여 테스트를 진행하면서 또한, D.CODE Architecture에서 Data와 Domain 영역을 테스트하였습니다.
D.CODE Client Architecture
현재 프론트에서 사용 중인 디코드 클라이언트 아키텍쳐는 클린 아키텍쳐를 기반으로 두고 있습니다.
디코드 아키텍쳐를 이용하여 특정 티켓의 작업을 해소 해야만 하는 경우가 있습니다.
이번에는 Architecture에서 View와 제일 밀접한 관계가 있는 ViewModel영역을 테스트하겠습니다.
사례
•
어드민 3.0으로 산타 통합 (브랜드, 카테고리 맵핑)
사실 2부에서 했던 사례를 이용하여 VM 관련 테스트 코드를 만들 수 있었으나, 해당 사례가 기능이 더 많고 보여주기에 더 좋다고 선택하였습니다.
해당 테스트 코드도 VM뿐만 아니라, Domain, Data도 이미 테스트 코드가 다 작성되어 있습니다.
인수조건 및 테스트 내용
인수조건이 너무 많습니다.
Page 작업에 간단하더라도 작은 기능 하나하나가 모든 인수조건에 해당됩니다.
또한 VM에서는 View에서 일어나는 이벤트를 처리하는 곳이라서 테스트의 양이 늘어날 수 밖에 없는 상황입니다.
브랜드 맵핑 인수조건
•
목록 데이터가 보여지는가?
◦
브랜드 목록
◦
매핑된 브랜드 목록
•
기능이 동작하는가?
◦
매핑 브랜드 셀렉박스 변경 : 매핑 브랜드영역에서 셀렉박스를 클릭했을 때 불리는 이벤트를 테스트합니다. 여기서 브랜드를 선택했을 경우, VM에 정의된 상태가 변경되는지 테스트합니다.
◦
코멘트 : 코멘트영역에서 코멘트를 입력했을 때 불리는 이벤트를 테스트합니다. 여기서 코멘트를 입력했을 경우, VM에 정의된 상태가 변경되는지 테스트합니다.
테스트 코드
테스트는 jest 를 이용합니다.
jest는 React 프로젝트를 생성할 때 CRA 로 생성했다면 기본적으로 사용이 가능하도록 내부에 라이브러리가 삽입되어있습니다.
준비 : 보일러플레이트 구현
먼저 테스트 코드를 작성하기 전에 테스트를 진행하기 위한 준비 코드를 작성해봅시다.
describe('BrandMapVM', () => {
test('브랜드관리 > 부티크 관리 > 부티크 맵핑 > 브랜드탭 > 매핑 브랜드 리스트가 보여야한다.', async () => {});
test('브랜드관리 > 부티크 관리 > 부티크 맵핑 > 브랜드탭 > 매핑 브랜드 리스트 > 매핑브랜드 컬럼 > 브랜드 목록이 보여야한다.', async () => {});
test('브랜드관리 > 부티크 관리 > 부티크 맵핑 > 브랜드탭 > 매핑 브랜드 리스트 > 매핑브랜드 칼럼 > 브랜드를 선택할 경우 VM데이터가 변경되어야한다.', async () => {});
test('브랜드관리 > 부티크 관리 > 부티크 맵핑 > 브랜드탭 > 매핑 브랜드 리스트 > 코멘트 컬럼 > 코멘트 입력이 되어야한다.', async () => {});
});
TypeScript
복사
bm-init , bm-v-test-1~4
먼저, 저희가 테스트해야할 페이지에서 사용하는 기능들은 전부 BoutiqueMapVM 을 이용합니다. 따라서 실제 코드를 확인해봅시다.
그런데 BoutiqueMapVM은 현재 BoutiqueService를 의존하고 있습니다. 따라서 실제 BoutiqueService 사용하는게 아닌 mock 화된 BoutiqueService를 사용해봅시다.
mock화 코드를 작성하기 위해 서비스 로케이터에 관한 보일러플레이트를 작성해봅시다.
test('브랜드관리 > 부티크 관리 > 부티크 맵핑 > 브랜드탭 > 매핑 브랜드 리스트가 보여야한다.', async () => {
const context = NCApplicationContext.createContext(new AdminApplicationConfiguration());
});
TypeScript
복사
bm-servicelocator-init
그리고, BoutiqueService 를 mock화 하여 등록하고, BoutiqueMapVM 도 등록해봅시다.
test('브랜드관리 > 부티크 관리 > 부티크 맵핑 > 브랜드탭 > 매핑 브랜드 리스트가 보여야한다.', async () => {
const mockService = new (BoutiqueServiceImpl as jest.Mock)();
const context = NCApplicationContext.createContext(new AdminApplicationConfiguration());
context.serviceLocator.registSingle(BoutiqueServiceClassName, () => mockService);
context.serviceLocator.registSingle(
BoutiqueMapVMClassName,
() => new BoutiqueMapVMImpl(mockService)
);
});
TypeScript
복사
bm-v-regist-servie-1~2
그런데 생각해보니, 위 코드를 모든 테스트 코드마다 작성하는 것은 중복이 너무 심할 것 같네요.
그래서, jest에서 제공하는 beforeAll이라는 기능을 사용하려 합니다.
beforeAll(() => {
const mockService = new (BoutiqueServiceImpl as jest.Mock)();
const context = NCApplicationContext.createContext(new AdminApplicationConfiguration());
context.serviceLocator.registSingle(BoutiqueServiceClassName, () => mockService);
context.serviceLocator.registSingle(
BoutiqueMapVMClassName,
() => new BoutiqueMapVMImpl(mockService)
);
});
TypeScript
복사
bm-v-beforeAll
최근 CRA를 사용하여 리액트 프로젝트를 생성했다면, 해당 부분에서 에러가 있을 수 있습니다.
팀장님과 논의하에 테스트마다 초기화되지 않기를 원하였기 때문에 jest config에서 false로 변경하고 하나의 전체 테스트(describe) 가 끝날 때 전체 mock을 초기화 시켜주는 방향을 잡았습니다.
afterAll(() => {
jest.clearAllMocks(); // clear all mock.calls and mock.instances (not implementations)
});
TypeScript
복사
bm-v-afterAll
기본 준비가 끝난 것 같으니 바로 테스트 코드를 작성해봅시다.
1-1. 매핑 브랜드 목록, 브랜드 목록 테스트 : 구현부확인
페이지 처음 접근시 매핑 브랜드 목록과 브랜드 목록이 보이는지 테스트를 해야만 합니다.
먼저, 해당 코드가 실제로 어떻게 구현되어있는지 구현부를 확인합니다.
mappedBrand 와 brands가 선언되어있네요. 그리고 페이지 접근시 부르는 메서드를 찾아보겠습니다.
VM테스트는 View와 밀접한 관계를 가지고 있습니다. 먼저 View에서 해당 이벤트나 혹은 라이프사이클중 VM의 메서드를 호출하는지 판단하는 것이 먼저입니다.
보통 react의 라이프사이클 중 componentDidMount는 페이지가 로딩된 직후 특정한 로직을 1번만 처리하기 위해서 사용됩니다. 따라서, init 데이터 등을 셋팅하는 로직을 관례적으로 처리합니다. 현재 사례도 그렇게 처리하고 있네요.
그리고, VM에서 initPage 코드를 확인해봅시다.
자기 자신의 getBrandMapList 메서드를 호출하고 있습니다. 바로 확인해보죠.
위 내용으로 확인해보면, 해당 메서드는 다음처럼 정리가 됩니다.
BoutiqueService로부터 Model을 전달 받고 Model을 ViewModel로 맵핑하는 작업을 하고있다. 추가로, brand 목록도 동시에 받고 있네요.
BoutiqueService의 getBrandMapListFromSanta 라는 메서드를 통하여 모델을 전달받고 있습니다.
그렇다면 이부분의 구현 코드를 확인해보죠.
1.
반환데이터 확인 (반환결과를 만들어야하므로)
2.
화살표함수 확인
만약 화살표 함수라면 다음 작업으로 처리해야만 합니다.
jest.fn().mockResolvedValue();
jest.fn().mockReturnValue();
TypeScript
복사
1-2. 매핑 브랜드 목록, 브랜드 목록 테스트 : return Mock Data 준비
먼저 위 내용으로 확인해보면, BoutiqueService의 getBrandMapListFromSanta 라는 메서드를 통하여 모델을 전달받고 있습니다.
그렇다면 먼저 테스트용 모델 데이터를 구해봅시다.
사실, 이부분에서 실제로 model을 하나하나 작성해야하는게 맞지만, 그렇기에는 공수가 실상 너무 크기 때문에, postman에서 반환된 entity를 구현된 mapper 함수를 통하여 model화 해버립시다.
그리고, 서비스로케이터가 init 될 동안 다른 의존성을 제거하기 위해서, 그냥 return 이 아닌, 함수로 생성합니다.
다시 테스트 코드로 돌아가서 반환결과를 mock 화시켜줍시다.
beforeAll(() => {
const mockService = new (BoutiqueServiceImpl as jest.Mock)();
const context = NCApplicationContext.createContext(new AdminApplicationConfiguration());
// add
mockService.getBrandMapListFromSanta = jest.fn().mockResolvedValue(mockBrandMapList());
context.serviceLocator.registSingle(BoutiqueServiceClassName, () => mockService);
context.serviceLocator.registSingle(
BoutiqueMapVMClassName,
() => new BoutiqueMapVMImpl(mockService)
);
});
TypeScript
복사
bm-add-mockbrand
1-3. 매핑 브랜드 목록 테스트 : 테스트 코드 작성
테스트코드에서 vm으로부터 initPage 함수를 호출해봅시다.
test('브랜드관리 > 부티크 관리 > 부티크 맵핑 > 브랜드탭 > 매핑 브랜드 리스트가 보여야한다.', async () => {
const vm = Inject<BoutiqueMapVM>(BoutiqueMapVMClassName);
await vm.initPage(EBoutiqueMapType.BRAND);
});
TypeScript
복사
bm-call-initpage
아까 구현부에서 initPage를 호출 했을 경우 최종적으로 VM 내부에 있는 this._mappedBrand 가 변경되었을 것입니다.
그렇다면, 해당과 관련된 예상 테스트 코드를 작성해봅시다.
저는 다음과 같이 정하였습니다.
•
VM에 정의된 this_mappedBrand 가 원하는 타입으로 업데이트 되었는가?
위 내용으로 테스트 코드를 작성하면 다음처럼 작성됩니다.
vm.mappedBrandList.forEach((brand) => {
expect(typeof brand.id).toBe('number');
expect(typeof brand.brandId).toBe('number');
expect(typeof brand.brandName).toBe('string');
expect(
typeof brand.comment === 'string' || typeof brand.comment === 'undefined'
).toBeTruthy();
expect(typeof brand.mappingKey).toBe('string');
expect(typeof brand.boutiqueId).toBe('number');
});
TypeScript
복사
bm-expect-mappedBrand
위와 같이 테스트가 짜여진 이유는 실제로 전달받은 mappedBrandList의 타입은 다음과 같기 때문입니다.
위 테스트를 실행해봅시다.
성공
1-4. 브랜드 목록 테스트 : 테스트 코드 작성
결국 brands도 initPage 함수가 호출 될 경우 업데이트 되기 때문에 테스트코드에서 vm으로부터 initPage 함수를 호출해봅시다.
test('브랜드관리 > 부티크 관리 > 부티크 맵핑 > 브랜드탭 > 매핑 브랜드 리스트가 보여야한다.', async () => {
const vm = Inject<BoutiqueMapVM>(BoutiqueMapVMClassName);
await vm.initPage(EBoutiqueMapType.BRAND);
});
TypeScript
복사
bm-call-initpage
그리고, brands의 타입을 확인해봅시다.
export type MappingBrand = {
id: number;
name: string;
nameKr: string;
isDisplay: boolean;
};
TypeScript
복사
따라서, 다음과 같이 테스트코드가 도출되면 될 것 같습니다.
vm.brands.forEach((brand) => {
expect(typeof brand.id).toBe('number');
expect(typeof brand.name).toBe('string');
expect(typeof brand.nameKr).toBe('string');
expect(typeof brand.isDisplay).toBe('boolean');
});
TypeScript
복사
bm-expect-brands
위 테스트를 실행해봅시다.
성공
2-1. 브랜드 목록 Select 컬럼 테스트 : 구현부확인
위 View에서 VM에 어떠한 함수를 이용하여 데이터를 변경하고 있을 것 입니다.
먼저 View Code를 조사해봅시다.
현재 View에서는 VM의 onChangeMappingBrand 라는 함수로 테스트 하는 것을 확인했습니다.
그러면 구현부를 VM에서 onChangeMappingBrand 메서드의 구현부를 확인해봅시다.
이런 식으로 구현되어 있네요!
해당 메서드가 서비스에 의존되어있지 않아 테스트코드를 작성하는 것이 수월할 듯
합니다. 바로 작성해봅시다.
2-2. 브랜드 목록 Select 컬럼 테스트 : 테스트 코드 작성
구현된 함수를 살펴보니 리스트의 id와 선택한 brandId를 요구하고 있습니다.
아까 mock에 존재하는 id를 선택한 후, 데이터를 변경하여, 테스트가 제대로 동작하는지 테스트해봅시다.
먼저, vm.initPage를 이용하여 초기화 시켜줍니다.
그리고, onChangeMappingBrand 함수를 호출합니다.
test('브랜드관리 > 부티크 관리 > 부티크 맵핑 > 브랜드탭 > 매핑 브랜드 리스트 > 매핑브랜드 칼럼 > 브랜드를 선택할 경우 VM데이터가 변경되어야한다.', async () => {
const vm = Inject<BoutiqueMapVM>(BoutiqueMapVMClassName);
await vm.initPage(EBoutiqueMapType.BRAND);
vm.onChangeMappingBrand(4477, 1077);
// 기존 데이터 : 4477, 변경 데이터 : 1077
});
TypeScript
복사
bm-call-initPage
이렇게 완료되었다면, 테스트코드를 작성해봅시다.
onChangeMappingBrand 함수를 통하여 mappedBrand 데이터가 변경이 되었을 것입니다. 그리고 그 Id값을 찾고 expect().toBe로 변경되었는지 확인하면 됩니다.
const mappedBrand = vm.mappedBrandList.find((brand) => brand.id === 4477);
expect(mappedBrand?.brandId).toBe(1077);
TypeScript
복사
bm-find
위 테스트를 실행해봅시다.
성공
3-1. Comment 컬럼 입력 테스트 : 구현부확인
위 View에서 VM에 어떠한 함수를 이용하여 데이터를 변경하고 있을 것 입니다.
먼저 View Code를 조사해봅시다.
현재 View에서는 VM의 onChangeComment 라는 함수로 테스트 하는 것을 확인했습니다.
그러면 구현부를 VM에서 onChangeComment 메서드의 구현부를 확인해봅시다.
이런 식으로 구현되어 있네요!
해당 메서드도 마찬가지로 서비스에 의존되어있지 않아 테스트코드를 작성하는 것이 수월할 듯 합니다. 바로 작성해봅시다.
3-2. Comment 컬럼 입력 테스트 : 테스트 코드 작성
이전 셀렉트 테스트와 비슷하네요.
이번에는 리스트의 id와 comment 를 요구하고 있습니다.
아까 mock에 존재하는 id를 선택한 후, 데이터를 변경하여, 테스트가 제대로 동작하는지 테스트해봅시다.
4477 번은 기본값으로 comment 가 null인데요. mapper를 통해 아마 undefined로 변환되고있는것 같습니다.
4477번의 comment를 ‘Test Input Commnet’ 로 변경하고 이를 테스트해봅시다.
먼저, vm.initPage를 이용하여 초기화 시켜줍니다.
그리고, onChangeComment 함수를 호출합니다.
test('브랜드관리 > 부티크 관리 > 부티크 맵핑 > 브랜드탭 > 매핑 브랜드 리스트 > 매핑브랜드 칼럼 > 브랜드를 선택할 경우 VM데이터가 변경되어야한다.', async () => {
const vm = Inject<BoutiqueMapVM>(BoutiqueMapVMClassName);
await vm.initPage(EBoutiqueMapType.BRAND);
const comment = 'Test Input Comment'
vm.onChangeComment(4477, comment);
});
TypeScript
복사
bm-call-initPage
테스트코드를 작성해봅시다.
onChangeComment 함수를 통하여 mappedBrand 데이터가 변경이 되었을 것입니다. 그리고 그 Id값을 찾고 expect().toBe로 변경되었는지 확인하면 됩니다.
const mappedBrand = vm.mappedBrandList.find((brand) => brand.id === 4477);
expect(mappedBrand?.comment).toEqual(comment);
TypeScript
복사
bm-find
위 테스트를 실행해봅시다.
성공
느낀점
jest 를 이용하여 mock 화하는것이 나름 편리했지만, 직관적으로 mock이 되었는지 안되었는지 판단하기 아직까지는 어렵고 불편한 것 같습니다. 현재까지 그저 경험으로 판단하고 있는데 이게 위험하다는 생각이 게속들고있습니다. 물론 jest측에서도 mock화 되었는지 판단하는 함수가 존재하는 것을 확인했습니다.
추가로, mock화가 안되어서 에러가 발생한 것은 알겠는데 왜 mock코드를 사용했음에도 안되었는지 찾아내는 방법이 너무나도 고통스럽습니다. (초기화문제로 수시간을 삽질했습니다.)
또한 클래스의 get, static 코드 mock화 하는 것이 정말 어렵다는 것을 깨달았습니다.
좋았던 점 : 이번 세미나와 지금까지 테스트코드를 작성하면서 느꼇던 제일 좋았던 점은 테스트코드를 통해서 실제 구현부를 더 좋게 변경하게 된 계기를 마련해준것 같습니다. 이전 테스트코드를 작성할 때 까지만 해도 구현부에 테스트코드를 억지로 맞춰 끼워넣으려고 했습니다. 하지만 이제는 반대로 테스트코드를 편리하게 작성하기 위해서 구현부를 변경하는 방향으로 진행해봤는데요. 이렇게 할 경우, 좀 더 좋은 코드가 생산되는 듯한 느낌을 받았습니다.
추가로, 이번 세미나를 준비하면서 기존에 PR올라간 테스트코드를 일부분 변경하고 추가되어야함을 느꼈습니다.
읽어주셔서, 감사합니다.
참고
•
•
Mock 초기화 갑론을박 :
9935
