Макетные данные Spring Boot

модульный тест
Макетные данные Spring Boot

задний план

При использовании Spring Boot для микросервисов или связанной с этим разработки и проектирования разделения фронтенда и бэкенда интерфейсы обычно используются для стыковки, и в это время выдвигаются соответствующие требования к тестированию проекта, такие как модульное тестирование, тестирование наследования, вызов стороннего тестирования интерфейса и т. д., чтобы достичь соответствующего тестового покрытия; тест стороннего интерфейса должен возвращать результаты моделирования сам по себе, а затем для достижения эффекта тестирования требуется соответствующий возврат данных Mock.

Связанные фиктивные данные

  • Mockito
  • PowerMock
  • TestNG
  • EasyMock
  • etc.

Вот лишь некоторые из наиболее популярных, и существует множество практичных mock-фреймворков, каждый со своими характеристиками.Например, PowerMock может реализовывать макеты статических методов, частных методов и других методов, вы можете сами найти соответствующие mock-фреймворки, которые приведены здесь.ссылка PowerMock(GitHub).

упражняться

В проекте, если вам не нужно выполнять проверочные тесты для статических методов, частных методов и т. д., вы можете использовать только Mockito, который поставляется с загрузкой Spring, для завершения Mock соответствующих тестовых данных.При необходимости вы можете использовать PowerMock, это просто и практично, и его можно комбинировать с Spring, используйте внедрение аннотаций;

Некоторые простые варианты использования здесь повторяться не будут. Вы можете запросить соответствующие статьи (базовых статей много). Здесь мы в основном описываем сторонние интерфейсы, встречающиеся в проекте, которые необходимо вызывать моками. При тестировании уровня контроллера, вам нужно инкапсулировать межуровневый макет.Сторонний интерфейс возвращает данные для проверки правильности программы. (Как показано на рисунке, фиктивные данные пересекают слои)

Я нашел много статей в Интернете, большинство из которых содержат повторяющиеся и бесполезные базовые варианты использования, и в проекте нет данных о межуровневых макетах; межуровневый макет включает в себя автоматическое внедрение контейнера IOC Spring, поэтому макет базовый учебник не включает в себя межуровневую мокировку. После практики подведите итоги мок-операций, которые использовались в проекте;

1. Используйте Mockito (без специального метода Mock)

Поскольку Mockito интегрирован в тест Junit Sring Boot, его не нужно специально вводить при использовании, достаточно использовать напрямую: (код следующий)

public abstract class AbstractMockBeanTest {

/**
   * BasiceServer封装了对应的三方接口服务,主要是转换接口返回结果数据
   * 使用MockBean注解,自动将IOC容器中需要的对象直接替换成Mock对象,然后Mock相应数据
   */
  @MockBean
  protected BasicServer basicServer;

  /**
   * 如需 basicServer对应的方法需要返回什么类型结果,请在测试前自行调用以下对应方法
   */
  protected void mockGetCarTypeSuccess() {
    String msg = "{\"code\":200,\"data\":{\"businessType\":0,\"capacityType\":0,\"engineType\":0,\"id\":0," +
        "\"seats\":0},\"msg\":\"success\",\"ok\":true}";
    Mockito.when(basicServer.getCarType(anyLong())).thenReturn(msg);
  }

  protected void mockGetCarTypeSuccessNoData() {
    String msg = "{\"code\":200,\"msg\":\"success\",\"ok\":true}";
    Mockito.when(basicServer.getCarType(anyLong())).thenReturn(msg);
  }


  protected void mockGetCitySuccess() {
    String msg = "{\"code\":200,\"data\":{\"businessType\":0,\"capacityType\":0,\"engineType\":0,\"id\":0," +
        "\"seats\":0},\"msg\":\"success\",\"ok\":true}";
    Mockito.when(basicServer.getCityByCode(anyString())).thenReturn(msg);
  }

  protected void MockGetCitySuccessSuccessNoData() {
    String msg = "{\"code\":200,\"msg\":\"success\",\"ok\":true}";
    Mockito.when(basicServer.getCityByCode(anyString())).thenReturn(msg);
  }
}

2. Используйте PowerMock (доступен специальный метод Mock)

Из-за межуровневого взаимодействия, когда тестовый код использует контейнер IOC для автоматического внедрения соответствующего объекта, вам необходимо вручную указать, чтобы заменить автоматически внедренный объект вашим собственным фиктивным объектом, чтобы он вступил в силу; процесс замены определяется отражением. (код показан ниже)

package cn.jasmine.capacity.apis.v1;

import cn.jasmine.capacity.application.impl.CarServiceImpl;
import cn.jasmine.capacity.third.basic.v1.BasicService;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.hamcrest.core.Is;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.modules.junit4.PowerMockRunnerDelegate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;

import java.util.Optional;

import static org.mockito.ArgumentMatchers.anyLong;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.handler;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 * @author Jasmine
 * @date 19/06/19
 */
@ActiveProfiles("test")
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringRunner.class)
@PrepareForTest({BasicService.class})
@PowerMockIgnore({"javax.management.*", "javax.net.ssl.*"})
@SpringBootTest
public class TestController {
  @Autowired
  private WebApplicationContext context;

