-
Notifications
You must be signed in to change notification settings - Fork 2
코드의 안정성을 높이기 위해 테스트코드를 작성해보자
프로젝트가 막바지로 다가오게 되면서 작성한 코드의 양이 많아지고 구조가 점점 더 복잡해지는 것을 많이 느끼게 되었습니다. 물론 구현이 최우선이지만 결국 나중에 가서 기존 코드의 복잡성 때문에 새로운 기능 구현이 어려워진다면 의미가 없어질 것 같아 리팩토링에 관한 고민을 가지게 되었고 이를 기반으로 멘토님에게 피드백을 요청하게 되었습니다.
그때 멘토님께서 해주신 말씀중에 좋은 테스트 코드를 작성하고 이를 기반으로 리팩토링을 진행하는것이 안전하다는 말씀을 해주셨습니다. 각각의 동작에 관하여 정확한 흐름을 언제든 확인 할 수 있는 수단이 있어야 코드의 형태가 변하더라도 아웃풋을 정확히 낼 수 있다는 말이 인상이 깊어 테스트 코드를 도입하게 되었습니다.
테스트 코드의 종류는 크게 3가지로
- 단위 테스트 (Unit Test): 개별 함수, 메서드, 클래스 단위의 테스트
- 통합 테스트 (Integration Test): 여러 컴포넌트간의 상호작용 테스트
- E2E 테스트 (End-to-End Test): 실제 사용자 시나리오 기반 테스트
사실 그동안 많이 진행을 했던 테스트는 단위테스트였는데 물론 한 함수가 복잡한 로직을 내부에서 포함을 하고 있으면 매우 중요한 테스트이지만 현재 inear의 주요 로직은 주로 외부 컴포넌트들과의 상호작용이 복잡하게 얽혀있다보니 통합 테스트 혹은 E2E 테스트가 적합할 것이라고 생각을 했습니다.
물론 E2E 테스트로 한번에 모든 사용자 시나리오에 관한 테스트를 진행하면 좋지만 그전에 핵심기능들에 관한 독립적인 테스트를 우선적으로 진행하고 빠르게 확인을 하고 싶었습니다. 그렇게 통합 테스트를 선택하게 되었고 가장 먼저 테스트하기로 한 로직은 admin의 앨범 및 노래 정보 업로드 이후 DB에 형식에 맞게 잘 저장이 되는지를 테스트하는 것이었습니다. 메인 페이지에 많은 정보들이 해당 업로드된 정보에 의존함으로 주요 로직으로 분류하고 먼저 진행하였습니다.
admin의 앨범 및 노래 정보 등록은 http request 중 POST /admin/album
을 통하여 이루어집니다. 다만 해당 요청 처리 로직은 서버에서 꽤 많은 역할을 진행합니다
그러다보니 해당 요청에 관하여 MySQL만 독립적으로 테스트를 하기 위해서는 다른 외부 컴포넌트와의 interaction을 모킹해주는 것이 선행이 되어야했습니다
우선 POST /admin/album
의 주요 동작 로직은 다음을 포함합니다
- Album 정보를 AlbumDto 형식으로 받아서 MySQL에 저장을 하고 albumID를 반환 → 주요 테스트 로직
- 받은 음악파일을 m3u8이랑 ts 파일로 파싱을 진행한 이후에 해당 파싱된 파일을 s3 object storage에 업로드 합니다 → Mocking 필요!
- Album의 image와 banner image를 object storage에 저장하고 url을 반환합니다 → Mocking 필요
- 각 앨범의 이미지 url을 MySQL에 저장합니다 → 주요 테스트 로직
- 프로세싱을 마친 각 노래에 관한 정보를 MySQL에 저장 → 주요 테스트 로직
- 실시간 세션에 관한 정보를 Redis에 업데이트 → Mocking 필요
우선 Request 요청은 supertest를 통해 서버에서 직접적으로 불러와서 실행을 할 수가 있습니다
const response = await supertest(app.getHttpServer())
.post('/admin/album')
.field('albumData', JSON.stringify(albumData))
.attach('albumCover', mockFiles.albumCover.buffer, 'album.jpg')
.attach('bannerCover', mockFiles.bannerCover.buffer, 'banner.jpg')
.attach('songs', mockFiles.songs[0].buffer, 'song1.mp3')
.attach('songs', mockFiles.songs[1].buffer, 'song2.mp3');
다음과 같이 Redis와 s3 object storage와의 interaction은 철저히 모킹으로 진행하여 MySQL 저장이 잘 이루어지는지를 우선적으로 확인하고자 하였습니다
그렇기에 해당 로직을 담당하는 다른 레이어들의 함수를 모킹하여 return 값을 미리 지정을 하는 과정을 진행하였습니다
jest.spyOn(adminService as any, 'uploadImageFiles').mockResolvedValue({
albumCoverURL: 'mock-album-url',
bannerCoverURL: 'mock-banner-url',
});
jest
.spyOn(musicProcessingService, 'processUpload')
.mockImplementation(async (file, tempDir, songMetadata) => {
const randomDuration =
Math.floor(Math.random() * (300 - 120 + 1)) + 120;
return {
...songMetadata,
duration: randomDuration,
};
});
jest
.spyOn(adminService, 'initializeStreamingSession')
.mockResolvedValue();
jest.spyOn(roomService, 'initializeRoom').mockResolvedValue();
이후로는 직접적으로 전달을 담당할 앨범 및 노래에 관한 Mock 데이터들을 우선적으로 지정을 진행하게 되었습니다
export const createTestAlbumData = (): AlbumDto => {
const albumDto = new AlbumDto();
albumDto.title = 'TEST ALBUM 1';
albumDto.artist = 'TEST ARTIST 1';
albumDto.releaseDate = new Date('2024-01-01');
...
return albumDto;
};
Integration 테스트 특성상 직접적으로 MySQL DB에 확인을 진행해야하지만 기존의 DB를 사용하면 안되기에 inear-test라는 테스트 DB를 만들게 되고 여기에서 테스트를 진행하게 되었습니다. 해당 spec.ts에서 프로젝트에서 사용하는 함수들을 불러와서 MySQL을 직접적으로 테스트하는 코드입니다
const savedAlbum = await albumRepository.findById(
response.body.albumId,
);
expect(savedAlbum).toBeDefined();
expect(savedAlbum.title).toBe(albumData.title);
...
const savedSongs = await songRepository.getAlbumTracksSorted(
savedAlbum.id,
'ASC',
);
expect(savedSongs).toHaveLength(mockFiles.songs.length);
savedSongs.forEach((song, index) => {
expect(song.albumId).toBe(savedAlbum.id);
expect(song.trackNumber).toBe(index + 1);
...
});
앨범 정보는 이상없이 잘 저장이 되었으나 노래는 그렇지 못한다는것이 확인이 되었습니다.
조금 더 디테일하게 기존의 구현코드들을 보니 노래 정보를 순차적으로 업로드 하는 부분에서 비동기 처리가 제대로 되지 않은 점이 확인이 되었습니다. 이를 기반으로 수정을 진행하였고, 테스트코드의 도입으로 코드의 안정성을 높일수가 있었습니다.
async saveSongs(songs: Song[], albumId: string) {
const songsToSave = songs.map(
(song) => new Song(new SongSaveDto({ ...song, albumId: albumId })),
);
await this.songRepository.saveSongList(songsToSave);
}
→ 기존에는 forEach 안에서 각각의 Song을 저장하는 로직을 사용했습니다. forEach에서 비동기 작업은 병렬로 실행되어 순서가 보장되지 않습니다. 또한 각각의 save 작업이 개별 트랜잭션으로 처리됩니다
이에 다음과 같이 수정을 진행했습니다
- map으로 객체 생성과 데이터 변환을 먼저 처리
- saveSongList로 단일 트랜잭션에서 일괄 처리
🚀 ffmpeg는 stderr로 디버깅을 하는 이유
🚀 HLS 프로토콜에 관한 정리 및 FFmpeg 사용기
🚀 비트는 tsconfig.json이 세 개?
🚀 NestJS 기본 개념 - Modules
🚀 Socket.io 최(강)적화
🚀 도커와 nginx의 사용기
🚀 부하테스트를 해보자
🚀 FSD 사용기, 근데 이제 나만의 규칙을 곁들인
🚀 CICD 구조 수정
🚀 앨범 단위로 스트리밍 하기 (with HLS)
🚀 HLS로 음악 주고받기
🚀 vite + react + typescript 환경에서 path alias 설정
🚀 React Scan이 뭐죠?
🚀 로컬 환경 개발 모드 배포
🚀 앨범 전체를 스트리밍한다고? (with HLS)
🚀 코드의 안정성을 높이기 위해 테스트코드를 작성해보자
🚀 새로고침 시 HLS ERROR
🚀 input 태그에 한글 입력 후, Enter를 누르면 함수가 두번 호출되는 오류
🚀 nginx proxy pass를 바꿨더니 생긴 에러 - 스웨거 인식 문제
🚀 배포 환경에서 클라이언트-서버 WS handshake
🚀 렌더링 범인은 하나!
🌈 그라운드 룰
🥔 팀원 소개
🔎 코드 & 깃 컨벤션
🌳 깃 branch 전략
📌 노션 문서 저장소
🎨 피그마
🧑💻 기획 공유 발표 자료
🎤 2주차 발표 자료
😎 백로그
🗓️ 1주차
🗓️ 2주차
🗓️ 3주차
🗓️ 4주차
🗓️ 5주차