Модульное тестирование SpringBoot с Mockito

модульный тест

Модульное тестирование SpringBoot с Mockito

Модульные тесты должны следовать →Принципы ВОЗДУХА

Поддержка тестирования SpringBoot обеспечивается двумя модулями:

  • spring-boot-testСодержит основные элементы
  • spring-boot-test-autoconfigureАвтоматическая настройка для поддержки тестов

Обычно мы просто импортируемspring-boot-starter-testПросто зависите от него, он включает в себя некоторые часто используемые модули Junit, Spring Test, AssertJ, Hamcrest, Mockito и т. д.

Связанные комментарии

SpringBoot использует Junit4 в качестве среды модульного тестирования, поэтому аннотации соответствуют Junit4.

аннотация эффект
@Test(исключено==xx.class,timeout=миллисекунды) При изменении метода тестирования параметр ExcePted может игнорировать определенные классы исключений.
@Before Выполняется один раз перед запуском каждого метода тестирования
@BeforeClass Выполняется до выполнения всех тестовых методов
@After Выполняется один раз после запуска каждого метода тестирования
@AfterClass Выполняется после выполнения всех тестовых методов
@Ignore Украшенные классы или методы игнорируются исполнителем тестов.
@RunWith Изменить средство запуска тестов

@SpringBootTest

SpringBoot предоставляет А.@SpringBootTestАннотации используются для тестирования приложений SpringBoot, которые можно использовать в качестве стандартного весеннего теста.@ContextConfigurationАльтернатива аннотациям, работающая за счет создания ApplicationContext в тесте через SpringApplication.

@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTest {
}

Эта аннотация предоставляет два свойства для конфигурации:

  • webEnvironment: указывает среду веб-приложения, это могут быть следующие значения
    • MOCK: Предоставляет аналоговую среду сервлета, встроенный контейнер сервлета не запускается, и сотрудничество можно использовать в сочетании с @autoconfigureMockmvc для тестирования приложений на основе MockMVC.
    • RANDOM_PORT: загрузить EmbeddedWebApplicationContext и предоставить настоящую среду встроенного сервлета, случайный порт.
    • DEFINED_PORT: загружает EmbeddedWebApplicationContext и предоставляет действительно встроенную среду сервлета, порт по умолчанию 8080 или указанный в файле конфигурации.
    • NONE: использовать SpringApplication для загрузки ApplicationContext, но не предоставлять какой-либо контекст сервлета.
  • classes: укажите класс запуска приложения, обычно нет необходимости его устанавливать, поскольку SpringBoot будет автоматически искать до тех пор, пока не найдет аннотацию @SpringBootApplication или @SpringBootConfiguration.

откат модульного теста

Если вы добавите аннотацию @Transactional, она будет откатываться в конце каждого тестового метода.

Но в настоящей среде сервлетов, такой как RANDOM_PORT или DEFINED_PORT, HTTP-клиент и сервер будут работать в разных потоках, тем самым разделяя транзакции. В этом случае любые транзакции, запущенные на сервере, не будут откатываться.

утверждение

JUnit4 в сочетании с Hamcrest предоставляет новый синтаксис утверждений —assertThat, в сочетании с сопоставителями, предоставленными Hamcrest, вы можете выразить все идеи тестирования.

// 一般匹配符
int s = new C().add(1, 1);
// allOf:所有条件必须都成立,测试才通过
assertThat(s, allOf(greaterThan(1), lessThan(3)));
// anyOf:只要有一个条件成立,测试就通过
assertThat(s, anyOf(greaterThan(1), lessThan(1)));
// anything:无论什么条件,测试都通过
assertThat(s, anything());
// is:变量的值等于指定值时,测试通过
assertThat(s, is(2));
// not:和is相反,变量的值不等于指定值时,测试通过
assertThat(s, not(1));

