TIL

내일배움캠프 15주차 월요일 TIL

news0516 2025. 2. 3. 20:38

NestJS TrelloProject 컨트롤러, 서비스 테스트 파일 구성
> jest, @nestjs/testing 모듈 사용

1. lists.controller.spec.ts 구성
컨트롤러 테스트 > 컨트롤러가 요청을 받았을 때 서비스의 메서드를 올바르게 호출하는 지, 올바른 결과를 반환하는지 검증해야함

Jest  문법

1. describe() 함수
관련된 테스트 케이스들을 그룹화하는데 사용.
첫 번째 인자는 테스트그룹 명칭 문자열, 두 번째 인자는 해당 그룹에서 실행해야할 텟스트 케이스를 정의한 함수

 

describe('ListsController', () => {
	// 여기에 ListsController 관련 테스트 케이스 작성
});


2. it 메서드
test() 함수와 함께 특정 개별 테스트 케이스를 정의하는데 사용, 동일한 기능 수행
첫번째 인자는 테스트 설명, 두 번째 인자는 테스트 실행 함수

it('리스트 생성 테스트', async () => {


3. expect
Jest의 단언 메서드. 특정 조건이 충족되는지 검사.
테스트 결과를 평가하는데 사용되는 일련의 메처 함수 포함
toBe(): 객체의 동일성(===)을 검사하는 매처 함수(값과 형식까지)
toEqual() : 객체의 동일성을 검사하는 매처 함수
toHaveBeenCalledWith() : 메서드가 특정 인자로 호출되었는지 검사
toBeUndefined(): 값이 undefined인지 검사

4. mockResolvedValue
모의(mock) 함수의 동작을 설정하는 Jest 메서드


실제 테스트 코드 작성

0. 모의객체 설정 및  초기화
- 실제 서비스 대신 모의객체를 사용하여 컨트롤러의 의존성을 주입
- 테스트 모듈 구성 시 실제 서비스 대신 mockListsService를 주입 >> 실제 서비스 구현 없이 테스트 가능
- beforeEach 블록이 각 테스트 케이스 실행 전 호출되어 위 테스트 모듈 설정 진행
- afterEach 블록이 각 테스트 케이스 완료 후 호출되어 모든 모의함수 호출 기록 초기화 >> 각 테스트가 독립적으로 실행

describe('ListsController', () => {
  let controller: ListsController;
  let service: ListsService;

  // 서비스에 대한 모의 객체(mock)를 정의
  const mockListsService = {
    create: jest.fn(),
    update: jest.fn(),
    remove: jest.fn(),
    updatePositions: jest.fn(),
  };
	
  // beforeEach : 각 테스트 케이스 실행 전 호출
  beforeEach(async () => {
	// Test.createTestingModule 메서드로 모듈 생성
	// controller, provider 설정
    const module: TestingModule = await Test.createTestingModule({
      controllers: [ListsController],
      providers: [
        {
          provide: ListsService,
          useValue: mockListsService,
        },
      ],
    }).compile();
	
	// module.get 메서드를 사용해 설정한 모듈에서 ListsController, ListsService 인스턴스 가져옴
    controller = module.get<ListsController>(ListsController);
    service = module.get<ListsService>(ListsService);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });



1. create 메서드

// src/lists/lists.controller.ts
...
@Post()
async create(@Body() createListDto: CreateListDto) {
  return this.listsService.create(createListDto);
}
...

HTTP POST 요청이 들어오면 create 메서드가 실행.
클라이언트에서 전달된 body 데이터를 createListDto로 받고, 이를 서비스 create 메서드에 전달 후 결과 반환

describe('create', () => {
  it('리스트 생성 테스트', async () => {
    
	// 테스트 데이터 설정
    const createListDto: CreateListDto = {
      boardId: 1,
      title: 'New List',
    };

	// result : 예상 결과 값
    const result = { id: 1, boardId: 1, title: 'New List', position: 1 };

	// mockListsService.create의 동작 설정
	// 이 메서드 호출 시 result값을 반환하도록 설정 >> 테스트 성공 결과 미리 지정
    mockListsService.create.mockResolvedValue(result);

	// create 메서드 호출, createListDto가 인자로 전달, await으로 비동기 작업 완료까지 대기
    const response = await controller.create(createListDto);

	// 검증 단계
	// 1. service.create 가 createListDto 인자로 호출되었는지 검증
	// 2. response가 예상 결과와 동일한지 확인
    expect(service.create).toHaveBeenCalledWith(createListDto);
    expect(response).toEqual(result);
  });
});



2. update 메서드

// src/lists/lists.controller.ts
...
@Patch(':id')
async update(@Param('id') id: string, @Body() updateListDto: UpdateListDto) {
  return this.listsService.update(+id, updateListDto);
}
...

URL의 파라미터를 id로 받아 해당하는 리스트를 업데이트
내부에서 문자열로 전달된 파라미터를 숫자로 변환하여 서비스의 update 메서드에 전달

describe('update', () => {
  it('리스트 업데이트 테스트', async () => {
	
	// 테스트 데이터 설정
    const id = '1';
    const updateListDto: UpdateListDto = { title: '수정된 리스트 제목' };

	// result : 예상 결과 값
    const result = { id: 1, boardId: 1, title: '수정된 리스트 제목', position: 1 };

	// update 메서드 호출 시 result값을 반환하도록 설정 >> 테스트 성공 결과 미리 지정
    mockListsService.update.mockResolvedValue(result);

    // update 메서드 호출, id, updateListDto 인자로 전달, await으로 비동기 작업 완료까지 대기
    const response = await controller.update(id, updateListDto);

    	// 검증 단계
	// 1. service.update 가 1, updateListDto 인자로 호출되었는지 검증
	// 2. response가 예상 결과와 동일한지 확인
    expect(service.update).toHaveBeenCalledWith(1, updateListDto);
    expect(response).toEqual(result);
  });
});


3. remove 메서드

// src/lists/lists.controller.ts
...
@Delete(':id')
async remove(@Param('id') id: string) {
  return this.listsService.remove(+id);
}
...

URL의 파라미터를 id로 받아 해당하는 리스트를 삭제
내부에서 문자열로 전달된 파라미터를 숫자로 변환하여 서비스의 remove 메서드에 전달


describe('remove', () => {
  it('리스트 삭제 테스트', async () => {
    
	// 테스트 데이터 설정
    const id = '1';
	
	// remove 메서드 호출 시 undefined 반환하도록 설정 >> 삭제 경우이므로
    mockListsService.remove.mockResolvedValue(undefined);

    // remove 메서드 호출, id 인자로 전달, await으로 비동기 작업 완료까지 대기
    const response = await controller.remove(id);

    // 검증 단계
	// 1. service.remove 가 1 인자로 호출되었는지 검증
	// 2. response가 undefined인지 검증
    expect(service.remove).toHaveBeenCalledWith(1);
    expect(response).toBeUndefined();
  });
});


4. updatePositions 메서드

// src/lists/lists.controller.ts
...
@Patch() // 명확한 엔드포인트 지정 권장
async updatePositions(
  @Body() updateListPositionsDto: UpdateListPositionsDto,
): Promise<{ message: string }> {
  await this.listsService.updatePositions(updateListPositionsDto);
  return { message: '리스트 위치가 성공적으로 업데이트되었습니다.' };
}
...

body에 전달된 updateListPositionsDto를 이용해 여러 리스트의 위치를 업데이트

describe('updatePositions', () => {
  it('리스트 포지션 업데이트 테스트', async () => {
   
	 // 테스트 데이터 설정
    const updateListPositionsDto: UpdateListPositionsDto = {
      boardId: 1,
      lists: [
        { id: 1, position: 1 },
        { id: 2, position: 2 },
        { id: 3, position: 3 },
      ],
    };
	
	// updatePositions 메서드 호출 시 undefined 반환하도록 설정 >> 성공한 경우
    mockListsService.updatePositions.mockResolvedValue(undefined);

    // updatePositions 메서드 호출, updateListPositionsDto 인자로 전달, 
	// await으로 비동기 작업 완료까지 대기
    const response = await controller.updatePositions(updateListPositionsDto);

    // 검증 단계
	// 1. updatePositions 가 updateListPositionsDto 인자로 호출되었는지 검증
	// 2. response가 예상 결과와 동일한지 확인
    expect(service.updatePositions).toHaveBeenCalledWith(updateListPositionsDto);
    expect(response).toEqual({
      message: '리스트 위치가 성공적으로 업데이트되었습니다.',
    });
  });
});


5. 최종 lists.controller.spec.ts

// src/lists/lists.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { ListsController } from './lists.controller';
import { ListsService } from './lists.service';
import { CreateListDto } from './dto/create-list.dto';
import { UpdateListDto } from './dto/update-list.dto';
import { UpdateListPositionsDto } from './dto/update-list-positions.dto';

describe('ListsController', () => {
  let controller: ListsController;
  let service: ListsService;

  const mockListsService = {
    create: jest.fn(),
    update: jest.fn(),
    remove: jest.fn(),
    updatePositions: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [ListsController],
      providers: [
        {
          provide: ListsService,
          useValue: mockListsService,
        },
      ],
    }).compile();

    controller = module.get<ListsController>(ListsController);
    service = module.get<ListsService>(ListsService);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('create', () => {
    it('리스트 생성 테스트', async () => {
      const createListDto: CreateListDto = {
        boardId: 1,
        title: 'New List',
      };

      const result = { id: 1, boardId: 1, title: 'New List', position: 1 };

      mockListsService.create.mockResolvedValue(result);

      const response = await controller.create(createListDto);

      expect(service.create).toHaveBeenCalledWith(createListDto);
      expect(response).toEqual(result);
    });
  });

  describe('update', () => {
    it('리스트 업데이트 테스트', async () => {
      const id = '1';
      const updateListDto: UpdateListDto = { title: '수정된 리스트 제목' };

      const result = { id: 1, boardId: 1, title: '수정된 리스트 제목', position: 1 };

      mockListsService.update.mockResolvedValue(result);

      const response = await controller.update(id, updateListDto);

      expect(service.update).toHaveBeenCalledWith(1, updateListDto);
      expect(response).toEqual(result);
    });
  });

  describe('remove', () => {
    it('리스트 삭제 테스트', async () => {
      const id = '1';

      mockListsService.remove.mockResolvedValue(undefined);

      const response = await controller.remove(id);

      expect(service.remove).toHaveBeenCalledWith(1);
      expect(response).toBeUndefined();
    });
  });

  describe('updatePositions', () => {
    it('리스트 포지션 업데이트 테스트', async () => {
      const updateListPositionsDto: UpdateListPositionsDto = {
        boardId: 1,
        lists: [
          { id: 1, position: 1 },
          { id: 2, position: 2 },
          { id: 3, position: 3 },
        ],
      };

      mockListsService.updatePositions.mockResolvedValue(undefined);

      const response = await controller.updatePositions(updateListPositionsDto);

      expect(service.updatePositions).toHaveBeenCalledWith(updateListPositionsDto);
      expect(response).toEqual({
        message: '리스트 위치가 성공적으로 업데이트되었습니다.',
      });
    });
  });
});