Написание тестов — важная часть создания качественного ПО, но в то же время кажется неинтересным и утомительным занятием. Попробуем улучшить этот процесс, объединив сразу несколько крутых технологий.
Если говорить про тестирование Java-приложений, то первым на ум приходит JUnit. Этот фреймворк часто используется в Test-Driven Development (TDD) подходе, при котором сначала пишутся тесты на ожидаемое поведение системы, а затем код, который обеспечивает указанное поведение. В качестве альтернативы JUnit выступает Spock Framework, использующий подход Behavior-Driven Development (BDD). Его суть заключается в том, чтобы создавать читаемые, продуктивные и понятные спецификации поведения системы.
BDD сценарии, в отличие от TDD, обычно более доступны для понимания и использования всеми участниками команды разработки, включая нетехнических участников команды, таких как бизнес-аналитики. BDD помогает фокусироваться на бизнес-требованиях и легче сопровождать код в долгосрочной перспективе.
Учитывая специфику Spock Framework, возникают следующие вопросы:
В чём состоит особенность написания BDD тестов в Spock?
Каким образом выполняется интеграция Spock и Spring Boot?
Как при тестировании Spring приложения выполнить конфигурацию контекста?
Можно ли тестировать persistence layer?
Как выглядит тестирование Java приложения с помощью языка Groovy?
Чтобы ответить на эти вопросы, далее в статье описан процесс интеграции Spring Boot и Spock Framework, а ��акже приведены примеры тестирования в BDD подходе.
Теория
Основная структура спецификации (теста)
Тестирование в Spock устроено по принципу behavior-driven development (BDD). Это означает, что вместо написания тестов для отдельных компонентов будут разрабатываться спецификации и требования, основанные на работе системы.
В Spock используется система блоков given-when-then (и другие), что помогает семантически разделить код:

given: "an empty bank account"
// ...
when: "the account is credited $10"
// ...
then: "the account's balance is $10"
// ...
Так выглядит типичный тест с использованием Spock, написанный на языке Groovy:
class HashMapSpec extends Specification { def "HashMap accepts null key"() { given: def map = new HashMap() when: map.put(null, "value") then: notThrown(NullPointerException) } }
Для сравнения, этот же тест на JUnit 5, написанный на Java:
public class HashMapTest { @Test public void testNullKey() { HashMap map = new HashMap(); assertDoesNotThrow(() -> { map.put(null, "value") }); } }
Инструменты Spock
Обычно системы состоят из сочетаний множества компонентов, взаимодействие которых со временем становится трудно отслеживать. В этом могут помочь инструменты: Mock, Stub и Spy. Они предоставляют удобный способ имитации и контроля вызовов зависимостей в тестах. Подробнее разобраться в них поможет эта статья.
Вкратце:
Stub - заглушка, не имеющая реализации, просто предоставляет данные;
Mock - добавляет к функционалу Stub возможность контролировать вызовы метода;
Spy - обертка для реального объекта, официальная документация не советует его использовать.
Практика
Совместимость версий
Spock Framework последней версии не будет совместим с версиями Spring Boot ниже 2.7. Лучший вариант, чтобы версии Spring и Spock были примерно одного времени выхода. Также будет требоваться версия Groovy, которая соответствует той, что указывается в конце версии Spock.
В качестве примера для сборки будет использоваться Gradle. Добавляем следующие зависимости в готовый Spring Boot проект (на момент написания статьи использовалась версия 2.7.12):
testImplementation 'org.codehaus.groovy:groovy-all:3.0.16' testImplementation 'org.spockframework:spock-core:2.3-groovy-3.0' testImplementation 'org.spockframework:spock-spring:2.3-groovy-3.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test'
Также нужно добавить следующую строку в разделе plugins:
id 'groovy'
Тесты в Spock пишутся на языке Groovy, что даёт несколько преимуществ. Например, строки в названиях функций (так будет проще описать поведение теста) или поддержка перегрузки операторов, что улучшает читаемость кода.
Свойства
В модуле test для удобства можно создать файл application.proprties, содержащий свойства, который будет использоваться вместо основного во время проведения тестов.
Иногда в целях тестирования приходится переопределять уже существующие бины, и чтобы это стало возможно, нужно прописать следующее свойство:
spring.main.allow-bean-definition-overriding=true
Добавляем H2 базу данных (если надо)
Чтобы во время тестов не затрагивать базу данных проекта, можно использовать in-memory database. Для этого добавим ещё одну зависимость:
testImplementation 'com.h2database:h2:2.1.214'
Также нужно добавить следующие свойства:
spring.datasource.url=jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1; spring.datasource.username=test_usernamee spring.datasource.password=test_password spring.datasource.driver-class-name=org.h2.Driver spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=create
Зачастую на проекте используется ORM система, и существует уже готовая схема БД, на основе которой нужно проводить тестирование. Из-за этого могут возникнуть конфликты, которые можно решить, прописав свойства для свойства (да-да, всё верно).
Если у вас названия таблиц с маленькой буквы, то добавляем к первому свойству:
DATABASE_TO_UPPER=false;
Следующее свойство решает проблему инициализации схемы:
INIT=CREATE SCHEMA IF NOT EXISTS public;
Интеграция Spring Boot
В качестве примера протестируем сервис для работы с клиентами.
Если у тестируемого модуля сложная конфигурация, её можно вынести в отдельный groovy-класс вот таким образом:
@Configuration class ClientServiceConfig { private final mockFactory = new DetachedMockFactory() @Bean SearchIntegration searchIntegration() { return mockFactory.Mock(SearchIntegration) } @Bean RegionService regionService() { return mockFactory.Stub(RegionService) } @Bean ClientDataService clientDataService() { return mockFactory.Stub(ClientDataService) } //... }
Для запуска теста должны быть предоставлены зависимости тестируемых классов. Это можно сделать с помощью DetachedMockFactory выбрав Mock, Stub или Spy. Если для создания класса вы использовали заглушку Mock или Stub, то прописывать вложенные в них зависимости не нужно.
Затем полученную конфигурацию легко добавить в контекст к тестируемому объекту (через аннотацию @SpringBootTest или @ContextConfiguration). Классы, добавленные через @Autowired, должны присутствовать в контексте. Соответствующие конфигурации можно создавать для каждого модуля.
Таким образом, можно составить такую спецификацию сервиса для работы с клиентами:
@SpringBootTest(classes = [ClientServiceConfig.class, ClientService.class]) class ClientServiceSpec extends Specification { @Autowired @Subject ClientService clientService @Autowired ClientRepository clientRepository Client validClient def setup() { validClient = new Client() validClient.setInn("0123456789") } def cleanup() { clientRepository.deleteAll() } def "should save valid client"() { when: def savedClient = clientService.save(validClient) then: savedClient.getId() != null } def "should not save invalid client"() { expect: clientService.save(new Client()) == null } def "getting client without inn should throw an exception"() { when: clientService.getByInn(null, true) then: def exception = thrown(ClientException) println(exception.message) } }
Интеграционное тестирование
Ничего не мешает также использовать MockMvc, чтобы тестировать работу контроллеров без деплоя на сервер.
Вот пример тестирования эндпоинта аутентификации:
@SpringBootTest @AutoConfigureMockMvc class AuthControllerSpecification extends Specification { @Autowired MockMvc mockMvc @Autowired ObjectMapper objectMapper @SpringBean JwtUtils jwtUtils = Mock() def "should generate token for user with correct credentials"() { given: "correct credentials" def credentials = AuthRequest.builder() .login("user@example.com") .password("password") .build() when: "passed to: /account/auth" def responseHeader = mockMvc.perform(post("/account/auth") .content(objectMapper.writeValueAsString(credentials)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andReturn() .response.getHeader(HttpHeaders.AUTHORIZATION) then: "response header contains jwt token" 1 * jwtUtils.generateToken(_, _) >> "Authenticated!" responseHeader == "Authenticated!" } }
Так как это простая конфигурация, то создать Mock, Stub или Spy можно с помощью аннотации @SpringBean.
Следующая строка отслеживает, чтобы вызов метода generateToken выполнился только один раз, а также переопределяет его возвращаемое значение:
1 * jwtUtils.generateToken(_, _) >> "Authenticated!"
С помощью этой техники можно изменить внутреннее поведение сервиса для создания различных тест-кейсов.
Заключение
Spock и Spring Boot при правильной настройке хорошо сочетаются вместе. Как раз на этом этапе часто возникают трудности, особенно у начинающих разработчиков. В статье мне хотелось просто и на примерах описать процесс настройки и тестирования такого сетапа.
Ссылки
Spock Framework Reference Documentation
Spring Module (spockframework.org)