// 数值匹配符
double d = new C().div(10, 3);
// closeTo:浮点型变量的值在3.0±0.5范围内,测试通过
assertThat(d, closeTo(3.0, 0.5));
// greaterThan:变量的值大于指定值时,测试通过
assertThat(d, greaterThan(3.0));
// lessThan:变量的值小于指定值时,测试通过
assertThat(d, lessThan(3.5));
// greaterThanOrEuqalTo:变量的值大于等于指定值时,测试通过
assertThat(d, greaterThanOrEqualTo(3.3));
// lessThanOrEqualTo:变量的值小于等于指定值时,测试通过
assertThat(d, lessThanOrEqualTo(3.4));

// 字符串匹配符
String n = new C().getName("Magci");
// containsString:字符串变量中包含指定字符串时,测试通过
assertThat(n, containsString("ci"));
// startsWith:字符串变量以指定字符串开头时,测试通过
assertThat(n, startsWith("Ma"));
// endsWith:字符串变量以指定字符串结尾时,测试通过
assertThat(n, endsWith("i"));
// euqalTo:字符串变量等于指定字符串时,测试通过
assertThat(n, equalTo("Magci"));
// equalToIgnoringCase:字符串变量在忽略大小写的情况下等于指定字符串时,测试通过
assertThat(n, equalToIgnoringCase("magci"));
// equalToIgnoringWhiteSpace:字符串变量在忽略头尾任意空格的情况下等于指定字符串时,测试通过
assertThat(n, equalToIgnoringWhiteSpace(" Magci   "));

// 集合匹配符
List<String> l = new C().getList("Magci");
// hasItem:Iterable变量中含有指定元素时,测试通过
assertThat(l, hasItem("Magci"));

Map<String, String> m = new C().getMap("mgc", "Magci");
// hasEntry:Map变量中含有指定键值对时,测试通过
assertThat(m, hasEntry("mgc", "Magci"));
// hasKey:Map变量中含有指定键时,测试通过
assertThat(m, hasKey("mgc"));
// hasValue:Map变量中含有指定值时,测试通过
assertThat(m, hasValue("Magci"));

Пример базового модульного теста

Ниже приведен базовый пример модульного теста, который утверждает возвращаемый результат метода:

@Service
public class UserService {

    public String getName() {
        return "lyTongXue";
    }
    
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

    @Autowired
    private UserService service;

    @Test
    public void getName() {
        String name = service.getName();
        assertThat(name,is("lyTongXue"));
    }

}

Тест контроллера

