CircuitBreaker 자동화 테스트, @MockBean과 @SpyBean 사용하기

Alex·2024년 11월 21일
0

Plaything

목록 보기
22/118


@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로 정상 복구된다.

레디스가 계속 down됐다고?


계속 이 예외가 떴다.

아무리 고쳐도 처음 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상태로 변하게 했다.

profile
답을 찾기 위해서 노력하는 사람

0개의 댓글