- PVSM.RU - https://www.pvsm.ru -
Spring Framework часто приводят как пример Cloud Native [1] фреймворка, созданного для работы в облаке, разработки Twelve-Factor приложений [2], микросервисов, и одного из самых стабильных, но в то же время инновационных продуктов. Но в этой статье я бы хотел остановиться на еще одной сильной стороне Spring: это его поддержка разработки через тестирование (TDD-руемость?). Не смотря на TDD-руемость, я часто замечал, что в проектах на Spring либо игнорируются некоторые best practices для тестирования, либо изобретаются свои велосипеды, либо вообще не пишутся тесты потому что они "медленные" или "ненадежные". И вот именно о том, как писать быстрые и надежные тесты для приложений на Spring Framework и вести разработку через тестирование я и расскажу. Так что если вы используете Spring (или хотите начать), понимаете что такое вообще тесты (или хотите понять), или думаете что contextLoads
это и есть необходимый и достаточный уровень интеграционного тестирования — то будет интересно!
"TDD-руемость" характеристика очень неоднозначная, и плохо измеримая, но все же у Spring есть много всего что by design помогает писать интеграционные и юнит тесты с минимумом усилий. Например:
Для начала, маленькое, но необходимое, введение про TDD и тестирование вообще.
В основе TDD лежит очень простая идея — пишем тесты до того, как пишем код. В теории звучит пугающе, но через какое-то время приходит понимание практик и приемов, и вариант написание тестов после вызывает ощутимый дискомфорт. Одна из ключевых практик это итеративность, т.е. делать все маленькими, сфокусированными итерациями, каждая из которых описывается как Red-Green-Refactor.
В красной фазе — пишем падающий тест, при чем очень важно, чтобы он падал с ясной, понятной причиной и описанием и чтобы сам тест был законченым и проходил, когда написан код. Тест должен проверять поведение, а не реализацию, т.е. следовать подходу "черного ящика", далее поясню почему.
В зеленой фазе пишем минимально необходимый код чтобы пройти тест. Иногда бывает интересно попрактиковаться и доводить до асбсурда (хотя лучше не увлекаться) и когда функция возвращает boolean в зависимости от состояния системы, первым "проходом" может быть просто return true
.
В фазе рефакторинга, к которой можно приступать только когда все тесты зеленые, рефакторим код и приводим его в надлежащее состояние. Необязательно даже для куска кода, который мы написали, поэтому и начинать рефакторинг важно на стабильной системе. Подход "черного ящика" как раз поможет выполнять рефакторинг, меняя реализацию, но не трогая поведение.
Я буду еще говорить о разных аспектах TDD в будущем, в конце-концов это идея серии статей, поэтому сейчас особо не буду останавливаться на деталях. Но заранее отвечая на стандартную критику TDD, упомяну пару мифов, которые я слышу часто.
Главная цель TDD и вообще тестирования — дать команде уверенность, что система работает стабильно. Поэтому никакая из практик тестирования не определяет, сколько и каких тестов писать. Пишите, сколько считаете нужным, сколько вам нужно, чтобы быть уверенным, что прямо сейчас код можно поставить в продакшен и он будет работать. Есть люди, которые считают быстрые интеграционные тесты, как ультимативный "черный ящик" необходимыми и достаточными, а юнит-тесты — опциональными. Кто-то говорит, что e2e тесты при возможности быстрого отката к предыдущей версии и наличии canary-релизов не так критичны. Сколько команд — столько и подходов, важно найти свой.
Одна из моих целей — это отойти в рассказе о TDD от формата "разработка через тестирование функции, которая складывает два числа", и посмотреть на реальное приложение, своего рода выпаренную до минимального приложения практику тестирования, собранную на реальных проектах. В качестве такого полуреального примера я буду использовать небольшое веб-приложение, которое сам придумал, для абстрактной фабрики пекарни-булочной — Cake Factory. Я планирую писать небольшие статьи, фокусируясь каждый раз на отдельном куске функциональности приложения и показывать, через TDD можно проектировать API, внутреннюю структуру приложения и поддерживать постоянный рефакторинг.
Примерный план для серии статей, как я его вижу на данный момент, это:
Эта вводная статья будет про пункты 1 и 2 — я создам каркас приложения и базовый UI тест, используя подход BDD — или behaviour-driven development. Каждая статья будет начинаться с user story [3], но для экономии времени про "продуктовую" часть я говорить не буду. User story будет написана по-английски, скоро станет понятно, почему. Все примеры кода можно найти на GitHub, так что я не буду разбирать весь код, только важные части.
User story — это описание фичи приложения на естественном языке, которые обычно пишутся от лица пользователя системы.
As Alice, a new user
I want to see a welcome page when visiting Cake Factory web-site
So that I know when Cake Factory is about to launchAcceptance criteria:
Scenario: a user visiting the web-site visit before the launch date
Given I’m a new user
When I visit Cake Factory web-site
Then I see a message 'Thanks for your interest'
And I see a message 'The web-site is coming soon…'
Потребуются знания: что такое Behaviour-Driven Development [4] и Cucumber [5], основ Spring Boot Testing [6].
Первая user story совсем базовая, но цель пока не в сложности, а в создании walking skeleton [7] — минимального приложения, чтобы запустить TDD цикл.
После создания нового проекта на Spring Initializr [8] с Web и Mustache модулями, для начала мне понадобится еще несколько изменений в build.gradle
:
testImplementation('net.sourceforge.htmlunit:htmlunit')
. Версию указывать не нужно, Spring Boot dependency management плагин для Gradle автоматически выберет нужную и совместимую версиюCakeFactoryApplicationTests
с неизбежным contextLoads
По большому счету, это уже базовый "скелет" приложения, уже можно писать первый тест.
Теперь мне как раз пригодится user-story, написанная на английском. Лучший триггер для запуска очередной итерации TDD — это acceptance criteria написанные в таком виде, что их можно с минимум телодвижений превратить в исполняемую спецификацию.
В идеале, user story должны быть написаны так, чтобы их можно было просто скопировать в BDD спецификацию и запустить. Это далеко не всегда просто и не всегда возможно, но это должно быть целью product owner-a и всей команды, пусть и не всегда достижимой.
Итак, моя первая фича.
Feature: Welcome page
Scenario: a user visiting the web-site visit before the launch date
Given a new user, Alice
When she visits Cake Factory web-site
Then she sees a message 'Thank you for your interest'
And she sees a message 'The web-site is coming in December!'
Если сгененрировать описания шагов (очень помогает плагин Intellij IDEA [9] для поддержки Gherkin) и запустить тест, то он, разумеется, будет зеленым — он пока ничего не тестирует. И здесь наступает важная фаза работы над тестом — нужно написать тест, как будто основной код написан.
Часто у тех, кто начинает осванивать TDD здесь наступает ступор — сложно уложить в голове алгоритмы и логику чего-то, что еще не существует. И поэтому очень важно иметь как можно более маленькие и сфокусированные итерации, начиная от user-story и спускаясь на интеграционный и юнит-уровень. Важно фокусироваться на одном тесте за раз и стараться мокать и игнорировать зависимости, которые пока не важны. Я иногда замечал, как люди легко уходят в сторону — создают интерфейс или класс для зависимости, тут же генерят для него пустой тестовый класс, там добавляется еще одна зависимость, создается еще один интерфейс и так далее.
Если story будет "надо бы при сейве рефрешить статус" ее очень сложно автоматизировать и формализовать. В моем же примере, каждый шаг четко можно уложить в последовательность шагов, которые можно описать кодом. Понятно, что это самый простой пример и он мало что демонстрирует, но надеюсь что дальше, с возрастанием сложности, будет интереснее.
Итак, для мой первой фичи я создал следующие описания шагов:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WelcomePage {
private WebClient webClient;
private HtmlPage page;
@LocalServerPort
private int port;
private String baseUrl;
@Before
public void setUp() {
webClient = new WebClient();
baseUrl = "http://localhost:" + port;
}
@Given("a new user, Alice")
public void aNewUser() {
// nothing here, every user is new by default
}
@When("she visits Cake Factory web-site")
public void sheVisitsCakeFactoryWebSite() throws IOException {
page = webClient.getPage(baseUrl);
}
@Then("she sees a message {string}")
public void sheSeesAMessageThanksForYourInterest(String expectedMessage) {
assertThat(page.getBody().asText()).contains(expectedMessage);
}
}
Пара моментов, на которые следует обратить внимание:
Features.java
используя RunWith
аннотацию из JUnit 4, Cucumber не поддерживает версию 5, увы@SpringBootTest
аннотация добавлена на описание шагов, ее оттуда подхватывает cucumber-spring
и конфигурирует тестовый контекст (т.е. запускает приложение)webEnvironment = RANDOM_PORT
и этот случайный порт передается в тест используя @LocalServerPort
, Spring найдет эту аннотацию и установит значение поля в порт сервераИ тест, ожидаемо, падает с ошибкой 404 for http://localhost:51517
.
Ошибки, с которыми падает тест, невероятно важны, особенно когда речь идет о юнит или интеграционных тестах и эти ошибки — часть API. Если тест падает с
NullPointerException
это не слишком хорошо, а вотBaseUrl configuration property is not set
— гораздо лучше.
Чтобы сделать тест зеленым я добавил базовый контроллер и view с минимальным HTML:
@Controller
public class IndexController {
@GetMapping
public String index() {
return "index";
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Cake Factory</title>
</head>
<body>
<h1>Thank you for your interest</h1>
<h2>The web-site is coming in December!</h2>
</body>
</html>
Тест зеленый, приложение работает, хоть и выполнено в традицих сурового инженерного дизайна.
На реальном проекте и в сбалансированной команде [10] я бы, конечно, сел вместе с дизайнером и мы бы превратили голый HTML во что-то гораздо более прекрасное. Но в рамках статьи чуда не произойдет, царевна так и останется лягушкой.
Вопрос "какую часть в TDD занимает дизайн" не такой простой. Одна из практик, которую я нашел полезной — сначала вообще даже не смотреть на UI (даже не запускать приложение, чтобы сберечь нервы), написать тест, сделать его зеленым — и потом, имея стабильное основание, работать над фронт-ендом, постоянно перезапуская тесты.
В первой итерации никакого рефакторинга особо нет, но хотя я последние 10 минут потратил выбирая шаблон для Bulma [11], которые можно засчитать за рефакторинг!
Пока в приложении нет ни работы с безопасностью, ни с БД, ни API — то тесты и TDD выглядят довольно просто. Да и в общем-то из пирамиды тестирования я затронул только самую верхушку, UI тест. Но в этом, отчасти, и секрет lean подхода — делать все небольшими итерациями, один компонент за раз. Это помогает фокусироваться на тестах, делать их простыми, и контролировать качество кода. Надеюсь, что в следующих статьях будет больше интересного.
P.S. Название статьи не такое безумное, как может показаться в начале, думаю многие уже догадались. "How to build a pyramid in your boot" отсылка к пирамиде тестирования (расскажу про нее дальше) и Spring Boot, где boot в британском английском значит еще и "багажник".
Автор: alek_sys
Источник [14]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/300495
Ссылки в тексте:
[1] Cloud Native: https://pivotal.io/spring-app-framework
[2] Twelve-Factor приложений: https://12factor.net/
[3] user story: https://en.wikipedia.org/wiki/User_story
[4] Behaviour-Driven Development: https://docs.cucumber.io/bdd/
[5] Cucumber: https://docs.cucumber.io/
[6] Spring Boot Testing: https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html
[7] walking skeleton : https://devops.stackexchange.com/questions/712/what-is-a-walking-skeleton
[8] Spring Initializr: https://start.spring.io/
[9] плагин Intellij IDEA: https://plugins.jetbrains.com/plugin/9164-gherkin
[10] сбалансированной команде: https://builttoadapt.io/use-balanced-teams-to-suck-less-at-software-a10b6ee8ff51
[11] Bulma: https://bulma.io/
[12] GitHub: https://github.com/alek-sys/cake-factory
[13] Concourse CI: https://ci.alexnesterov.com/teams/main/pipelines/cake-factory
[14] Источник: https://habr.com/post/431306/?utm_campaign=431306
Нажмите здесь для печати.