How to build a pyramid in the trunk or Test-Driven Development applications for Spring Boot

    The Spring Framework is often cited as an example of the Cloud Native framework created for working in the cloud, developing Twelve-Factor applications , microservices, and one of the most stable, but at the same time innovative products. But in this article I would like to dwell on another strong point of Spring: is it his support for developing through testing (TDD)? In spite of TDD, I often noticed that projects on Spring either ignore some best practices for testing, either invent their bikes, or do not write tests at all because they are "slow" or "unreliable." And that's exactly how to write fast and reliable.tests for applications on the Spring Framework and I will tell you how to develop it through testing. So if you use Spring (or want to start), understand what tests are in general (or want to understand), or think that contextLoadsthis is a necessary and sufficient level of integration testing - it will be interesting!


    "TDD-rumenost" feature is very ambiguous, and poorly measurable, but still Spring has a lot of things that by design helps to write integration and unit tests with a minimum of effort. For example:


    • Integration testing - you can easily start an application, lock components, override parameters, etc.
    • Focus integration testing - data access only, web only, etc.
    • Out-of-box support - in-memory databases, message queuing, authentication and authorization in tests
    • Testing through contracts (Spring Cloud Contract)
    • Support for testing Web UI using HtmlUnit
    • Application configuration flexibility — profiles, test configurations, components, etc.
    • And much more

    For a start, a small, but necessary, introduction about TDD and testing in general.


    Test Driven Development


    At the heart of TDD is a very simple idea - we write tests before we write code. In theory, it sounds scary, but after a while, an understanding of practices and techniques comes along, and writing tests afterwards causes discomfort. One of the key practices is iteration , i.e. make all small, focused iterations, each of which is described as a Red-Green-Refactor .


    In the red phase, we write a falling test, and it’s very important that it falls with a clear, understandable reason and description, and that the test itself is complete and passes when the code is written. The test should check the behavior , not the implementation , i.e. follow the black box approach, further explain why.


    In the green phase, we write the minimum necessary code to pass the test. Sometimes it is interesting to practice and bring to asbestos (although it is better not to get carried away) and when the function returns a boolean depending on the state of the system, the first “pass” can be simple return true.


    In the refactoring phase , which can be started only when all the tests are green , we refactor the code and bring it to the proper state. It is not necessary even for a piece of code that we wrote, therefore, it is important to start refactoring on a stable system. The “black box” approach will help to perform refactoring, changing the implementation, but not touching the behavior.


    I will also talk about different aspects of TDD in the future, after all, this is the idea of ​​a series of articles, so now I’ll not particularly dwell on the details. But in advance of responding to the standard criticism of TDD, I will mention a couple of myths that I hear often.


    • "TDD is about 100% coverage of the code, but it does not guarantee" - development through testing has no relation to 100% coverage at all. In many teams where I worked, this metric was not even measured, and was assigned to the category of vanity metrics. And yes, 100% test coverage means nothing.
    • "TDD works only for simple functions, a real application with a database and a complex state cannot be made with it" - a very popular excuse, usually supplemented "We have such a complex application that we don’t write tests at all, it’s not possible at all." I saw a working TDD approach on completely different applications - web (with SPA and without), mobile, API, microservices, monoliths, the most complicated banking systems, cloud platforms, frameworks, retail platforms written in different languages ​​and technologies. So the popular myth “We are unique, everything is different here” is often an excuse not to invest in testing and not a real reason (although there may be real reasons too).
    • "With TDD there will still be bugs" - of course, like in any other software. TDD is not about bugs at all or their absence, it is a development tool. Like debugging. Like an IDE. Like documentation. None of these tools guarantees the absence of bugs, they only help to cope with the increasing complexity of the system.

    The main goal of TDD and testing in general is to give the team confidence that the system is stable. Therefore, none of the testing practices determine how much and what tests to write. Write as you see fit, how much you need to be sure that right now the code can be put into production and it will work . There are people who consider rapid integration tests as an ultimatum “black box” necessary and sufficient, and unit tests as optional. Someone says that e2e tests are not so critical with the possibility of a quick rollback to the previous version and the availability of canary releases. How many teams - so many approaches, it is important to find your own.

    One of my goals is to move away from the TDD story from the “test-develop function” function that adds two numbers ” and look at a real application, a kind of test practice evaporated to a minimal application, collected on real projects. As such a half-real example, I will use a small web application, which I myself invented, for an abstractfactoriesbakery-bakery - Cake Factory . I plan to write small articles, focusing each time on a separate piece of application functionality and show, through TDD, you can design the API, the internal structure of the application and maintain constant refactoring.


    A rough plan for a series of articles, as I see it at the moment, is:


    1. Walking skeleton - an application framework on which you can run the Red-Green-Refactor cycle
    2. UI Testing and Behavior Driven Design
    3. Data Access Testing (Spring Data)
    4. Testing authorization and authentication (Spring Security)
    5. Reactive stack (WebFlux + Project Reactor)
    6. Interaction (micro) services and contracts (Spring Cloud)
    7. Testing Message Queuing (Spring Cloud)

    This introductory article will be about points 1 and 2 - I will create an application framework and a basic UI test using the BDD approach - or behavior-driven development . Each article will begin with a user story , but to save time about the "food" part, I will not speak. User story will be written in English, it will soon be clear why. All code examples can be found on GitHub, so I will not review all the code, only the important parts.


    User story is a description of the features of the application in natural language, which are usually written on behalf of the system user.

    User story 1: The user sees the welcome page.


    Of As by Alice, a new user
    I of want to see a welcome page the when a visiting the Cake Factory of web-site
    for So That I of the know the when the Cake Factory is about to launch

    Acceptance Criteria:
    the Scenario: a user a visiting the of web-site visit the before the launch date
    Given I of
    When I visit the Web site,
    I’m a new user,
    And I’m a new user .

    Knowledge will be required: what is Behavior-Driven Development and Cucumber , the basics of Spring Boot Testing .


    The first user story is quite basic, but the goal is not yet in complexity, but in the creation of walking skeleton , a minimal application to start the TDD cycle .


    After creating a new project on Spring Initializr with Web and Mustache modules, first I will need a few more changes to build.gradle:


    • add HtmlUnit testImplementation('net.sourceforge.htmlunit:htmlunit'). The version does not need to be specified, the Spring Boot dependency management plugin for Gradle will automatically select the required and compatible version
    • migrate the project from JUnit 4 to JUnit 5 (because 2018 is outside)
    • add dependencies to Cucumber library, which I will use to write BDD specifications
    • delete the default created CakeFactoryApplicationTestswith the inevitablecontextLoads

    By and large, this is already the basic “skeleton” of the application, you can already write the first test.


    To make it easier to navigate the code, I will briefly tell you about the technologies used.


    Cucumber


    Cucumber is a Behavior-Driven Development framework that helps create "executable specifications", i.e. run tests (specifications) written in natural language. The Cucumber plugin analyzes Java source code (and many other languages) and uses step definitions to run real code. Step definitions are class methods, annotated @Given, @When, @Thenand other annotations.


    HtmlUnit


    The main page of the project calls HtmlUnit "GUI-less browser for Java applications". Unlike Selenium, HtmlUnit does not launch a real browser and, most importantly, does not render the page at all, working directly with the DOM. JavaScript is supported through the Mozilla Rhino engine. HtmlUnit is well suited for classic applications, but not very friendly with Single Page Apps. For a start, it will be enough, and then I will try to show that even such things as a test framework can be made implementation detail, and not the foundation of the application.


    First test


    Now I just need a user-story written in English. The best trigger for launching the next TDD iteration is the acceptance criteria written in such a way that they can be turned into executable specifications with a minimum of gestures.


    Ideally, user stories should be written in such a way that they can simply be copied into the BDD specification and run. This is not always easy and not always possible, but it should be the goal of the product owner and the entire team, although not always achievable.

    So, my first feature.


    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!'

    If you generate steps descriptions (the Intellij IDEA plugin helps Gherkin support) and run the test, it will of course be green - it is not testing anything yet. And here comes the important phase of work on the test - you need to write a test, as if the main code was written .


    Often, those who are starting to detoxify TDD have a stupor here - it is difficult to put algorithms and logic in their head for something that does not yet exist. And therefore, it is very important to have as small and focused iterations as possible , starting from the user-story and going down to the integration and unit-level. It is important to focus on one test at a time and try to get wet and ignore dependencies that are not important yet. I sometimes noticed how people easily go to the side - they create an interface or class for dependency, they immediately generate an empty test class for it, another dependency is added there, another interface is created, and so on.


    If the story is "it would be necessary to refresh the status at the save game" it is very difficult to automate and formalize it. In my example, each step can be clearly laid out in a sequence of steps that can be described by code. It is clear that this is the simplest example and it shows little, but I hope that further, with increasing complexity, it will be more interesting.

    Red


    So, for my first feature, I created the following steps:


    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    publicclassWelcomePage{
        private WebClient webClient;
        private HtmlPage page;
        @LocalServerPortprivateint port;
        private String baseUrl;
        @BeforepublicvoidsetUp(){
            webClient = new WebClient();
            baseUrl = "http://localhost:" + port;
        }
        @Given("a new user, Alice")
        publicvoidaNewUser(){
            // nothing here, every user is new by default
        }
        @When("she visits Cake Factory web-site")
        publicvoidsheVisitsCakeFactoryWebSite()throws IOException {
            page = webClient.getPage(baseUrl);
        }
        @Then("she sees a message {string}")
        publicvoidsheSeesAMessageThanksForYourInterest(String expectedMessage){
            assertThat(page.getBody().asText()).contains(expectedMessage);
        }
    }

    A couple of points to pay attention to:


    • launching features is performed by another file Features.javausing the RunWithannotation from JUnit 4, Cucumber does not support version 5, alas
    • @SpringBootTestthe annotation is added to the description of the steps, it is picked up from there cucumber-springand the test context is configured (i.e., it starts the application)
    • Spring test application starts with webEnvironment = RANDOM_PORTand this random port is transferred to the test using @LocalServerPort, Spring will find this annotation and set the field value to the server port

    And the test, as expected, falls with an error 404 for http://localhost:51517.


    The errors that the test falls with are incredibly important, especially when it comes to unit or integration tests, and these errors are part of the API. If the test falls off NullPointerExceptionit is not too good, but BaseUrl configuration property is not setmuch better.

    Green


    To make the test green, I added a base controller and view with minimal HTML:


    @ControllerpublicclassIndexController{
        @GetMappingpublic String index(){
            return"index";
        }
    }

    <!DOCTYPE html><htmllang="en"><head><metacharset="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>

    The test is green, the application works, although it is done in the tradition of austere engineering design.


    On a real project and in a balanced team, I would, of course, have sat down with the designer and we would have turned naked HTML into something much more beautiful. But as part of the article miracle does not happen, the princess will remain a frog.

    The question “what part of a design in TDD” is not so simple. One of the practices that I found useful - at first not even look at the UI at all (do not even run the application to save nerves), write a test, make it green - and then, having a stable foundation, work on the front end, constantly restarting the tests .


    Refactor


    In the first iteration, there is no particular refactoring, but even though I spent the last 10 minutes choosing a template for Bulma , which can be counted as refactoring!


    Finally


    As long as the application does not work with security, neither from the database nor the API, then the tests and TDD look pretty simple. And in general, from the testing pyramid, I touched only the very top, the UI test. But this, in part, is the secret of the lean approach - to do everything in small iterations, one component at a time. It helps to focus on tests, make them simple, and control the quality of the code. I hope that the following articles will be more interesting.


    Links



    PS The title of the article is not as crazy as it may seem at the beginning, I think many have already guessed. "How to build a pyramid in your boot" is a reference to the testing pyramid (I’ll tell you more about it later) and Spring Boot, where boot in British English also means "trunk".


    Also popular now: