Spring Boot 测试 - 测试套件中的异步方法不起作用

问题描述 投票:0回答:1

我正在编写一个测试类,我想验证一个用 Async 注释并返回 CompletableFuture 的方法是否执行代码分支

有什么用

  1. Spring Boot 3.x
  2. 莫基托
  3. 春季测试
  4. Spring 异步
  5. Spring 集成 (JMS)
  6. VSCode

我开发了什么

我开发了这个Java类ClientTracingServiceImpl

package com.tutorial.experiment.service.tracing;

import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

import com.tutorial.experiment.component.mapper.IClientTracingMapper;
import com.tutorial.experiment.data.interfaces.TracingMessage;
import com.tutorial.experiment.data.types.ClientTracing;
import com.tutorial.experiment.data.types.EErrorCodes;
import com.tutorial.experiment.data.types.EMessageTopology;
import com.tutorial.experiment.data.types.EStep;
import com.tutorial.experiment.database.db2.entity.tracing.ClientTracingEntity;
import com.tutorial.experiment.database.db2.repository.tracing.IClientTracingRepository;
import com.tutorial.experiment.exception.common.SystemApplicationException;
import com.tutorial.experiment.exception.tracing.ClientTracingNotFoundException;
import com.tutorial.experiment.exception.tracing.InvalidTracingMessageTypeException;

import org.springframework.data.domain.Example;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import io.micrometer.core.annotation.Timed;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@RequiredArgsConstructor
@Slf4j
@Service
public class ClientTracingServiceImpl implements IClientTracingService {

    private final IClientTracingRepository repository;
    private final IClientTracingMapper mapper;
    private final JmsTemplate jmsTemplate;
    
    @Async("jmsThreadPoolTaskExecutor")
    @Override
    public CompletableFuture<Void> notifyTracing(TracingMessage message) throws InvalidTracingMessageTypeException {
        String destinationName = "";
        EMessageTopology type = message.getType();
        if (type == null) {
            throw new InvalidTracingMessageTypeException();
        }
        destinationName = switch (type) {
            case REQUEST -> "tracing.client.request";
            case RESPONSE -> "tracing.client.response";
            default -> throw new InvalidTracingMessageTypeException();
        };
        jmsTemplate.convertAndSend(destinationName, message);
        log.info("Notified message: '{}'", message);
        return new CompletableFuture<>();
    }
    
    @Timed(value = "find.tracing.by.id.timer", description = "Time of execution of the search a specific tracing by id")
    @Override
    public ClientTracing findTracingById(Long idTracing, String xRequestId)
            throws ClientTracingNotFoundException, SystemApplicationException {
        ClientTracing response = null;
        try {
            ClientTracingEntity queryResult = repository.findById(idTracing)
                    .orElseThrow(() -> new ClientTracingNotFoundException(idTracing));
            response = mapper.entityToData(queryResult);
        } catch (ClientTracingNotFoundException e) {
            throw e;
        } catch (Exception e) {
            throw new SystemApplicationException(
                    "Occurred an internal error while search tracing with id: " + idTracing, e,
                    EErrorCodes.ENTITY_SEARCH, EStep.FIND_TRACE);
        }
        log.info("Record found with id: '{}'", idTracing);
        return response;
    }
    
    @Timed(value = "find.tracing.timer", description = "Time of execution of the search a specific tracing")
    @Override
    public List<ClientTracing> findTracing(String xRequestId, String requestId, String responseId)
            throws ClientTracingNotFoundException, SystemApplicationException {
        List<ClientTracing> response = null;
        try {
            ClientTracingEntity entity = ClientTracingEntity.builder().requestId(requestId).responseId(responseId)
                    .build();
            List<ClientTracingEntity> queryResult = repository.findAll(Example.of(entity));
            if (queryResult.isEmpty()) {
                throw new ClientTracingNotFoundException(requestId, responseId);
            }
            response = queryResult.stream()
                    .map(mapper::entityToData)
                    .collect(Collectors.toList());
        } catch (ClientTracingNotFoundException e) {
            throw e;
        } catch (Exception e) {
            throw new SystemApplicationException(
                    "Occurred an internal error while search tracing with requestId: " + requestId + " and responseId: "
                            + responseId,
                    EErrorCodes.ENTITY_SEARCH, EStep.MAPPING_TRACE_RESPONSE);
        }
        return response;
    }

}

