Статьи

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

Мы прилагаем большие усилия, чтобы создать систему или платформу с тысячами строк кода и циклов тестирования, чтобы обеспечить качество и охват. Поскольку валидация QA уже является частью цикла, модульное тестирование также должно рассматриваться как важный предварительный шаг перед превращением кода в сборку и развертывание. Таким образом, убедиться, что сборка успешна локально, и достичь ожидаемого покрытия кода.

TDD (Test Driven Development) — эффективный способ разработки системы путем постепенного добавления кода и написания тестов. Модульные тесты могут быть написаны с использованием платформы Mockito. 

В этой статье мы рассмотрим несколько областей написания модульных тестов с использованием инфраструктуры Mockito. Мы используем Java для фрагментов кода здесь.

Что значит издеваться?

Насмешка — это способ создания фиктивных объектов, операций и результатов, как если бы они были реальными сценариями. Это означает, что он не имеет никаких реальных подключений к базе данных и реального сервера и работает. Тем не менее, он имитирует их так, что линии также покрыты и ожидают фактический результат. Таким образом, его можно сравнить с ожидаемым результатом и утверждать.

Mockito — это фреймворк, который облегчает насмешки в тестах. Объекты Mockito являются своего рода прокси-объектами, которые работают на операциях, серверах и соединениях с базой данных. Мы будем использовать пустышку, подделку и заглушку, где это применимо, для насмешек.

Мы будем использовать JUnit с платформой Mockito для наших модульных тестов.

Шагая в издевательство

У нас есть композиция в классе Java: StockPlan.java

public class Account {
 public List < String > getPlans(String accountId) { ...
 }
}
public class StockPlan {
 private Account account;
 public StockPlan(Account account) {
  this.account = account;
 }
 public List < String > getPlans(String accountId)
 return account.getPlans(accountId);
}
//....
}

Теперь мы увидим тест для класса StockPlan:

...
import org.junit.Before;
import org.junit.Test;
import org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.ArgumentMatchers.any;
public class StockPlanTest {
 Account account;
 StockPlan stockPlan;
 @Before
 public void setUp() {
  account = mock(Account.class);
  stockPlan = new StockPlan(account):
 }
  
