@SpyBean
private DuplicateRequestChecker duplicateRequestChecker;
@SpyBean
private PointKeyRepository pointKeyRepository;
@MockBean
RedisTemplate<String, Object> mockRedis;
@Autowired
private PointKeyFacadeV1 pointKeyFacadeV1;
@Autowired
private AuthServiceV1 authServiceV1;
@DisplayName("레디스 에러가 발생했을 때 광고 시청 시 fallback 메서드가 작동한다.")
@Test
void test2() {
String transactionId = "tx123";
// Mock ValueOperations 객체 생성
ValueOperations<String, String> valueOperations = mock(ValueOperations.class);
// opsForValue() 호출시 valueOperations 반환하도록 설정
when(mockRedis.opsForValue()).thenReturn(valueOperations);
// setIfAbsent 호출시 예외 발생하도록 설정
when(valueOperations.setIfAbsent(
eq("dusgh1234" + ":" + transactionId),
eq("success"),
eq(10L),
eq(TimeUnit.SECONDS)
)).thenThrow(new RedisCommandTimeoutException("Redis is down"));
// when & then
AdRewardRequest adRewardRequest = new AdRewardRequest("dd", 3);
pointKeyFacadeV1.createPointKeyForAd("dusgh1234",adRewardRequest, LocalDateTime.now(), transactionId);
assertThatThrownBy(()->pointKeyFacadeV1.createPointKeyForAd("dusgh1234",adRewardRequest, LocalDateTime.now(), transactionId))
.isInstanceOf(CustomException.class).hasMessage("TRANSACTION ALREADY PROCESSED");
verify(pointKeyRepository, times(2)).existsByTransactionId(transactionId);
verify(duplicateRequestChecker, times(2)).fallback(
eq("dusgh1234"),
eq(transactionId),
any(RedisConnectionFailureException.class) // CustomException이 아닌 실제 발생하는 예외
);
}
이번 테스트에서는 verify를 사용했다.
이 메서드를 사용하려면 mock이거나 Spy여야 한다.
Mokito가 mock을 생성할 때는 실제 인스턴스가 아니라, 객체의 타입만 보고 한다. 이 mock 객체가 다른 인스턴스와 상호작용하는 것만 보기 위해서 만들기 때문이다.
spy는 실제 인스턴스를 안에 품고 있다. 그래서, 실제 인스턴스처럼 행동하지만, 이 spy는 다른 인스턴스와의 상호작용을 추적할 수 있다.
spybean 어노테이션을 붙이지 않고 @Autowired를 한 다음에 verify를 하면
이처럼 mock이 아니라는 예외가 발생한다.
verify는 실제 그 메서드가 몇번 동작했는지를 추적하게 해준다.
지금 테스트 하려는 건 레디스 서버와 연결이 안 되는 상황에서
fallback 메서드가 동작하는지이다.
그래서, fallback 메서드 안에서 사용하는 PointKeyRepository와 이 fallback 메서드가 있는 DuplicateRequestChecker를 @Spybean으로 등록해놓고 이들의 메서드가 실제로 사용됐는지를 확인했다.
@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry;
@Test
void testCircuitBreakerStateTransition() {
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(SIMPLE_CIRCUIT_BREAKER_CONIFG);
ValueOperations<String, String> valueOperations = mock(ValueOperations.class);
when(mockRedis.opsForValue()).thenReturn(valueOperations);
when(valueOperations.setIfAbsent(any(), any(), anyLong(), any()))
.thenThrow(new RedisCommandTimeoutException("Redis is down"));
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED);
// CircuitBreaker가 OPEN 상태가 될 때까지 충분한 실패 요청 발생
for(int i = 0; i < 10; i++) {
try {
duplicateRequestChecker.checkDuplicateRequest("user" + i, "tx" + i);
} catch (Exception ignored) {}
}
// OPEN 상태에서는 즉시 fallback 호출됨 (Redis 호출 없이)
duplicateRequestChecker.checkDuplicateRequest("test", "tx");
verify(mockRedis, times(7)).opsForValue(); // minimum-number-of-calls까지만 실제 호출
verify(pointKeyRepository, times(10)).existsByTransactionId(any()); // 모든 요청이 fallback으로
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN);
try {
Thread.sleep(8000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.HALF_OPEN);
}
이런식으로 레디스를 닫아놓은 다음에
요청을 계속 보내고 나서 서킷브레이커의 상태가 변했는지 테스트할 수도 있다.
@DisplayName("half-closed 상태에서 성공하면 closed 상태가 된다")
@Test
void testHalfOpenToClosed() throws InterruptedException {
ValueOperations<String, String> valueOperations = mock(ValueOperations.class);
when(mockRedis.opsForValue()).thenReturn(valueOperations);
when(valueOperations.setIfAbsent(any(), any(), anyLong(), any()))
.thenThrow(new RedisCommandTimeoutException("Redis is down")); // 처음에는 실패
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(SIMPLE_CIRCUIT_BREAKER_CONIFG);
for (int i = 0; i < 10; i++) {
try {
duplicateRequestChecker.checkDuplicateRequest("user" + i, "tx" + i);
} catch (Exception ignored) {
}
}
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN);
Thread.sleep(5000);
when(valueOperations.setIfAbsent(any(), any(), anyLong(), any()))
.thenReturn(true);
for (int i = 0; i < 5; i++) { // permitted-number-of-calls-in-half-open-state 값만큼
duplicateRequestChecker.checkDuplicateRequest("user" + i, "tx" + i);
}
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED);
}
half-closed 상태에서 레디스를 정상 모드로 변경해놓고
계속 호출을 성공시키면 closed로 정상 복구된다.
계속 이 예외가 떴다.
아무리 고쳐도 처음 mock한것의 영향이 계속 남아있어서
@Test
void testHalfOpenToClosed() throws InterruptedException {
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(SIMPLE_CIRCUIT_BREAKER_CONIFG);
ValueOperations<String, String> valueOperations = mock(ValueOperations.class);
when(mockRedis.opsForValue()).thenReturn(valueOperations);
// 슬로우콜로 OPEN 상태 만들기
when(valueOperations.setIfAbsent(any(), any(), anyLong(), any()))
.thenAnswer(invocation -> {
Thread.sleep(2000); // slow-call-duration-threshold보다 길게
return true;
});
// OPEN 상태 만들기
for (int i = 0; i < 10; i++) {
duplicateRequestChecker.checkDuplicateRequest("user" + i, "tx" + i);
}
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN);
Thread.sleep(4000);
// 성공 케이스
when(valueOperations.setIfAbsent(any(), any(), anyLong(), any()))
.thenReturn(true);
// HALF_OPEN에서 permitted-number-of-calls-in-half-open-state만큼 성공
for (int i = 0; i < 5; i++) {
duplicateRequestChecker.checkDuplicateRequest("user" + i, "tx" + i);
}
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED);
}
이렇게 슬로우 콜을 주면서 open 상태가 되게하고 그 뒤로 closed상태로 변하게 했다.