我想做什么

  1. 在异步模式下测试notifyMethod的所有积极场景
  2. 在异步模式下使用 cautch 异常测试 notifyMethod 的所有负面场景

这是我的测试类ClientTracingServiceImplTest

package com.tutorial.experiment.service.tracing;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.verify;

import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import com.tutorial.experiment.data.interfaces.TracingMessage;
import com.tutorial.experiment.data.types.EErrorCodes;
import com.tutorial.experiment.data.types.EMessageTopology;
import com.tutorial.experiment.data.types.EStep;
import com.tutorial.experiment.database.db2.repository.tracing.IClientTracingRepository;
import com.tutorial.experiment.exception.tracing.InvalidTracingMessageTypeException;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.jms.core.JmsTemplate;

@SpringBootTest
class ClientTracingServiceImplTest {

    @Autowired
    private IClientTracingService iClientTracingService;

    @Autowired
    private IClientTracingRepository iClientTracingRepository;

    @SpyBean
    private JmsTemplate jmsTemplate;

    private TracingMessage message;
    private final ArgumentCaptor<String> queueCaptor = ArgumentCaptor.forClass(String.class);
    private final ArgumentCaptor<TracingMessage> messageCaptor = ArgumentCaptor.forClass(TracingMessage.class);

    private static final Integer TIMEOUT_SECONDS = 60;
    private static final String REQUEST_QUEUE = "tracing.client.request";
    private static final String RESPONSE_QUEUE = "tracing.client.response";

    @BeforeEach
    void init() {
        message = new TracingMessage();
        message.setApiPath("/test/api");
        message.setHttpMethod("GET");
        message.setStatusCode("200");
        message.setRequestId(UUID.randomUUID().toString());
        message.setResponseId(UUID.randomUUID().toString());
    }

    @AfterEach
    void tearDown() {
        iClientTracingRepository.deleteAll();
    }

    @DisplayName("Notify Tracing Message Request")
    @Test
    void whenNotifyJMSRequestMessage_thenQueueEqualsRequest() {
        message.setType(EMessageTopology.REQUEST);
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            iClientTracingService.notifyTracing(message);
        });
        try {
            future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
            assertTrue(!future.isCompletedExceptionally());
            verify(jmsTemplate).convertAndSend(queueCaptor.capture(), messageCaptor.capture());
            assertEquals(REQUEST_QUEUE, queueCaptor.getValue(), "Queue not match: ".concat(queueCaptor.getValue()));
            assertEquals(message, messageCaptor.getValue(), "Original message was modified");
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            fail("Async execution failed or timed out");
        }
    }

    @DisplayName("Notify Tracing Message Response")
    @Test
    void whenNotifyJMSResponseMessage_thenQueueEqualsResponse() {
        message.setType(EMessageTopology.RESPONSE);
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            iClientTracingService.notifyTracing(message);
        });
        try {
            future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
            assertTrue(!future.isCompletedExceptionally());
            verify(jmsTemplate).convertAndSend(queueCaptor.capture(), messageCaptor.capture());
            assertEquals(RESPONSE_QUEUE, queueCaptor.getValue(), "Queue not match: ".concat(queueCaptor.getValue()));
            assertEquals(message, messageCaptor.getValue(), "Original message was modified");
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            fail("Async execution failed or timed out");
        }
    }

    @DisplayName("Notify Tracing Message With Unknown Message Type")
    @Test
    void givenException_whenNotifyJMSUnknownMessageType_thenInvalidMessageTypeException() {
        message.setType(EMessageTopology.UNKNOWN);
        CompletableFuture<Void> future = iClientTracingService.notifyTracing(message);
        try {
            future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
            fail("Expected InvalidTracingMessageTypeException, but no exception was thrown");
        } catch (InterruptedException | ExecutionException e) {
            Throwable cause = e.getCause();
            assertTrue(cause instanceof InvalidTracingMessageTypeException);
            InvalidTracingMessageTypeException exception = (InvalidTracingMessageTypeException) cause;
            assertEquals(EStep.TRACE_MESSAGE, exception.getStep());
            assertEquals(EErrorCodes.LOGICAL_ERROR, exception.getCode());
            assertThat(exception.getMessage()).contains("Invalid message type");
        } catch (TimeoutException e) {
            fail("Timeout waiting for CompletableFuture to complete");
        }
    }

    @DisplayName("Notify Tracing Message With Null Message Type")
    @Test
    void givenException_whenNotifyJMSNullMessageType_thenInvalidMessageTypeException() {
        message.setType(null);
        CompletableFuture<Void> future = iClientTracingService.notifyTracing(message);
        try {
            future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
            fail("Expected InvalidTracingMessageTypeException, but no exception was thrown");
        } catch (InterruptedException | ExecutionException e) {
            Throwable cause = e.getCause();
            assertTrue(cause instanceof InvalidTracingMessageTypeException);
            InvalidTracingMessageTypeException exception = (InvalidTracingMessageTypeException) cause;
            assertEquals(EStep.TRACE_MESSAGE, exception.getStep());
            assertEquals(EErrorCodes.LOGICAL_ERROR, exception.getCode());
            assertThat(exception.getMessage()).contains("Invalid message type");
        } catch (TimeoutException e) {
            fail("Timeout waiting for CompletableFuture to complete");
        }
    }
}

