Разъяснение с примерами для новичков

Модульное тестирование - это часть работы каждого разработчика, который работает в компании или пишет свой собственный код. В большинстве случаев новые Java-программисты начинают учиться писать тесты JUnit, что является еще одной базой для Java-разработчиков. Однако знать только JUnit недостаточно. Моки являются важной частью модульного тестирования и широко используются многими разработчиками.

Одна из самых популярных библиотек Java для работы с mockito - это Mockito.

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

В этой статье я объясню случаи, когда разработчику стоит использовать Mockito. Также я приведу несколько примеров кода.

Зачем нужны моки?

Концепция модульного тестирования заключается в тестировании только одного класса / модуля в тестовом классе. Однако в большинстве случаев у этого класса есть несколько других зависимостей. Например, класс обслуживания может иметь зависимости от других классов: репозиториев, помощников и т. Д. Класс обслуживания вызывает код из других классов, но мы хотим протестировать только класс this-service, а не все остальные, которые зависимы. Боковая реализация в других классах не должна интересовать нас при написании модульного теста. И чтобы избежать побочного тестирования, мы должны использовать моки. Если бы макеты не использовались для зависимостей классов, нам пришлось бы создавать реальные объекты, и такое тестирование переросло бы в интеграционные тесты, которые не являются частью модульного тестирования.

Замена зависимости и фиктивная реализация такой зависимости называется макетом.

Практические примеры

В качестве примера кода я использую Spring Boot с Maven. Первое, что нужно сделать, это добавить необходимую зависимость в файл pom:

<dependency>
   <groupId>org.mockito</groupId>
   <artifactId>mockito-junit-jupiter</artifactId>
   <version>3.3.3</version>
   <scope>test</scope>
</dependency>

Я собираюсь протестировать класс BookService, который имеет только один метод с несколькими зависимостями:

@Component
public class BookService {

    private static final Logger LOGGER = Logger.getLogger("InfoLoggin");
@Autowired
    private BookRepo bookRepo;
@Autowired
    private VoiceOverAgencyRepository agencyRepo;

    public Book saveBook(Book book) throws CustomException {

        //1. Checking if this book already exists in database.
        if (bookRepo.existsByIbanId(book.getIbanId())) {
            throw new CustomException();
        }

        //2. Setting registration date of a new book.
        book.setRegistrationDateTime(LocalDateTime.now());

        //3. Checking if this book has an audio version.
        //If not, we register it in agency's DB too.
        if (book.isAudioVersion()) {
            LOGGER.info(book.getBookName() + ": audio version       exists.");
        } else {
            agencyRepo.registerBookForAudio(book.getIbanId());
        }
        //4. Saving into the database.
        return bookRepo.save(book);
    }
}

Этот метод получает объект книги и проверяет:

а) Если такая книга уже существует в базе данных. Если книга существует, генерируется специальное исключение.

б) Если книги еще нет в базе данных, устанавливается дата и время регистрации.

в) Проверяет, есть ли у книги аудиоверсия. Если у него нет аудиоверсии, он регистрирует эту книгу в репозитории голосового агентства.

г) Наконец, он регистрирует эту книгу в хранилище книг.

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

Пойдем шаг за шагом

  1. Мне нужно создать тестовый класс и добавить все зависимости.
    a) Мой тестовый класс получает аннотацию @ExtendWith (MockitoExtensionc.class).
    Эта аннотация содержит все макеты. в тестовом классе.
    б) Я издеваюсь над всеми зависимостями с помощью аннотации @Mock и вставляю эти макеты в сервисный класс, который я хочу протестировать, используя аннотацию @InjectMocks :
@ExtendWith(MockitoExtension.class)
public class TestBookService {

    @Mock
    private Book book;

    @Mock
    private BookRepo bookRepo;

    @Mock
    private VoiceOverAgencyRepository agencyRepo;

