-
Notifications
You must be signed in to change notification settings - Fork 2
[BE] 테스트 용도의 Redis Docker 생성 및 통합 테스트 진행
⚙️ Web BE |
---|
김진 |
2025.01.08. 작성 |
graph LR
A[테스트 환경 구축] --> B[컨테이너 실행]
B --> C[테스트 수행]
C --> D[컨테이너 종료 후 제거]
D --> E[테스트 완료]
- 테스트를 수행하기 전 필요한 환경을 컨테이너 기반으로 구축
- 테스트를 수행할 때 생성된 컨테이너에 접근해 테스트
- 테스트가 종료되면 생성된 컨테이너를 제거
👍🏻 장점
- 각각의 테스트만을 위한 격리된 환경이므로 테스트 간 발생될 수 있는 간섭을 제거해 신뢰도 향상
- 로컬, CI/CD 파이프라인 등 어떤 환경에서든 동일한 테스트가 동작할 수 있도록 보장
👎🏻 단점
- docker 환경이 구축되어 있어야 사용할 수 있음
- container 생성이 필요해 추가 리소스가 사용됨
- 독립적인 테스트 환경이 많아질 수록 컨테이너 시작과 종료를 여러 번 수행하기 때문에 시간이 오래 걸림
# server/src/docker-compose.yml
services:
redis:
image: redis:latest
container_name: redis_test
ports:
- "6379:6379"
- 아직 Redis를 어디에 둘 지 등 결정되지 않은 게 많은 상태여서
server/
위치에 임시용 파일을 생성했다.
해당 파일을 실행하면 다음과 같이 동작한다.
-
-d
는 백그라운드 실행 옵션 - 윈도우 이용 중이라
wsl
에 docker와 docker-compose를 설치하여 사용할 수 있었다.- 별 다른 이유는 없고.. 그냥
Docker desktop
을 깔고 싶지 않았다. 이미 설치되어 있던wsl
를 활용하고 싶었다.
- 별 다른 이유는 없고.. 그냥
종료도 정상적으로 동작한다.
이를 바탕으로 통합 테스트부터 진행해보자! (소켓 때문에…. 아직 e2e는… 🥲)
열심히 코드를 작성하고 확인해보았는데 자꾸 오류가 발생했다.
thrown: "Exceeded timeout of 5000 ms for a hook.
Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout."
39 | });
40 |
> 41 | beforeEach(async () => {
| ^
42 | client = {
43 | handshake: {
44 | auth: { roomId: '', playerId: '' },
at drawing/drawing.gateway.e2e.spec.ts:41:3
at Object.<anonymous> (drawing/drawing.gateway.e2e.spec.ts:10:1)
beforeEach
부분에서 계속 timeout이 발생해 console.log
를 작성하면서 확인해본 결과….
// drawing.gateway.e2e.spec.ts - beforeEach 부분
beforeEach(async () => {
client = {
// ...
} as any;
console.log('client 완료');
console.log('room hash 세팅 시작');
await redisService.hset('room:room1', { roomId: 'room1' });
console.log('room hash 세팅 완료');
await redisService.hset('room:room1:players:player1', { playerId: 'player1' });
});
room hash 세팅 완료
라는 콘솔 메시지가 출력되지 않는 것을 확인했다.
그래서 코드를 살짝 바꿔서 hset
만 되지 않는 것인지, 그냥 redis 연결이 안됐는지를 확인해 보았다.
// drawing.gateway.e2e.spec.ts - beforeEach 부분
beforeEach(async () => {
client = {
// ...
} as any;
console.log('client 완료');
console.log('redis 연결 확인: ' + (await redisService.checkConnection()));
await redisService.hset('room:room1', { roomId: 'room1' });
await redisService.hset('room:room1:players:player1', { playerId: 'player1' });
});
// ...
// redis.service.ts - checkConnection 부분
async checkConnection() {
return (await this.redis.ping()) === 'PONG';
}
이미지처럼 client 완료
메시지는 제대로 출력되지만 redisService
를 사용하는 부분은 동작하지 않는다는 것을 확인했다.
문제는 상당히 쉽게.. 해결할 수 있었다ㅠㅠ(별건 아닌데 소요시간은 오래 걸렸다..ㅠㅠ)
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
DrawingGateway,
DrawingService,
DrawingRepository,
RedisService,
{
provide: ConfigService,
useValue: {
get: jest.fn().mockImplementation((key: string) => {
if (key === 'REDIS_HOST') return 'redis';
if (key === 'REDIS_PORT') return '6379';
return null;
}),
},
},
],
}).compile();
gateway = module.get<DrawingGateway>(DrawingGateway);
service = module.get<DrawingService>(DrawingService);
redisService = module.get<RedisService>(RedisService);
});
REDIS_HOST
를 service 이름인 redis
로 사용해서 발생한 문제였다.
Docker Compose로 Redis를 실행해서 사용하는 거라 redis
라는 서비스 이름으로 접근해야 한다고 생각했었는데, 수행 중인 테스트는 Docker 컨테이너 안이 아닌 내 로컬 컴퓨터에서 실행해야 하는 거라 redis
라는 이름을 인식하지 못했던 것이다.
해당 부분을 localhost
로 바꾸면서 해결할 수 있었다.
이렇게~ 테스트가 실행되는 것을 확인할 수 있었다! 이제 실패한 테스트에 대해서만 해결하면 통합 테스트는 완료된다!
(오늘의 배운점)
it.only()
또는 describe.only()
를 사용하면 해당 테스트 블럭만 실행할 수 있다! 나처럼 하나의 it
만 실패하는 경우에 손쉽게 해당 테스트만 실행하고 확인할 수 있을 것이다ㅎㅎ!
코드는 아래와 같이 작성했다.
it.only('room과 player가 정상적으로 존재하는 경우', async () => {
client.handshake.auth = { roomId: 'room1', playerId: 'player1' };
await gateway.handleConnection(client);
expect(client.join).toHaveBeenCalledWith('room1');
expect(client.data.roomId).toBe('room1');
expect(client.data.playerId).toBe('player1');
});
코드는 내가 짜서 하는 말이 절대 아니고 진짜 오류 날 부분이 없어보여서 redis에 저장된 값을 먼저 확인해보았다.
문제는 바로 여기에 있었다!
beforeEach(async () => {
client = {
handshake: {
auth: { roomId: '', playerId: '' },
},
data: {},
join: jest.fn(),
to: jest.fn().mockReturnValue({
emit: jest.fn(),
}),
} as any;
await redisService.hset('room:room1', { roomId: 'room1' });
await redisService.hset('room:room1:players:player1', { playerId: 'player1' });
});
가장 마지막 줄 코드를 보면 아래처럼 작성이 되어있다.
await redisService.hset('room:room1:**players**:player1', { playerId: 'player1' });
그리고.. 레포지토리에서 확인해보면?
async existsPlayer(roomId: string, playerId: string) {
const exists = await this.redisService.exists(`room:${roomId}:**player**:${playerId}`);
return exists === 1;
}
문제는 바로 player가 아닌 players라고 s를 붙여서 문제가 됐던 것이다! 어쩐지 전혀 오류가 날 부분이 아니었는데… 다음부터는 직접 치지 말고 레포지토리에서 복사 붙여넣기해서 작업하는 편이 훨씬 좋을 거 같다..
(e2e 테스트하려고 만든건데 시간 관계상 통합 테스트만 진행해서 파일명을 수정했다.)
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { Socket } from 'socket.io';
import { RedisService } from '../redis/redis.service';
import { DrawingGateway } from './drawing.gateway';
import { DrawingService } from './drawing.service';
import { DrawingRepository } from './drawing.repository';
import { BadRequestException, PlayerNotFoundException, RoomNotFoundException } from '../exceptions/game.exception';
describe('DrawingGateway 통합 테스트', () => {
let gateway: DrawingGateway;
let service: DrawingService;
let redisService: RedisService;
let client: Socket;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
DrawingGateway,
DrawingService,
DrawingRepository,
RedisService,
{
provide: ConfigService,
useValue: {
get: jest.fn().mockImplementation((key: string) => {
if (key === 'REDIS_HOST') return 'localhost';
if (key === 'REDIS_PORT') return '6379';
return null;
}),
},
},
],
}).compile();
gateway = module.get<DrawingGateway>(DrawingGateway);
service = module.get<DrawingService>(DrawingService);
redisService = module.get<RedisService>(RedisService);
});
beforeEach(async () => {
client = {
handshake: {
auth: { roomId: '', playerId: '' },
},
data: {},
join: jest.fn(),
to: jest.fn().mockReturnValue({
emit: jest.fn(),
}),
} as any;
await redisService.hset('room:room1', { roomId: 'room1' });
await redisService.hset('room:room1:player:player1', { playerId: 'player1' });
});
// 테스트가 수행될 때마다 DB를 비워줌
afterEach(async () => {
await redisService.flushAll();
});
describe('handleConnection', () => {
it('roomId 또는 playerId의 값이 존재하지 않는 경우', async () => {
const authInfo = [
{ roomId: '', playerId: '' },
{ roomId: 'room1', playerId: '' },
{ roomId: '', playerId: 'player1' },
];
for (const auth of authInfo) {
client.handshake.auth = auth;
await expect(gateway.handleConnection(client)).rejects.toThrowError(
new BadRequestException('Room ID and Player ID are required'),
);
}
});
it('room이 redis 내에 존재하지 않는 경우', async () => {
client.handshake.auth = { roomId: 'failed-room', playerId: 'player1' };
await expect(gateway.handleConnection(client)).rejects.toThrowError(new RoomNotFoundException('Room not found'));
});
it('player가 redis 내에 존재하지 않는 경우', async () => {
client.handshake.auth = { roomId: 'room1', playerId: 'failed-player' };
await expect(gateway.handleConnection(client)).rejects.toThrowError(
new PlayerNotFoundException('Player not found in room'),
);
});
it('room과 player가 정상적으로 존재하는 경우', async () => {
client.handshake.auth = { roomId: 'room1', playerId: 'player1' };
await gateway.handleConnection(client);
expect(client.join).toHaveBeenCalledWith('room1');
expect(client.data.roomId).toBe('room1');
expect(client.data.playerId).toBe('player1');
});
});
describe('handleDraw', () => {
it('roomId 값이 존재하지 않는 경우', async () => {
client.data = {};
const data = { drawingData: {} };
await expect(gateway.handleDraw(client, data)).rejects.toThrowError(
new BadRequestException('Room ID is required'),
);
});
it('정상적으로 그림이 그려지는 경우', async () => {
client.data = { roomId: 'room1', playerId: 'player1' };
const data = { drawingData: { pos: 56, fillColor: { R: 0, G: 0, B: 0, A: 0 } } };
await gateway.handleDraw(client, data);
expect(client.to).toHaveBeenCalledWith('room1');
expect(client.to('room1').emit).toHaveBeenCalledWith('drawUpdated', {
playerId: 'player1',
drawingData: data.drawingData,
});
});
});
});
이전과 달리 모킹하지 않고 직접 gateway.handleConnection()
이나 gateway.handleDraw()
함수를 실행해도 정상적으로 동작하는 것을 확인할 수 있다!
-
REDIS_HOST
와REDIS_PORT
는 이후 CI/CD 환경에서.env
파일을 생성하지 않더라도 원활하게 동작할 수 있게 값을 모킹했다.