  private MockMvc mvc;

  @Mock
  private BasicService basicService;

  @Autowired
  private CarServiceImpl carService;

  @Before
  public void setUp() throws Exception {
    mvc = MockMvcBuilders.webAppContextSetup(context).build();
    // 使用反射将IOC容器注入的service的属性替换指定为mock的对象
    ReflectionTestUtils.setField(carService, "basicService", basicService);
    PowerMockito.when(this.basicService, "getCarType", anyLong()).thenReturn(Optional.empty());
  }

  @Test
  @Transactional
  public void testA() throws Exception {
        MvcResult result = mvc.perform(MockMvcRequestBuilders.get("/v1/cars")
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .accept(MediaType.APPLICATION_JSON)
    ).andExpect(status().isOk())
            .andExpect(handler().handlerType(CarController.class))
            .andExpect(handler().methodName("search"))
            .andDo(MockMvcResultHandlers.print())
            .andReturn();
    JSONObject object = JSON.parseObject(result.getResponse().getContentAsString(), JSONObject.class);
    Assert.assertThat("数据量不是2", object.getJSONObject("data").getJSONArray("list").size(), Is.is(2));
  }
}

3. Практическое применение

Сцены:

  • Проект микросервиса SpringBoot
  • Сервис бизнес-уровня и тест уровня dao (включая db [mysql])

Пример:

  • Используйте базу данных H2 в памяти вместо одиночного теста mysql
  • Настройте файл build.gradle (maven добавляется в указанном формате)
    /**
     * 测试使用
     */
    testRuntimeOnly 'com.h2database:h2'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testCompile group: 'org.powermock', name: 'powermock-module-junit4', version: '2.0.0'
    testCompile group: 'org.powermock', name: 'powermock-api-mockito2', version: '2.0.0'
  • Настройте файл application-test.yml
# 使用H2代替MySQL
spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=MySQL
    schema: classpath:init_table.sql
    data: classpath:init_data.sql
  • Поскольку служба содержит операции уровня dao, для достижения тестового покрытия уровня dao это нельзя сделать с помощью поспешных имитаций, в противном случае его нельзя точно проверить; одиночный тест не имитирует операции уровня dao, используйте весеннюю загрузку
// ……
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.modules.junit4.PowerMockRunnerDelegate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.List;

import static org.hamcrest.Matchers.not;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;

/**
 * 上报信息保存
 *
 * @author Jasmine
 * @date 19/06/18
 */
@ActiveProfiles("test")
@RunWith(PowerMockRunner.class)
// 单元测试需启动spring
@PowerMockRunnerDelegate(SpringRunner.class)
@PowerMockIgnore("javax.management.*")
@PrepareForTest(CarReportInfoService.class)
@SpringBootTest
@Transactional
public class CarReportInfoServiceImplTest extends AbstractServiceTest {

  @Autowired
  private CarReportInfoService reportInfoService;

  @Autowired
  private CarReportInfoMapper reportInfoMapper;
  
   @Test
  public void save() {
    // 模拟请求参数
    // 调用mapper数据
    // 返回结果
    String driverNo = "10000";
    double lon = 120.234453;
    CartReportInfoCreate reportInfoRequest = new CartReportInfoCreate();
    reportInfoRequest.setDriverNo(driverNo);
    reportInfoRequest.setLon(lon);
    reportInfoRequest.setLat(120.234643);
    reportInfoRequest.setReportTime(new Date());
    reportInfoRequest.setPhoneVersion("IOS");
    reportInfoRequest.setAppVersion("12.2");
    reportInfoRequest.setReportType(0);
    reportInfoRequest.setReportAddress("胜利大街");

    reportInfoService.create(reportInfoRequest);

    List<CarReportInfoEntity> carReportInfos = reportInfoMapper.selectAll();
    int sizeCount = carReportInfos.size();

    assertThat("司机编号错误", carReportInfos.get(sizeCount - 1).getDriverNo(), equalTo(driverNo));

    // 上报重复
    try {
      reportInfoService.create(reportInfoRequest);
    } catch (CapacityRuntimeException e) {
      assertEquals(e.getCode(), StatusCode.ALREADY_REPORTED.getCode());
    }
  }

  /**
   * 少传递参数抛出异常
   */
  @Test(expected = DataIntegrityViolationException.class)
  public void saveException() {
    String driverNo = "456789876768684342";
    double lon = 120.234453;
    CartReportInfoCreate reportInfoRequest = new CartReportInfoCreate();
    reportInfoRequest.setDriverNo(driverNo);
    reportInfoRequest.setLon(lon);
    reportInfoRequest.setLat(120.234643);
    reportInfoRequest.setReportTime(new Date());
//    reportInfoRequest.setAppType("IOS");
    reportInfoRequest.setAppVersion("12.2");

    reportInfoService.create(reportInfoRequest);
  }