发生了什么

  1. 当我运行单个测试时它有效
  2. 当我运行所有测试时,除了 whenNotifyJMSResponseMessage_thenQueueEqualsResponse 之外,它们都有效,因为 verify 方法即使从运行的日志中也无法检测到代码分支

日志

[2023-08-14 09:48:41,911] - [JmsClientTracing-1] - [INFO] - [] - [Notified message: 'TracingMessage(requestId=5ded9d50-aa41-4e9b-ba2d-eabb5125e470, responseId=6e1fa3a2-26f9-47c1-a42c-8d5b2a096830, statusCode=200, apiPath=/test/api, httpMethod=GET, type=REQUEST)']
[2023-08-14 09:48:41,913] - [JmsClientTracing-1] - [DEBUG] - [] - [Method: 'CompletableFuture com.tutorial.experiment.service.tracing.ClientTracingServiceImpl.notifyTracing(TracingMessage)' executed in 34ms]
[2023-08-14 09:48:41,914] - [Thread-2 (ActiveMQ-server-org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl$6@2db55dec)] - [INFO] - [] - [AMQ601265: User anonymous@invm:0 is creating a core consumer on target resource ServerSessionImpl() with parameters: [0, tracing.client.request, null, 0, false, true, null]]
[2023-08-14 09:48:41,922] - [JmsClientTracing-2] - [INFO] - [] - [Notified message: 'TracingMessage(requestId=690743d7-4a42-4d52-a10a-5d1be19211bb, responseId=f9a8feb8-a66e-44b4-9e82-3dbdd9b3c531, statusCode=200, apiPath=/test/api, httpMethod=GET, type=RESPONSE)']
java spring-boot junit mockito spring-test
1个回答
0
投票

您的测试 whenNotifyJMSResponseMessage_thenQueueEqualsResponse() 执行两个异步操作,并且测试仅在第一个操作中被阻止

  1. 测试在主线程(t1)中执行
  2. runAsync 生成另一个线程 (t2) 并在 t1 中返回 CompletableFuture
  3. t1 被阻塞,直到 t2 完成(future.get())。
  4. 在 t2 中,执行被测试的方法。 @Async 注释导致另一个线程生成 (t3)。另一个 CompletableFuture 返回给 t2。该返回值被忽略(它不存储在任何变量中)
  5. t2 完成后,t1 不再被阻塞。
  6. 测试恢复工作并可能在 t3 之前或之后完成,但与 t3 无关
  7. T3 随时完成,无需通知 t1

测试“givenException_whenNotifyJMSUnknownMessageType_thenInvalidMessageTypeException” 直接调用被测方法。

我不知道为什么测试在单独执行时会完成。我有一些最喜欢的,但没有证据。

© www.soinside.com 2019 - 2024. All rights reserved.