    @InjectMocks
    private BookService bookService;
...

Как видите, я высмеял все зависимости, которые использовались с аннотацией @Autowired в классе BookService. Я также издевался над книгой, которую использую в качестве параметра для своего метода в классе BookService. Я смогу манипулировать этими объектами, как захочу, и мне не нужно будет создавать настоящие объекты.

2. Вернемся к классу BookService.

Здесь есть только один метод, и я буду тестировать его шаг за шагом. Это означает, что я хочу создать несколько разных методов тестирования, которые будут проверять различные действия моего метода.

Первая пара строк моего метода проверяет, находится ли книга уже в репозитории книг. И если он существует, он генерирует настраиваемое исключение:

public Book saveBook(Book book) throws CustomException {

        //1. Checking if this book already exists in database.
        if (bookRepo.existsByIbanId(book.getIbanId())) {
            throw new CustomException();
        }

Как я могу это проверить? Я должен сказать своему тест-классу, что эта книга уже существует в репозитории, поэтому она вызовет исключение. Я могу сделать это с помощью методов Mockito , когда…. thenReturn… Я передаю переменную Mockito any (), потому что она не обязательно должна быть реальным объектом - единственное, что меня волнует, - это вызов моего исключения. Поэтому я говорю Mockito: когда вы дойдете до строки кода, в которой написано bookRepo.existsByIbanId (any ()), затем верните true . Теперь я могу проверить, действительно ли моя программа выдает исключение. Проверить это:

3. Вот вторая часть класса обслуживания:

//2. Setting registration date of a new book.
        book.setRegistrationDateTime(LocalDateTime.now());

Мой тест должен проверить, действительно ли класс обслуживания выполняет эту строку. Я хочу быть уверенным, что мой код где-то не закончился, не дойдя до него.

Для этого я могу использовать метод Mockito verify (что что-то произошло).

Перед вызовом этого метода я должен дать тестовый класс инструкции:

а) когда мой код достигает 1-й строки, которая проверяет, существует ли уже эта книга, я должен сообщить своему коду, что эта (фиктивная) книга еще не существует (иначе мой код вызовет исключение).

б) после этого мой код дойдет до строки, которую я хочу протестировать. Однако этого недостаточно. Я должен написать инструкции для моего метода тестирования и что он должен делать, когда достигает следующих строк метода:

4. Перейдем к следующей части метода:

//3. Checking if this book has an audio version.
// If not, we register it in agency's DB too.
        if (book.isAudioVersion()) {
            LOGGER.info(book.getBookName() + ": audio version exists.");
        } else {
            agencyRepo.registerBookForAudio(book.getIbanId());
        }

Я хочу быть уверенным, что мой код действительно регистрирует книгу в репозитории агентства, если у нее нет аудиоверсии. В этом случае я скажу своему макету, что у книги нет аудиоверсии.

5. И наоборот: если аудиоверсия уже существует, я хочу быть уверенным, что мой код никогда не регистрирует эту книгу для агентства. Я могу сказать своей функции verify, что эту строку никогда не следует вызывать:

6. Наконец, последняя часть класса BookService:

//4. Saving into the database.
        return bookRepo.save(book);
    }
}

Я хочу быть уверен, что он сохранит правильный объект в базе данных.

Для этого я создам реальный объект книги и сохраню данные, которые я сохраняю в хранилище книг.

ArgumentCaptor разрешает доступ к аргументам вызовов методов и проверяет их. Он работает вместе с методом verify (..). Я легко могу это инициировать:

ArgumentCaptor ‹ObjectThatIWantToCapture› captor = ArgumentCaptor.forClass (ObjectThatIWantToCapture.class);

Давайте посмотрим:

Теперь вы узнали, как писать модульные тесты с использованием Mockito. Мои примеры включают наиболее часто используемые методы Mockito, которые вы можете использовать для своего кода:

  • when (будет вызван какой-то метод) .thenReturn (something);
  • проверить (что этот метод будет вызываться, x раз);
  • проверить (никогда не будет вызван этот метод);
  • проверить (что при вызове этого метода)… (захват фиксирует аргументы, которые я хочу проверить).

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

Более подробную информацию о Mockito можно найти на официальном сайте документации.