Spring предоставляет MockMVC для поддержки тестирования Spring MVC в стиле RESTful с использованием MockMvcBuilder для создания экземпляров MockMvc. MockMvc имеет две реализации:

  • StandaloneMockMvcBuilder: указывает WebApplicationContext, который будет получен от соответствующего контроллера и для предоставления соответствующего контекста MockMvc.

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class UserControllerTest  {
        @Autowired
        private WebApplicationContext webApplicationContext;
        private MockMvc mockMvc;
        @Before
        public void setUp() throws Exception {
            mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    } 
    
  • DefaultMockMvcBuilder: Укажите набор контроллеров через параметры, чтобы их не нужно было получать из контекста.

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class UserControllerTest  {
        private MockMvc mockMvc;
        @Before
        public void setUp() throws Exception {
            mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
        } 
    }    
    

Вот простой пример использования UserController/v1/users/{id}интерфейс для тестирования.

@RestController
@RequestMapping("v1/users")
public class UserController {

    @GetMapping("/{id}")
    public User get(@PathVariable("id") String id) {
        return new User(1, "lyTongXue");
    }

    @Data
    @AllArgsConstructor
    public class User {
        private Integer id;
        private String name;
    }

}
// ...
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {

    @Autowired
    private WebApplicationContext webApplicationContext;
    private MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void getUser() {
        mockMvc.perform(get("/v1/users/1")
                .accept(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(status().isOk())
           .andExpect(content().string(containsString("\"name\":\"lyTongXue\"")));
    }
  
}

Описание метода

  • perform: выполнить запрос RequestBuilder и вернуть объект экземпляра ResultActions, который можно ожидать, и другие операции над результатом запроса.

  • get: Объявить метод для отправки запроса на получение, другие типы запросов можно найти →Документация MockMvcRequestBuilders

  • andExpect: добавьте правило проверки ResultMatcher, чтобы проверить правильность результата запроса, правило проверки можно найти →Документация MockMvcResultMatchers

  • andDo: добавить обработчик результатов ResultHandler, например вывод результата на консоль при отладке, можно найти больше обработчиков →Документация MockMvcResultHandlers

  • andReturn: возвращает результат выполнения запроса, который является объектом экземпляра MvcResult →MVCRESULT Документ

ИМИТАЦИОННЫЕ данные

При модульном тестировании вызовы уровня службы часто связаны с внешними зависимостями, такими как базы данных и промежуточное ПО. В соответствии с принципом модульного тестирования AIR модульное тестирование должно повторяться и не должно зависеть от внешней среды. На данный момент мы можем справиться с этой ситуацией с реализацией Mock.

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

@MockBean

Когда SpringBoot выполняет модульные тесты, он заменяет аннотированный компонент собственным компонентом в контейнере IOC.

Например, в следующем коде ProjectService выполняет операции запросов к базе данных с помощью метода selectById класса ProjectMapper:

@Service
public class ProjectService {

    @Autowired
    private ProjectMapper mapper;

    public ProjectDO detail(String id) {
        return mapper.selectById(id);
    }

}

На этом этапе мы можем заменить собственный bean-компонент в контейнере IOC объектом ProjectMapper Mock для имитации операций запросов к базе данных, таких как:

@RunWith(SpringRunner.class)
@SpringBootTest
public class ProjectServiceTest {
  
    @MockBean
    private ProjectMapper mapper;
    @Autowired
    private ProjectService service;

    @Test
    public void detail() {
        ProjectDemoDO model = new ProjectDemoDO();
        model.setId("1");
        model.setName("dubbo-demo");
        Mockito.when(mapper.selectById("1")).thenReturn(model);
        ProjectDemoDO entity = service.detail("1");
        assertThat(entity.getName(), containsString("dubbo-demo"));
    }

}

Общий метод Мокито

Подробнее об использовании Mockito можно посмотреть→официальная документация

макет () объект
List list = mock(List.class);

verify() проверяет интерактивное поведение
@Test
public void mockTest() {
	List list = mock(List.class);
  list.add(1);
  // 验证 add(1) 互动行为是否发生
  Mockito.verify(list).add(1);
}

when() имитирует ожидаемый результат
@Test
public void mockTest() {
  List list = mock(List.class);
  when(mock.get(0)).thenReturn("hello");
  assertThat(mock.get(0),is("hello"));
}

doThrow() имитирует создание исключения
@Test(expected = RuntimeException.class)
public void mockTest(){
  List list = mock(List.class);
  doThrow(new RuntimeException()).when(list).add(1);
  list.add(1);
}

@Мок-аннотация

В приведенном выше тесте мы имеем в каждом методе тестированияmockобъект List, чтобы избежать повторенияmock, чтобы сделать тестовый класс более читабельным, мы можем использовать следующие аннотации, чтобы быстро имитировать объекты:

// @RunWith(MockitoJUnitRunner.class) 
public class MockitoTest {
    @Mock
    private List list;

    public MockitoTest(){
      	// 初始化 @Mock 注解
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void shorthand(){
        list.add(1);
        verify(list).add(1);
    }
}

когда () сопоставление параметров
@Test
public void mockTest(){
	Comparable comparable = mock(Comparable.class);
  //预设根据不同的参数返回不同的结果
  when(comparable.compareTo("Test")).thenReturn(1);
  when(comparable.compareTo("Omg")).thenReturn(2);
  assertThat(comparable.compareTo("Test"),is(1));
  assertThat(comparable.compareTo("Omg"),is(2));
  //对于没有预设的情况会返回默认值
   assertThat(list.get(1),is(999));
   assertThat(comparable.compareTo("Not stub"),is(0));
}

Ответ Изменить возвращает ожидание по умолчанию для неустановленных вызовов
@Test
public void mockTest(){
  //mock对象使用Answer来对未预设的调用返回默认期望值
  List list = mock(List.class,new Answer() {
    @Override
    public Object answer(InvocationOnMock invocation) throws Throwable {
      return 999;
    }
  });
  //下面的get(1)没有预设,通常情况下会返回NULL,但是使用了Answer改变了默认期望值
  assertThat(list.get(1),is(999));
  //下面的size()没有预设,通常情况下会返回0,但是使用了Answer改变了默认期望值
  assertThat(list.size(),is(999));
}

spy() отслеживает реальные объекты

Mock не является реальным объектом, он просто создает фиктивный объект и может задавать поведение объекта. А Шпион — это реальный объект, но он может задавать поведение объекта.

@Test(expected = IndexOutOfBoundsException.class)
public void mockTest(){
  List list = new LinkedList();
  List spy = spy(list);
  //下面预设的spy.get(0)会报错,因为会调用真实对象的get(0),所以会抛出越界异常
  when(spy.get(0)).thenReturn(3);
  //使用doReturn-when可以避免when-thenReturn调用真实对象api
  doReturn(999).when(spy).get(999);
  //预设size()期望值
  when(spy.size()).thenReturn(100);
  //调用真实对象的api
  spy.add(1);
  spy.add(2);
  assertThat(spy.size(),is(100));
  assertThat(spy.size(),is(1));
  assertThat(spy.size(),is(2));
  verify(spy).add(1);
  verify(spy).add(2);
  assertThat(spy.get(999),is(999));
}

reset() сбрасывает макет
@Test
public void reset_mock(){
  List list = mock(List.class);
  when(list.size()).thenReturn(10);
  list.add(1);
	assertThat(list.size(),is(10));
  //重置mock,清除所有的互动和预设
  reset(list);
  assertThat(list.size(),is(0));
}

times() проверить количество вызовов
@Test
public void verifying_number_of_invocations(){
  List list = mock(List.class);
  list.add(1);
  list.add(2);
  list.add(2);
  list.add(3);
  list.add(3);
  list.add(3);
  //验证是否被调用一次,等效于下面的times(1)
  verify(list).add(1);
  verify(list,times(1)).add(1);
  //验证是否被调用2次
  verify(list,times(2)).add(2);
  //验证是否被调用3次
  verify(list,times(3)).add(3);
  //验证是否从未被调用过
  verify(list,never()).add(4);
  //验证至少调用一次
  verify(list,atLeastOnce()).add(1);
  //验证至少调用2次
  verify(list,atLeast(2)).add(2);
  //验证至多调用3次
  verify(list,atMost(3)).add(3);
}

inOrder() проверяет порядок выполнения
@Test
public void verification_in_order(){
  List list = mock(List.class);
  List list2 = mock(List.class);
  list.add(1);
  list2.add("hello");
  list.add(2);
  list2.add("world");
  //将需要排序的mock对象放入InOrder
  InOrder inOrder = inOrder(list,list2);
  //下面的代码不能颠倒顺序,验证执行顺序
  inOrder.verify(list).add(1);
  inOrder.verify(list2).add("hello");
  inOrder.verify(list).add(2);
  inOrder.verify(list2).add("world");
}

verifyZeroInteractions() проверяет поведение с нулевым взаимодействием
 @Test
 public void mockTest(){
   List list = mock(List.class);
   List list2 = mock(List.class);
   List list3 = mock(List.class);
   list.add(1);
   verify(list).add(1);
   verify(list,never()).add(2);
   //验证零互动行为
   verifyZeroInteractions(list2,list3);
 }

verifyNoMoreInteractions() проверяет избыточные взаимодействия
@Test(expected = NoInteractionsWanted.class)
public void mockTest(){
  List list = mock(List.class);
  list.add(1);
  list.add(2);
  verify(list,times(2)).add(anyInt());
  //检查是否有未被验证的互动行为,因为add(1)和add(2)都会被上面的anyInt()验证到,所以下面的代码会通过
  verifyNoMoreInteractions(list);

  List list2 = mock(List.class);
  list2.add(1);
  list2.add(2);
  verify(list2).add(1);
  //检查是否有未被验证的互动行为,因为add(2)没有被验证,所以下面的代码会失败抛出异常
  verifyNoMoreInteractions(list2);
}