결과: 통합 테스트 8개 + 실제 Redis 테스트 3개, 전체 11개 통과. 5회 반복 실행 모두 성공. 실행: cd backend && ./gradlew test (Redis가 localhost:6379에 없으면 Redis 테스트 3개는 자동 skip)


1. 왜 테스트가 필요했나

이 프로젝트에는 처음에 테스트가 하나도 없었습니다(src/test 디렉터리 없음). 그동안의 검증은 브라우저 탭 두 개를 띄워 눈으로 확인하는 방식이었고, 그 결과 다음 문제들이 오래 남아 있었습니다.

눈으로는 못 찾은 문제 왜 안 보였나
늦은 입장자에게 1:1 메시지가 도착하지 않음 예외도 로그도 없이 조용히 버려짐
같은 세션의 이동 순서 역전 혼자 로컬에서 천천히 움직이면 거의 발생하지 않음
퇴장 직후 flush로 플레이어 부활 수 ms 사이의 타이밍에서만 발생
cook 역할로 입장 불가 테스트할 때 늘 server로만 입장

2. 테스트 구조

src/test/java/com/snowpeak/game/
├── repository/
│   ├── InMemoryPlayerRepository.java      # 테스트용 저장소 (Redis 대체)
│   └── RedisPlayerRepositoryTest.java     # 실제 Redis로 Lua·직렬화 검증
└── sync/
    └── GameSyncIntegrationTest.java       # 실제 서버 + STOMP 클라이언트 E2E

2-1. 통합 테스트: 실제 서버에 실제 STOMP 클라이언트로 접속

@SpringBootTest(webEnvironment = RANDOM_PORT, properties = "snowpeak.sync.flush-interval-ms=50")
class GameSyncIntegrationTest {

    @TestConfiguration
    static class InMemoryStoreConfig {
        @Bean @Primary
        PlayerRepository inMemoryPlayerRepository() { return new InMemoryPlayerRepository(); }
    }
    ...
    stompClient = new WebSocketStompClient(
            new SockJsClient(List.of(new WebSocketTransport(new StandardWebSocketClient()))));

2-2. Redis 테스트: Redis만 할 수 있는 것은 Redis로

메모리 구현으로는 Lua Script와 직렬화 호환성을 검증할 수 없습니다. 그래서 RedisPlayerRepository는 실제 Redis에 붙여 따로 테스트합니다.

assumeTrue(redisUp, "Redis is not running on localhost:6379");
테스트 검증
saveIfAbsentRejectsDuplicateNickname HSETNX로 두 번째 저장이 거절되고 첫 값이 유지됨
updateAllIfPresentOnlyTouchesExistingPlayersAndStaysReadable Lua가 존재하는 필드만 갱신(1개 갱신, 1개 무시). Lua로 저장한 JSON을 HVALS로 다시 객체로 읽을 수 있음
deleteReportsWhetherPlayerExisted HDEL 반환값으로 퇴장 여부를 판단할 수 있음

2-3. 타이밍 버그는 "상황을 직접 만들어서" 테스트