В этой статье мы воспользуемся плагином на базе искусственного интеллекта для IntelliJ, чтобы автоматически сгенерировать юнит-тесты.
Шаг 1. Установка плагина и импорт проекта
Плагин для IntelliJ IDEA полностью бесплатен для разработки программ с открытым исходным кодом. Чтобы установить его, перейдите в меню “Настройки” (Preferences) IntelliJ IDEA, в его левой части выберите Плагины (Plugins) и найдите через поиск Diffblue Cover, как показано ниже.
После завершения установки и при наличии существующего проекта, который импортирован в IntelliJ IDEA, плагин начнет индексировать и анализировать всю кодовую базу (т. е. классы, зависимости и т. д.) в качестве фоновой задачи. Это может занять некоторое время в зависимости от её размера и сложности.
Вы также можете дополнительно настроить плагин Diffblue Cover через меню IntelliJ IDEA, чтобы подогнать его под свои нужды.
Если вам хочется поэкспериментировать, рекомендую начать с базового проекта. Ниже приведен пример проекта Spring Boot с контроллером, уровнем обслуживания, репозиторием, использующим данные spring, и резидентной (In-Memory)базой данных:
git clone https://github.com/rhamedy/sample-rest-service.git
Можете спокойно клонировать репозиторий и делать с ним, что угодно.
Шаг 2. Краткое руководство по проекту
Если вы клонировали проект-пример, приведенный выше, то этот раздел для вас. Здесь я кратко покажу классы, для которых мы собираемся создавать юнит-тесты.
Слой контроллера
Вот как выглядит контроллер Spring Boot, который мы собираемся протестировать:
/**
* Пример контроллера (Sample Controller), который предоставляет множество конечных точек, разрешающих такие операции, как
* CREATE, READ, DELETE и LIST относительно пользователей (users).
*
* @author Rafiullah Hamedy
*/
package com.sampleservice.demo.controller;
import java.util.*;
import com.sampleservice.demo.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.sampleservice.demo.dto.inbound.StudentInDTO;
import com.sampleservice.demo.dto.outbound.StudentOutDTO;
import com.sampleservice.demo.model.Student;
@RestController
@RequestMapping("/students")
public class StudentController {
@Autowired
private StudentService studentService;
public StudentController() {}
@GetMapping("list")
public List<StudentOutDTO> list() {
Collection<Student> studentsIterable = studentService.list();
List<StudentOutDTO> outDTOs = new ArrayList<>();
studentsIterable.forEach(entry -> outDTOs.add(new StudentOutDTO(entry)));
return outDTOs;
}
@GetMapping("{sid}")
public StudentOutDTO getById(@PathVariable("sid") Long id) {
Student student = studentService.findById(id);
return new StudentOutDTO(student);
}
@PostMapping
public StudentOutDTO save(@RequestBody StudentInDTO dto) {
Student student = dto.toEntity();
studentService.saveOrUpdate(student);
return new StudentOutDTO(student);
}
@DeleteMapping("{sid}")
public ResponseEntity<?> delete(@PathVariable("sid") Long id) {
Student existingStudent = studentService.findById(id);
studentService.delete(existingStudent);
return new ResponseEntity<>(HttpStatus.OK);
}
}
Как показано выше, у нас есть несколько конечных точек для получения списка всех студентов, а также добавления, получения и удаления по идентификатору каждого в отдельности.
Сервисный слой
Слой сервиса соединяет контроллер со слоем репозитория, где будет находиться большая часть логики.
package com.sampleservice.demo.service;
import com.sampleservice.demo.validator.StudentValidator;
import com.sampleservice.demo.dao.StudentDAO;
import com.sampleservice.demo.model.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
private StudentDAO studentDAO;
@Autowired
private StudentValidator studentValidator;
@Transactional(readOnly = true)
@Override
public Collection<Student> list() {
return StreamSupport.stream(studentDAO.findAll().spliterator(), false)
.collect(Collectors.toSet());
}
@Transactional(readOnly = true)
@Override
public Student findByFirstName(String firstName) {
Optional<Student> student = studentDAO.findByFirstNameLike(firstName);
studentValidator.validate404(student, "First Name", firstName);
return student.get();
}
@Transactional(readOnly = true)
@Override
public Student findById(Long id) {
Optional<Student> student = studentDAO.findById(id);
studentValidator.validate404(student, "id", String.valueOf(id));
return student.get();
}
@Transactional(readOnly = true)
@Override
public Student findByEmail(String email) {
Optional<Student> student = studentDAO.findByFirstNameLike(email);
studentValidator.validate404(student, "email", email);
return student.get();
}
@Transactional(rollbackFor = Exception.class)
@Override
public void delete(Student student) {
studentDAO.delete(student);
}
@Transactional(rollbackFor = Exception.class)
@Override
public Student saveOrUpdate(Student student) {
return studentDAO.save(student);
}
@Transactional(rollbackFor = Exception.class)
@Override
public Collection<Student> saveAll(List<Student> students) {
return StreamSupport.stream(studentDAO.saveAll(students).spliterator(), false)
.collect(Collectors.toSet());
}
}
Дальше мы попытаемся сгенерировать модульные тесты для вышеперечисленных классов с помощью плагина Diffblue Cover.
Шаг 3. Генерация юнит-тестов при помощи плагина, основанного на искусственном интеллекте
Именно здесь происходит вся магия. Другими словами, плагин, работающий на основе ИИ, вступает в дело и самостоятельно, без какой-либо помощи, генерирует юнит-тесты.
Если вы уже установили плагин, щелкните правой кнопкой мыши по StudentController
и выберите опцию “Написать тесты” (Write Tests) в меню действий, как показано выше.
Плагин начнет анализировать контроллер и каждый из его методов. Наконец, он сгенерирует юнит-тесты для каждого из методов.
Генерация тестов занимает какое-то время — Diffblue Cover анализирует класс и его методы. Но затраты времени минимальны по сравнению с тем, сколько понадобится для написания вручную.
Ниже приведен тест StudentControllerTest
, сгенерированный Diffblue Cover.
package com.sampleservice.demo.controller;
import static org.mockito.AdditionalMatchers.or;
import static org.mockito.Mockito.isA;
import static org.mockito.Mockito.isNull;
import static org.mockito.Mockito.when;
import com.sampleservice.demo.dto.inbound.StudentInDTO;
import com.sampleservice.demo.model.Student;
import com.sampleservice.demo.service.StudentServiceImpl;
import java.util.ArrayList;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
/**
* Эти тесты НЕ написаны человеком.
*/
@WebMvcTest(controllers = {StudentController.class})
@RunWith(SpringRunner.class)
public class StudentControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private StudentServiceImpl studentServiceImpl;
@Test
public void testGetById() throws Exception {
Student student = new Student();
student.setLastName("Doe");
student.setEmail("email");
student.setKlass(1);
student.setId(123L);
student.setFirstName("Jane");
when(this.studentServiceImpl.findById(or(isA(Long.class), isNull()))).thenReturn(student);
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/students/{sid}", 1L);
ResultActions actualPerformResult = this.mockMvc.perform(requestBuilder);
ResultActions resultActions = actualPerformResult.andExpect(MockMvcResultMatchers.status().isOk());
ResultActions resultActions1 = resultActions
.andExpect(MockMvcResultMatchers.content().contentType("application/json;charset=UTF-8"));
Matcher<String> matcher = Matchers
.containsString("{\"id\":123,\"firstName\":\"Jane\",\"lastName\":\"Doe\",\"klass\":1}");
resultActions1.andExpect(MockMvcResultMatchers.content().string(matcher));
}
@Test
public void testList() throws Exception {
Student student = new Student();
student.setLastName("Doe");
student.setEmail("favicon.ico");
student.setKlass(0);
student.setId(123L);
student.setFirstName("Jane");
ArrayList<Student> studentList = new ArrayList<Student>();
studentList.add(student);
when(this.studentServiceImpl.list()).thenReturn(studentList);
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/students/list");
ResultActions actualPerformResult = this.mockMvc.perform(requestBuilder);
ResultActions resultActions = actualPerformResult.andExpect(MockMvcResultMatchers.status().isOk());
ResultActions resultActions1 = resultActions
.andExpect(MockMvcResultMatchers.content().contentType("application/json;charset=UTF-8"));
Matcher<String> matcher = Matchers
.containsString("[{\"id\":123,\"firstName\":\"Jane\",\"lastName\":\"Doe\",\"klass\":0}]");
resultActions1.andExpect(MockMvcResultMatchers.content().string(matcher));
}
@Test
public void testList2() throws Exception {
when(this.studentServiceImpl.list()).thenReturn(new ArrayList<Student>());
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/students/list");
ResultActions actualPerformResult = this.mockMvc.perform(requestBuilder);
ResultActions resultActions = actualPerformResult.andExpect(MockMvcResultMatchers.status().isOk());
ResultActions resultActions1 = resultActions
.andExpect(MockMvcResultMatchers.content().contentType("application/json;charset=UTF-8"));
Matcher<String> matcher = Matchers.containsString("[]");
resultActions1.andExpect(MockMvcResultMatchers.content().string(matcher));
}
@Test
public void testList3() throws Exception {
Student student = new Student();
student.setLastName("Doe");
student.setEmail("favicon.ico");
student.setKlass(0);
student.setId(123L);
student.setFirstName("Jane");
ArrayList<Student> studentList = new ArrayList<Student>();
studentList.add(student);
Student student1 = new Student();
student1.setLastName("Doe");
student1.setEmail("favicon.ico");
student1.setKlass(0);
student1.setId(123L);
student1.setFirstName("Jane");
studentList.add(student1);
when(this.studentServiceImpl.list()).thenReturn(studentList);
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/students/list");
ResultActions actualPerformResult = this.mockMvc.perform(requestBuilder);
ResultActions resultActions = actualPerformResult.andExpect(MockMvcResultMatchers.status().isOk());
ResultActions resultActions1 = resultActions
.andExpect(MockMvcResultMatchers.content().contentType("application/json;charset=UTF-8"));
Matcher<String> matcher = Matchers.containsString(
"[{\"id\":123,\"firstName\":\"Jane\",\"lastName\":\"Doe\",\"klass\":0},{\"id\":123,\"firstName\":\"Jane\",\"lastName\":\"Doe"
+ "\",\"klass\":0}]");
resultActions1.andExpect(MockMvcResultMatchers.content().string(matcher));
}
@Test
public void testDelete() throws Exception {
Student student = new Student();
student.setLastName("Doe");
student.setEmail("email");
student.setKlass(1);
student.setId(123L);
student.setFirstName("Jane");
when(this.studentServiceImpl.findById(or(isA(Long.class), isNull()))).thenReturn(student);
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.delete("/students/{sid}", 1L);
ResultActions actualPerformResult = this.mockMvc.perform(requestBuilder);
actualPerformResult.andExpect(MockMvcResultMatchers.status().isOk());
}
@Test
public void testSave() throws Exception {
MockHttpServletRequestBuilder getResult = MockMvcRequestBuilders.get("/students");
ResultActions actualPerformResult = this.mockMvc
.perform(getResult.param("dto", String.valueOf(new StudentInDTO())));
actualPerformResult.andExpect(MockMvcResultMatchers.status().is(405));
}
}
Тесты выглядят очень надежными и следуют лучшим практикам Spring Boot относительно тестирования.
Все сгенерированные юнит-тесты целиком можно найти в этом пулл-реквесте.
Шаг 4. Тестовое покрытие сгенерированных юнит-тестов
Инструмент покрытия кода или тестового покрытия показывает процент кода, охваченного тестами.
Запустим его на сгенерированных ранее тестах, чтобы получить представление о том, какой процент кода покрывается этими тестами.
Мы получили довольно хороший процент покрытия по классам, для которых были сгенерированы тесты, то есть по пакетам Controller
, service
и util
.
Стоит отметить, что это крайне простая кодовая база без сложного кода или логики, и плагин, похоже, прилично справляется с работой по созданию ряда надежных модульных тестов. В случае устаревшего кода или запутанной базы плагин может вести себя по-другому.
Демонстрация по созданию юнит-тестов с плагином Diffblue Cover
Для тех, кому интересно, я приготовил также быструю видео-демонстрацию процесса: начиная от установки плагина до генерации тестов и оценки покрытия для сгенерированных тестов.
Быстрое демо по генерации как юнит-тестов на Java с помощью Diffblue Cover
Недостатки автоматизированной генерации юнит-тестов
Несмотря на многие преимущества, которые дает генерация юнит-тестов, у нее есть несколько недостатков, которые я хотел бы вкратце затронуть.
Во-первых, я полагаю, что разработчик, который пишет юнит-тесты, лучше, чем разработчик, который этого не делает.
Во-вторых, на мой взгляд, юнит-тестирование косвенно влияет на то, как мы пишем код. Если мы пишем код, который плохо спроектирован, то модульное тестирование становится еще более неприятным процессом. К примеру, представим метод, который делает десять разных вещей, и к тому же в нем содержится много вложенных условий и довольно сложная логика. Было бы непросто написать тесты, которые охватывают все пути выполнения таких методов.
Однажды, когда я приводил аргументы в пользу юнит-тестирования, один разработчик сказал мне:
Я трачу 20% своего времени на написание кода и 80% на написание юнит-тестов для этого кода.
Если код, который вы пишете, нелегко читать и понимать, он сильно связан и мало сопряжен, то модульное тестирование окажется очень трудоемким и неэффективным процессом.
Заключение
В целом я остался доволен работой плагина и качеством сгенерированных юнит-тестов для проекта-образца.
Однако я заметил, что плагин не может генерировать тесты для определенных классов и в некоторых случаях пропускает пути выполнения (ветви). В видео-примере выше есть дополнительная информация на этот счет.
Читайте также:
- Графовое моделирование данных на Java
- Микрооптимизации в Java. Enum - хороший, красивый и медленный
- 5 основных фреймворков для Java-разработчиков
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Rafiullah Hamedy: Use an AI Tool to Write Your Unit Tests