 @Test
 public void verify_account_plans_returned_successfully() {
  List < String > planNames = Arrays.asList("plan123"); //A
  when(account.getPlans(any(String.class)).thenReturn(planNames); //B
   List < String > resultPlans = stockPlan.getPlans("account123"); //C
   assertEquals(resultPlans, planNames); //D
  }
 }

Более глубокое понимание Кодекса

В строке A мы создаем ожидаемый список имен планов, которые должен вернуть объект Account.

В строке B мы смоделируем (фальсифицируем) вызов getPlans в Account. Таким образом, когда он пытается вызвать метод, он возвращает ожидаемый список имен планов.

В строке C мы делаем фактический вызов объекта StockPlan, чтобы получить имена планов. Он вызовет  getPlans в классе Account и вернет ожидаемый список имен планов (строка A).

В строке D мы сравниваем список планов и проверяем, совпадают ли они.

Методы насмешек с шпионом

На том же уровне класса, если метод вызывает другой, который является недействительным, мы высмеиваем вызов, создав «шпиона» для исходного объекта. Например:

public class StockPlan {
 Account account;
 public StockPlan(Account account) {
  this.account = account;
 }
 public Boolean updateAccount(Account accountRecord) {
  validate(accountRecord);
  ....
  //save to db. Once successful, return true.
  return true;
 }
 public void validate(Account account) {
  //validate account fields...
 }
 //....
}

Тест будет выглядеть так:

@Test
public void verify_account_updated successfully() {
 Account account = mock(Account.class);
 StockPlan stockPlan = new StockPlan(account);
 StockPlan spyPlan = spy(stockPlan);
 doNothing().when(spyPlan).validate(account);
 Boolean result = spyPlan.updateAccount(account);
 assertTrue(result);
}

Насмехаясь над брошенным исключением и ожидая его в тесте

Предположим, что реальный метод генерирует исключение, и нам нужно его протестировать. Мы можем сделать это путем фальсификации, как будто выдается исключение и ожидать каких-либо пользовательских исключений в тесте Например, у нас есть наш класс исключения WriteDBException :

public WriteDBException extends RunTimeException {
 public WriteDBException(String msg) {
  super(msg);
 }
}

Наш класс StockPlan модифицирован аннотацией lombok и обработкой исключений:

@Slf4j
public class StockPlan {
 Account account;
 DBRepository repository;
 public StockPlan(Account account, DBRepository repository) {
  this.account = account;
  this.repository = repository;
 }
 public Boolean updateAccount(Account account) throws WriteDBException {
  try {
   repository.update(account, "account_table_name");
  } catch (Exception e) {
   log.error("Failure in updating Account record with message={}", e.getMessage());
   throw new WriteDBException(e.getMessage());
  }
  //.....
 }
}

Тогда тест будет выглядеть так:

@Test(expected = WriteDBException.class)
public void verify_exception_thrown_when_updating_account_record_in_db() {
  Account account = mock(Account.class);
  DBRepository repository = mock(DBRepository.class);
  StockPlan stockPlan = new StockPlan(account, repository);
  when(repository.update(account, any(String.class))).thenThrow(new RunTimeException("FAILURE));
     //this would throw WriteDBException and log message would also be printed out
     stockPlan.updateAccount(account);
    }

Интерфейсные интерфейсы REST Controller

Мы увидим, как мы можем имитировать вызовы REST, например, метод read.

Скажем, в контроллере есть метод чтения (созданный с помощью Spring MVC с RequestMapping),

@RestController
@RequestMapping("/stockplans")
public class StockPlanController {
 .......
 @RequestMapping(value = "/stocks/{stockId}/", method = RequestMethod.GET) public ResponseEntity < List < StockPlan >> read(
  @PathVariable String stockId,
  @RequestParam(value = "planId", ) String planId) {
  .....
  //read list from service --> database
  ResponseEntity < List < StockPlan >> stockPlanList = createStockPlanList(......);
  return stockPlanList;
 }

Этот метод чтения возвращает список объектов StockPlan в формате JSON. Теперь мы пишем наш тест.

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
public class StockPlanControllerTest {
 @Autowired
 public WebApplicationContext context;
 public MockMvc mockMvc;
 @Before
 public void setUp() {
  mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
 }
 @Test
 public void verify_stock_plans_read_successfully() {
   mockMvc.perform(get("/stockplans/stocks/ABC/")) //line A
    .param("planId", "plan123") //line B
    .andExpect(status().isOk()) //line C
    .andExpect(content().contentType("application/json;-8)) //line D
     }
    }

В строке A мы смоделируем вызов API чтения , указав конечную точку со значением для параметра (динамически). Таким образом, значение ABC для stockId в параметре пути. Это обязательно, в противном случае конечная точка недействительна.

В строке B мы предоставляем значение для RequestParamplanId.

В строке C мы ожидаем, что статус будет возвращен как ОК, разместив эти значения в URL (конечная точка + значения). Если вызов успешен, он вернет «ОК». В противном случае он вернет любой код ошибки HTTP.

Строка D говорит нам, что ожидаемый контент должен быть JSON.

Это самая короткая версия тестирования вызовов чтения REST. Обратите внимание, что мы использовали несколько новых классов для нашего теста. Следующие классы должны быть импортированы в тест.

import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

Аналогичным образом мы можем протестировать и  операцию создания в контроллере.

public class StockPlanController {
 ....

 @RequestMapping(value = "/", method = RequestMethod.POST)
 public ResponseEntity < StockPlanResponse > create(
  @RequestBody @Valid StockPlan stockPlan, HttpServletRequest request,
  HttpServletResponse response) {

  //validate and send to service for saving the StockPlan into database
  //create StockPlanResponse and return....
 }

Тест будет выглядеть так:

mockMvc.perform(post("/stockplans/")
  .contentType(MediaType.APPLICATION_JSON)
  .content(stockPlan_object_converted_to_string)
  .andExpect(status().isOk());

Тестирование исключений контроллера с MvcResult

Предположим, что  API чтения в контроллере выдает исключение (например, NoDataFoundException ). Затем мы можем зафиксировать действия результата и убедиться, что выброшенное исключение — NoDataFoundException :

ResultActions actions = mockMvc.perform(get(....)).andExpect(status().isNotFound())....

MvcResult result = actions.andReturn();
assertThat(result.getResolvedException().getClass(), typeCompatibleWith(NoDataFoundException.class));

Важно отметить, что исключение NoDataFoundException является настраиваемым исключением, и оно должно быть выброшено из вашего глобального обработчика исключений, который имеет тип ControllerAdvice.

Разработка юнит-тестов — лучший способ валидации разработчика.