Вступление
Пожалуйста, обратите внимание: Следующая статья будет посвящена тестированию приложений Spring Boot. Предполагается, что вы знакомы, по крайней мере, с основами Java, Maven и Spring Boot (контроллеры, зависимости, Репозиторий баз данных и т. Д.).
В большинстве организаций в целом отсутствует тестирование. Возможно, даже ваша команда-одна из тех команд, у которых есть хорошие намерения в отношении тестирования, но это всегда откладывается или забывается по мере продвижения проектов.
Почему тестирование так сложно проводить последовательно? Преимущества тестирования хорошо известны, и все же, почему это так часто упускается из виду?
я думаю, что есть несколько причин, по которым тестирование имеет меньшее значение для большинства команд. Во-первых, создание, интеграция и поддержка тестов часто могут быть сложными. И во-вторых, если вы не инженер, который много тестировал и видел его важность и ценность, вы, вероятно, не поставите его на первое место в своем списке приоритетов, чтобы учиться и участвовать в процессе разработки.
К счастью, Spring Boot делает интеграцию и работу с тестами проще, чем когда-либо прежде.
Начало работы с тестированием весенней загрузки
Когда дело доходит до тестирования, существует несколько различных типов тестов, которые вы можете написать, чтобы помочь протестировать и автоматизировать работоспособность вашего приложения. Однако, прежде чем мы сможем начать какое-либо тестирование, нам необходимо интегрировать платформы тестирования.
С Spring Boot это означает, что нам нужно добавить стартер в зависимости нашего проекта, для тестирования нам нужно только добавить spring-boot-starter-test
зависимость:
org.springframework.boot spring-boot-starter-test {version} test
Эта единственная зависимость позволит вам настроить большинство ваших потребностей в тестировании.
Джунит и Хэмкрест
Первая платформа, которую интегрирует test starter, – это JUnit .
JUnit существует уже давно, и если вы когда-либо проводили модульное тестирование на Java, вы, скорее всего, использовали эту платформу раньше. При проведении базового модульного тестирования JUnit и Spring хорошо дополняют друг друга, как вы увидите в некоторых предстоящих демонстрациях. Несмотря на то, что JUnit предоставляет некоторую поддержку утверждений для анализа результатов тестов, Spring Boot также включает в себя Hamcrest . Эта платформа обеспечивает улучшенное сопоставление результатов тестирования и утверждения, которые в сочетании с JUnit позволяют автоматизировать тестирование от начала до конца.
Мокито
Следующая платформа, которую интегрирует test starter, – это Mockito . Иногда при тестировании код, который вы пытаетесь протестировать, является зависимостью для другого объекта. Иногда это просто фрагмент кода, который трудно использовать для модульного теста. В таких случаях использование фреймворка, такого как Mockito, для макетирования и заглушения этих объектов является решением. Таким образом, вы можете продолжить свои тесты, а затем проверить, что было вызвано и использовано для этого объекта после запуска теста.
Пружинные Инструменты
Наконец, зависимость запуска теста включает инструменты весеннего тестирования.
Они включают аннотации, утилиты тестирования и другую поддержку интеграции тестирования, которая позволяет работать с JUnit, Hamcrest и Mockito в среде Spring.
Запуск проекта Весенней Загрузки
В остальной части этой статьи мы будем настраивать и работать с различными аспектами тестирования в нашем приложении Spring Boot.
В этом разделе мы собираемся настроить наше приложение и среду для тестирования. Первое, что должно произойти, – это добавить spring-boot-starter-test
в зависимости нашего проекта.
Только после его добавления мы сможем создать простой модульный тест, чтобы увидеть, как работают основы. После этого мы рассмотрим несколько различных способов, с помощью которых вы можете запускать тесты в Spring Boot.
Вы можете либо создать проект Spring Boot с помощью своей среды IDE, либо сгенерировать его с помощью Spring Initializr .
В обоих случаях добавьте зависимость web
, которая включает в себя зависимость test-starter
в вашем проекте, в противном случае вам придется добавить ее вручную:
pom.xml:
org.springframework.boot spring-boot-starter-test test
При добавлении его вручную, добавьте его в нижнюю часть pom.xml
файл заставит Maven извлекать все зависимости от ваших тестовых ресурсов.
Одна вещь, которую следует отметить в отношении этой зависимости, заключается в том, что она включает область тестирования <область>тест
. Это означает, что при комплектации и упаковке приложения для развертывания любые зависимости, объявленные в области тестирования, игнорируются. Зависимости области тестирования доступны только при запуске в режимах разработки и тестирования Maven.
Теперь, когда у нас есть наши библиотеки тестирования, мы можем продолжить и создать тест.
Тестирование JUnit
Это наиболее распространенная практика, когда весь код, связанный с тестированием, помещается в папку src/test/java
. Архетип Maven, создавший проект, изначально включал тестовый класс, называемый, например, DemoApplicationTests
– на основе имени вашего основного класса, в том самом пакете.
Теперь нам просто нужно что-то проверить.
Давайте определим простой контроллер в нашей папке src/main/java
:
Домашний контролер:
@RestController public class HomeController { @RequestMapping("/") public String home() { return "Hello World!"; } }
Этот контроллер имеет один метод, возвращающий строку, которая запускается, когда мы обращаемся к корню нашего приложения. Такое поведение ожидается от этого контроллера, но давайте проверим его и посмотрим, правильно ли он себя ведет:
Тест контроллера JUnit:
public class JUnitControllerTest { @Test public void testHomeController() { HomeController homeController = new HomeController(); String result = homeController.home(); assertEquals(result, "Hello World!"); } }
assertEquals
– это статический метод, который из org.junit.Пакет Assert
и только один из методов assertion
, используемых в JUnit:
Требования к утверждениям | Проверяет, равны ли два примитивных типа или объекта. |
Утверждение верно | Проверяет, верно ли условие ввода. |
Фальшивый | Проверяет, является ли условие ввода ложным. |
Заверяю, что нет. | Проверяет, не является ли объект нулевым. |
ассертНулл | Проверяет, является ли объект нулевым. |
Утверждения же | Проверяет, указывают ли две ссылки на объекты на один и тот же объект в памяти. |
Утверждение не совпадает | Проверяет, не указывают ли две ссылки на объекты на один и тот же объект в памяти. |
assertArrayEquals | Проверяет, равны ли два массива друг другу. |
Мы начинаем наш тест с создания экземпляра нашего HomeController
. Для этого нет необходимости полагаться на инъекцию зависимостей. Мы используем метод assertEquals
, чтобы проверить, соответствует ли возвращаемое значение из нашего метода другой строке.
Это простой, но функциональный и завершенный модульный тест. Мы интегрировали платформы тестирования, создали тест JUnit, пометив метод аннотацией @Test
, после чего выполнили утверждение теста.
Теперь мы должны запустить тест и наблюдать за результатом – и существует несколько способов запуска тестов:
Первый способ-просто щелкнуть правой кнопкой мыши по всему тесту или по имени теста, если вы хотите запустить один тест. После этого выберите “Запуск от имени JUnit”. Это приведет к началу тестирования в вашей среде IDE:
Если бы мы изменили наш тест, и строки больше не совпадали, нам был бы предложен другой результат:
Другой способ, которым вы можете запускать тесты проекта, – это из командной строки или терминала. Если у вас есть Maven, настроенный в командной строке, и вы работаете с Maven, вы можете запустить команду Maven test из корневого каталога вашего проекта, чтобы получить те же результаты:
$ mvn test
Тестирование Mockito
Предыдущий тест, который мы создали, был очень простым. Он вернул некоторый статический текст, и поэтому его было довольно легко проверить.
По мере усложнения приложений вы не можете просто тестировать модули таким образом. Код, который вы тестируете, может зависеть от другого кода, управляемого Spring, или его трудно создать с помощью теста. В этом случае мы можем использовать Mockito , чтобы помочь нам протестировать.
Давайте создадим новый тестовый класс внутри src/test/java
:
@RestController @RequestMapping("api/v1/") public class UserController { @Autowired private UserRepository userRepository; @RequestMapping(value = "user/{id}", method = RequestMethod.GET) public User get(@PathVariable Long id) { return userRepository.findOne(id); } }
Цель этого метода-получить Пользователя
по его идентификатору
, поэтому мы собираемся проверить, делает ли он то, что должен. Мы начинаем с создания экземпляра нашего контроллера, а затем вызываем метод get()
:
public class MockitoControllerTest { @Test public void testGetUserById() { UserController userController = new UserController(); User user = userController.get(1L); assertEquals(1l, user.getId().longValue()); } }
Мы запрашиваем Пользователя
с идентификатором
1. Затем нам просто нужно запустить утверждение для возвращаемого объекта, чтобы убедиться, что id
действительно равен 1, как и ожидалось.
Если мы проведем тест, обратите внимание на результаты:
Наш тест не удался с NullPointerException
. Похоже, что UserRepository
равно null
.
Это связано с тем, что мы создали экземпляр UserController
и не использовали Spring для его внедрения , поэтому все введенные объекты , используемые UserController
, такие как UserRepository
, никогда не создавались Spring должным образом.
Git Essentials
Ознакомьтесь с этим практическим руководством по изучению Git, содержащим лучшие практики и принятые в отрасли стандарты. Прекратите гуглить команды Git и на самом деле изучите это!
Это именно та проблема, для которой были созданы макетные фреймворки. Используя Mockito, мы можем издеваться над UserRepository
, чтобы заставить наш тест работать:
public class MockitoControllerTest { @InjectMocks private UserController userController; @Mock private UserRepository userRepository; @Before public void init() { MockitoAnnotations.initMocks(this); } @Test public void testGetUserById() { User u = new User(); u.setId(1l); when(userRepository.findOne(1l)).thenReturn(u); User user = userController.get(1L); verify(userRepository).findOne(1l); assertEquals(1l, user.getId().longValue()); } }
Вместо создания экземпляра UserController
мы хотим создать его макет. Mockito предоставляет аннотацию, которая создает этот объект и вводит его в тест. Мы используем аннотацию @InjectMocks
, и это создает частный атрибут под названием UserController
, которым управляет Mockito для нас.
Затем мы создали объект UserRepository
mock, и это исправляет наше исключение NullPointerException
при тестировании контроллера. Для этого мы используем другую аннотацию Mockito – @Mock
.
Затем мы добавили метод настройки, который инициализирует все издевательские объекты вместе при выполнении теста. Метод, аннотированный @Перед
, запускается перед каждым методом тестирования. Метод init()
запускает MockitoAnnotations.initMocks(this)
, используя этот
экземпляр в качестве аргумента. Это настраивает наши насмешки перед каждым тестом.
Передача этого
экземпляра заставит Mockito подтвердить @InjectMocks
и @Mocks
аннотации и то, что их следует объединить.
В этом случае, поскольку UserController
содержит UserRepository
в нем, платформа Mockito продолжит и настроит это для нас, так же, как контейнер Spring с помощью инъекции зависимостей.
Вы можете быть удивлены использованием when()
в этом примере. Это еще один статический
импорт, предоставленный Mockito. Давайте пройдем через это шаг за шагом, и его цель будет ясна.
Во-первых, мы создали новый пользователь
объект и установили идентификатор
равным 1. Метод when()
позволяет нам обеспечить фактическое поведение насмешки. Это говорит Mockito о том, что при вызове метода findOne
в репозитории возвращенный Пользователь
должен быть заглушен .
Это означает, что возвращаемый класс является поддельным с запрограммированными возвращаемыми значениями, а не реальным возвращаемым объектом из базы данных. Это позволяет нам протестировать устройство без необходимости подключения к базе данных или Spring вообще.
Другая полезная функция, которую предоставляет Mockito, – это возможность проверить
, что наши издевательские или заглушенные вызовы методов действительно используются в процессе тестирования.
Мы можем разместить проверить
проверить, действительно ли вызывается метод с заглушкой – в нашем случае найти один()
. Это еще один способ определить, насколько хорошо работает ваш код.
Если по какой-либо причине контроллер вызовет метод findOne()
более одного раза, тест мгновенно завершится неудачей и предоставит вам информацию, которая поможет определить нарушающую логику и изящно исправить ее.
После применения этих изменений повторный запуск теста приведет к красивому зеленому пропуску:
Вы можете видеть, что возможности теперь становятся бесконечными при тестировании блоков, даже сложного кода. Настройка занимает несколько больше времени, но теперь вы можете тестировать контроллеры, службы или любые другие объекты без необходимости запускать интеграционный тест, который загружает контейнер Spring.
Модульные тесты с макетными объектами выполняются быстро – намного быстрее, чем интеграционные тесты.
Сопоставители Хэмкреста
В предыдущем тесте мы использовали оба утверждения JUnit для проверки результатов теста, а также Mockito verify
, чтобы убедиться, что издевательские объекты были вызваны соответствующим образом. Тем не менее, зачем интегрировать другую платформу, чтобы просто обрабатывать сопоставление и утверждения результатов тестирования?
Hamcrest обеспечивает более понятный, декларативный подход к утверждению и сопоставлению результатов теста. Многие разработчики начинают отдавать предпочтение синтаксическому сахару Hamcrest перед другими методами утверждения. Чтобы увидеть, как работает Hamcrest, мы вернемся к тесту контроллера Mockito
тесту, расположенному в папке src/test/java
:
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; public class MockitoControllerTest { @InjectMocks private UserController userController; @Mock private UserRepository userRepository; @Before public void init() { MockitoAnnotations.initMocks(this); } @Test public void testGetUserById() { User u = new User(); u.setId(1l); when(userRepository.findOne(1l)).thenReturn(u); User user = userController.get(1L); verify(userRepository).findOne(1l); //assertEquals(1l, user.getId().longValue()); assertThat(user.getId(), is(1l)); } }
В тестовом примере testGetUserById()
тест выполняется с использованием утверждения JUnit – assertEquals
. Он проверяет, соответствует ли id
возвращаемого объекта 1 или нет.
Мы можем изменить это с помощью статического метода Hamcrest assertThat ()
. Пожалуйста, обратите внимание, что логика теста, а также тестируемый код остаются нетронутыми – фактически, новая строка логически эквивалентна предыдущему утверждению JUnit.
Это все еще вызывает вопрос: если они логически одинаковы, зачем включать другую структуру? Сравнивая эти два утверждения, очевидно, что утверждение Хэмкреста более читаемо и менее оскорбительно для глаз. Кроме того, с первого взгляда легче понять, какова конечная цель тестового утверждения.
Утверждение утверждает, что()
является простым сравнением равенства, хотя Hamcrest предоставляет множество вариантов и совпадений, помимо этого. Включать их все в таблицу было бы безумием, поэтому, пожалуйста, обратитесь к официальной документации Hamcrest , если вам интересно прочитать о них.
Интеграционное тестирование
Последний тип тестирования, который мы рассмотрим, – это концепция Интеграционного тестирования .
Интеграционное тестирование-это тестирование всех частей приложения, работающих вместе, как в реальной или производственной среде. Это означает, что наше приложение, по сути, должно быть запущено для его тестирования. Из-за характера интеграционных тестов это создает некоторые проблемы при создании и запуске тестов такого типа.
До весенней загрузки существовали некоторые проблемы, с которыми, как правило, сталкивались приложения Spring.
Проблемы с интеграционным тестированием
Традиционное Применение Пружин
Контейнеры трудно проверить:
Любой код, являющийся частью вашего приложения, который полагается на контейнер или спецификацию сервлета, трудно протестировать, потому что вам нужно либо протестировать запуск контейнера и выполнить тесты против него, либо вам нужно имитировать контейнер и эмулировать его каким-либо другим способом.
Весенний контекст должен быть доступен:
Поскольку Spring Core, Spring Beans и внедрение зависимостей требуют, чтобы Spring запускал и управлял этими частями в контексте своего приложения. Все интеграционные тесты должны гарантировать, что контекст приложения Spring запущен.
Запуск приложения/теста может быть медленным:
Запуск контекста Spring и запуск или эмуляция контейнера могут занять некоторое время в более крупных приложениях. Интеграционные тесты, естественно, выполняются медленнее, чем обычные модульные тесты. Вы можете себе представить, что по мере добавления все большего количества интеграционных тестов время тестирования, необходимое для их выполнения, может резко увеличиться.
Состояние базы данных должно быть согласованным:
Если ваши интеграционные тесты изменяют базу данных или ожидают, что определенные данные в базе данных будут доступны для вашего тестового случая, то вы можете столкнуться с проблемами, если не сможете обеспечить согласованность базы данных при каждом запуске тестов.
Приложения для весенней загрузки
Нет контейнера, проще запустить приложение:
Поскольку приложения Spring Boot можно запускать как обычное приложение Java, сложность работы с контейнером и развертывания приложения устраняется. Конечно, Spring Boot по-прежнему имеет встроенный контейнер, но Spring Boot просто упрощает запуск и работу с вашим приложением.
Автоматическая настройка контекста пружины:
Интеграционные тесты в Spring Boot по-прежнему должны иметь контекст Spring. Основное различие между Spring Boot и традиционными приложениями Spring заключается в использовании стартеров и автоматической настройки. Это немного облегчает установку пружинного контейнера с пружинным загрузчиком.
Запуск приложения/теста может быть медленным:
Запуск и время выполнения интеграционного теста по-прежнему являются проблемами в среде весенней загрузки. Чем больше ваше приложение и чем больше у вас компонентов Spring, тем больше времени потребуется для запуска вашего приложения.
Состояние базы данных должно быть согласованным:
Согласованность базы данных также по-прежнему является проблемой при тестировании весенней загрузки.
Несмотря на все эти проблемы, интеграционные тесты по-прежнему являются одним из лучших способов убедиться, что ваше приложение в целом работает так, как задумано и разработано.
Когда дело доходит до интеграционного тестирования, приложения Spring Boot действительно начинают превосходить обычные приложения Spring. Чтобы преобразовать любой тест JUnit в надлежащий интеграционный тест, вам действительно нужно сделать две основные вещи.
Во-первых, вам нужно аннотировать свои тесты с помощью аннотации @RunWith
и указать, что вы хотите запустить их с помощью SpringJUnit4ClassRunner.class
.
Во-вторых, вам нужно добавить аннотацию @SpringApplicationConfiguration
и указать свой основной класс загрузки Spring для вашего приложения.
Этот тест будет отвечать за тестирование объекта данных UserRepository
Spring:
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(Demo.class) public class UserRepoIntegrationTest { @Autowired private UserRepository userRepository; @Test public void testFindAll() { Listusers = userRepository.findAll(); assertThat(users.size(), is(greaterThanOrEqualTo(0))); } }
Этот тест запрашивает репозиторий для всех пользователей, а затем использует Hamcrest, чтобы убедиться, что возвращаемый список больше или равен 0. Теперь, когда тест запускается, контекст Spring загрузится, и Spring введет полный UserRepository
в тест, точно так же, как если бы он работал в стандартном приложении.
Независимо от результата теста – успешного или неудачного, откройте вкладку консоли IDE, и вы должны заметить, что она выглядит так, как будто ваше приложение запущено (логотип Spring, информация и т.д.). Это происходит потому, что наше приложение фактически запускается с интеграционных тестов. По сути, каждый интеграционный тест будет загружать ваше приложение, и это одна из причин, по которой интеграционные тесты могут занять некоторое время, если у вас действительно большое приложение или у вас много тестов.
Вы можете подумать, что достигли зенита тестирования с помощью Spring Boot, но есть одна область, которую мы до сих пор вообще не охватили, и это фактический API REST, который предоставляют ваши контроллеры.
Теперь, когда JavaScript, MVC и мобильным приложениям нравится общаться с REST API, а не с шаблонной страницей JSP, приятно иметь возможность протестировать этот реальный API. Это, в свою очередь, проверяет весь ваш серверный стек. Итак, это концепция теста веб-интеграции.
Spring предоставляет простую аннотацию, которая помечает интеграционный тест как тест веб-интеграции @WebIntegrationTest
. Как обычно, внутри вашей папки src/test/java
создайте класс:
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(Demo.class) @WebIntegrationTest public class UserControllerWebIntegrationTest { @Test public void testListAll() throws IOException { RestTemplate restTemplate = new TestRestTemplate(); ResponseEntityresponse = restTemplate.getForEntity("http://localhost:8080/api/v1/users", String.class); assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); ObjectMapper objectMapper = new ObjectMapper(); JsonNode responseJson = objectMapper.readTree(response.getBody()); assertThat(responseJson.isMissingNode(), is(false)); assertThat(responseJson.toString(), equalTo("[]")); } }
Первое, что мы делаем, это создаем табличку RestTemplate – Табличка RestTemplate
– это то, как мы можем программно вызывать API, и в этом случае мы хотим программно вызвать пользовательский API.
Вызов API запрашивает всех пользователей в системе, и он содержит ответ. Хотя я жестко закодировал URL-адрес сервера здесь для учебных целей, но это то, что вы могли бы и, безусловно, должны перенести в файл application.properties
или настроить свои тесты так, чтобы они указывали на вашу тестовую среду, а не на среду разработки.
Затем мы запускаем утверждение, чтобы убедиться, что в качестве ответа мы получим 200 OK
, а если нет, тест немедленно завершится неудачей.
Затем мы хотим преобразовать ответ в реальный объект JSON и выполнить над ним утверждения, чтобы убедиться, что возвращаемый объект JSON находится в состоянии, которое имеет смысл для нашего приложения.
Поскольку в нашей базе данных на самом деле нет пользователей и, честно говоря, ее не существует, мы проверим, чтобы убедиться, что в качестве полезной нагрузки JSON мы получим пустой массив – equalTo("[]")
.
Опять же, эти тесты очень дороги в запуске, поэтому они могут быть чем-то, что вы хотите настроить только на сервере непрерывной сборки, и запускать их каждый раз, когда кто-то из вашей команды что-то проверяет или добавляет в хранилище кода.
Вывод
Поскольку Spring Boot объединяет JUnit, Mockito и Hamcrest, я хотел рассказать о том, как использовать эти инструменты в приложении Spring Boot. Как и большинство фреймворков, которые мы включили в наши тестовые примеры Spring Boot, вы можете и должны потратить некоторое время на изучение каждого из фреймворков самостоятельно, поскольку они предоставляют действительно полезные инструменты для разработки.
Чтобы начать писать тесты в своих проектах, на самом деле не требуется много усилий, когда вы интегрируетесь с Spring Boot. Начните привыкать к тестированию, потому что это определенно выделит вас среди других разработчиков. Это поможет написать код более высокого качества, так как вы, по сути, проведете обзор кода своей собственной работы, пытаясь ее протестировать.
Как вы видели на примерах, которые мы рассмотрели, существует множество вариантов тестирования в приложении Spring Boot. И хотя мы немного рассмотрели, что возможно, вам следует привыкнуть писать некоторые тесты для некоторого кода, который вы создаете в своем приложении, даже если вы все еще изучаете и тестируете код. Чем больше вы делаете, тем легче вам будет со временем.
Помните, что Spring Boot-это управление зависимостями с самого начала. Эти стартеры часто обеспечивают автоматическую настройку, которая позволяет легко и быстро интегрировать фреймворки в ваше приложение.
Затем вы можете переопределить и настроить потребности приложения с помощью файла application.properties
. Spring Boot освобождает вас от развертывания контейнеров, встраивая контейнер в приложение, так что теперь вы можете запускать свое Java-приложение в любом месте. Это значительно упрощает развертывание в облаке или такие вещи, как тестирование вашего приложения.
Обязательно найдите время и ознакомьтесь с официальной документацией по тестированию Spring Boot для получения дополнительной информации.