  /**
   * 测试获取时间段内听单时长
   */
  @Test
  public void getListenDuration() throws Exception {
    String driverNo = "345678976543";
    // 时间段没有上报记录,之前有出车记录()
//    Instant instantSpec = LocalDateTime.of(2019, 6, 25, 19, 00, 9).atZone().toInstant();
    Instant instantSpec = Instant.parse("2019-06-25T19:00:09.00Z");
    PowerMockito.mockStatic(Instant.class);
    PowerMockito.when(Instant.now()).thenReturn(instantSpec);

    // 时间段在当前时间之后,返回0
    String startString = "2019-06-25T10:00:09.00Z";
    String endString = "2019-06-25T10:01:09.00Z";
    PowerMockito.when(Instant.parse(startString))
            .thenReturn(DateTimeFormatter.ISO_INSTANT.parse(startString, Instant::from));
    PowerMockito.when(Instant.parse(endString))
            .thenReturn(DateTimeFormatter.ISO_INSTANT.parse(endString, Instant::from));
    CarReportPageSearch request = new CarReportPageSearch();
    request.setStartDate(Instant.parse(startString));
    request.setEndDate(Instant.parse(endString));
    Long listenDuration = reportInfoService.getListenDuration(driverNo, request);
    assertThat("时间错误", listenDuration, is(32400L));

    // 时间段没有上报记录,之前有出车记录()
    request = new CarReportPageSearch();
    startString = "2019-06-25T18:50:09.00Z";
    endString = "2019-06-25T19:00:09.00Z";
    PowerMockito.when(Instant.parse(startString))
            .thenReturn(DateTimeFormatter.ISO_INSTANT.parse(startString, Instant::from));
    PowerMockito.when(Instant.parse(endString))
            .thenReturn(DateTimeFormatter.ISO_INSTANT.parse(endString, Instant::from));
    request.setStartDate(Instant.parse(startString));
    request.setEndDate(Instant.parse(endString));
    listenDuration = reportInfoService.getListenDuration(driverNo, request);
    assertThat("时间错误", listenDuration, equalTo(600L));

    // 时间段没有上报记录,之前有收车记录
    request = new CarReportPageSearch();
    startString = "2019-06-25T18:55:10.00Z";
    endString = "2019-06-25T19:00:09.00Z";
    PowerMockito.when(Instant.parse(startString))
            .thenReturn(DateTimeFormatter.ISO_INSTANT.parse(startString, Instant::from));
    PowerMockito.when(Instant.parse(endString))
            .thenReturn(DateTimeFormatter.ISO_INSTANT.parse(endString, Instant::from));
    request.setStartDate(Instant.parse(startString));
    request.setEndDate(Instant.parse(endString));
    listenDuration = reportInfoService.getListenDuration(driverNo, request);
    assertThat("时间错误", listenDuration, is(299L));

    // 非正常情况(收车-出车)
    request = new CarReportPageSearch();
    Instant start21 = LocalDateTime.of(2019, 6, 25, 17, 54, 10).atZone(ZoneId.of("UTC")).toInstant();
    Instant end22 = LocalDateTime.of(2019, 6, 25, 18, 55, 10).atZone(ZoneId.of("UTC")).toInstant();
    Instant instant21 = LocalDateTime.of(2019, 6, 25, 17, 55, 9).atZone(ZoneId.of("UTC")).toInstant();
    Instant instant22 = LocalDateTime.of(2019, 6, 25, 18, 55, 9).atZone(ZoneId.of("UTC")).toInstant();
    startString = "2019-06-25T17:54:10.00Z";
    endString = "2019-06-25T18:55:10.00Z";
    PowerMockito.when(Instant.parse(startString))
            .thenReturn(DateTimeFormatter.ISO_INSTANT.parse(startString, Instant::from));
    PowerMockito.when(Instant.parse(endString))
            .thenReturn(DateTimeFormatter.ISO_INSTANT.parse(endString, Instant::from));
    request.setStartDate(Instant.parse(startString));
    request.setEndDate(Instant.parse(endString));
    listenDuration = reportInfoService.getListenDuration(driverNo, request);
    long allTime = ChronoUnit.SECONDS.between(start21, instant21) + ChronoUnit.SECONDS.between(instant22, end22);
    assertThat("时间错误", listenDuration, equalTo(3959L));
  }
}

Суммировать

По сравнению с Mockito PowerMock предоставляет больше методов Mock, которые являются относительно гибкими и рекомендуемыми.

Использование PowerMock теперь соответствует зависимости gradle, используйте powermock-api-mockito2, не используйте powermock-api-mockito:

testCompile group: 'org.powermock', name: 'powermock-module-junit4', version: '2.0.2'
testCompile group: 'org.powermock', name: 'powermock-api-mockito2', version: '2.0.2'
testCompile group: 'org.mockito', name: 'mockito-core', version: '2.28.2'

Примечание. При использовании более высокой версии powermock будут возникать некоторые ошибки.

  1. bug: GitHub.com/power mock/afraid…
  2. Решение:stackoverflow.com/questions/6…

Для получения подробной информации перейдите по следующим ссылкам:

  1. PowerMock
  2. Стыковка версий