How It Should Have Been Tested
Please note, this article was originally written around a year ago and has been gathering dust on a shelf until I finally decided to publish it. This may explain some slightly outdated versions of the libraries I use, but it shouldn’t hinder the main idea and concept that I put forward in it.
Introduction
In my previous article about 100% test code coverage I talked about how TDD combined with BDD automatically leads to the full test coverage. Bringing theory into practice, in this article, I would like to bring the audience’s attention to the very rueful situation, when many developers either are not familiar with Test Driven Development (TDD) principle at all or have a very superficial or misleading notion about it.
My experience of interviewing candidates for software developer positions and also developers I have met from different companies and projects show that around 90% of those who pretend to know what TDD is, actually share a misleading concept that it is just about writing tests before the implementation of production code, or just writing tests and that the order of implementation is immaterial. Around 5% of the developers I know, try practising TDD, at least by creating tests beforehand. I am not exaggerating when I say that only 1% of the developers I know, actually fully practise TDD, the core idea of which is to follow the Red-Green-Refactor loop, and the three rules of TDD (as Uncle Bob says):
1) don’t write any production code until you have first written a test that fails due to the lack of that code.
2) don’t write more of a test than is sufficient to fail — and failing to compile counts as a failure.
3) don’t write more production code than is sufficient to pass the currently failing test.” [1]
Another survey published in September 2020 found that although 41% of the respondents said their organisations have fully adopted TDD, only 8% said they write tests before code at least 80% of the time, which is the definition of TDD [2][3].
What surprises me is that the TDD process has been known for more than two decades; it is widely accepted as best practice; yet, let’s be honest with ourselves and admit it’s practised by a minority of the developer community.
This made me think about the roots of this situation. If people don’t know how to use a specific tool or methodology, apparently it’s because nobody taught them how to do it properly. Indeed, any observation of the existing online courses, books and articles will show that the vast majority of them don’t teach the principles of the Test Driven Development technique at all. Tests are often omitted completely or added according to the residual principle.
I believe that for the best of both the world and development society, there should be a significant shift in the way how all the known tutorial materials about software development are taught and presented.
How It Should Have Ended (HISHE) is an animated web-series that parodies popular films by creating alternate endings and pointing out various flaws.
I’m not pursuing the goal of making a parody itself. But rather I chose the title “How It Should Have Been Tested” as it is a parody of this animated web series’ name. The idea is that we should start by taking brilliant online courses and remaking them to focus on the TDD principle as a core development technique. In my search for a concrete example, I chose an online course from LinkedIn that I had the pleasure to watch: “Spring Boot 2.0: Essential Training” by Frank P Moley III. Undoubtedly this is a great course, Frank is a fascinating lecturer and I would strongly recommend this course as a very good starting point for learning Spring Boot. Nevertheless, I believe that the material there could have been presented in a slightly different way by focusing on the writing tests first.
I will use the structure of this “Spring Boot 2.0: Essential Training” course as the basis of my version of #HowItShouldHaveBeenTested, but I will advance to the Spring Boot 3.0 for the sake of introducing something new.
Spring Boot Basics
Booting from the web
I’ll start from Chapter 1, “Booting from the web” subchapter. We create a Spring Boot application using the “Spring initializr” with the “Spring Web” dependency as it is suggested in the video:
The first step is to ensure that our project is building and the auto-generated tests are passing:
mvn clean test
This will be our Green state.
The original course suggests adding an index.html file as a static resource. Instead, we’ll first create a Spring boot test:
For the sake of simplicity, we’ll also add a “lombok” dependency into the project. This will allow us to use @SneakyThrows annotation and not clutter the code with explicit “throws” instructions.
We run this test and expectedly it fails:
This is our Red state. Now let’s make it pass by adding an empty index.html file:
We run the test again and it passes:
Now we’re going to refactor our test to add an assertion on the view name. MockMvc won’t automatically perform forwarding, so to assert the content of the page we have to specify “index.html” in the path explicitly:
Again, our test fails:
So we add actual content to the index.html file and re-run the test:
Our test is green:
By doing this exercise we have ensured:
- That our configuration is healthy and correct.
- That our static resource has been placed in the right directory.
- That our static resource has the right name “index.html” so it is properly resolved and forwarded.
- And finally that our index page has the expected content.
Configuration in Spring Boot
Skipping a few theoretical paragraphs from the original video we jump to the practical example of the configuration of a Spring Boot application. In the video, it is suggested that we change the default server port from 8080 to 8000. So let’s do that by first writing a test:
If we run our Spring Boot Test it will fail with the error message:
Could not resolve placeholder 'server.port' in value "${server.port}".
Only now we go to the “application.properties” file to specify the desired port:
The test is green now.
Spring Boot Web
In this next chapter, we’re going to have a look at the Thymeleaf library. In the original video, a page with a list of hotel rooms is suggested as an example for implementation. We’ll follow the same idea, but of course, the first thing we’ll do is create a test:
The test fails as expected:
So we’ll add a new controller implementation. First, we’ll need to add a dependency for the Thymeleaf library:
A controller with a minimum implementation:
And an HTML file with default content suggested by Intellij IDEA:
This we place in the /resources/templates directory:
This minimal implementation allows our test to pass and we can proceed further. Let’s add a new assertion on the page contents. At this stage, we don’t expect it to have any dynamic content, but it should render a table with headers:
These new assertions lead to our test failing. So we proceed to the rooms.html file and by following the original video create a static table there:
Our test is green again.
The next step would be to add an assertion on the dynamic content of the table. We do so, and as expected, the test following this fails
And to make this test pass, Lo and behold! we add those values just into the rooms.html file:
And… our test of course now passes. Here we should take a pause and think: what sort of a test would help us to refactor our code and move the static list of rooms from the template file into the Java code? This wouldn’t be a question in a real-world application. If the real-world application needed to display only a static list of rooms, we would surely stop here and consider our job as done. Why would one introduce a complex logic with services and repositories if the displayed data isn’t supposed to be changed? So to add some dynamics to our application let’s develop a method for adding room information from a web service. This forces us to embrace a few topics discussed in the original video together and temporarily switch from a web controller to a rest controller. We need to introduce a set of web services that will let us fetch and add room items.
Let’s start with a dedicated integration test for the API functionality. Since it is an integration test, it would be handy to introduce method order. This way we can describe a flow of operations performed with our API:
- Fetch an empty list of rooms
- Add a few rooms
- Fetch the list of rooms added previously
The first implementation of this test will look like this:
This test should fail with a 404 error, so we create a rest controller with an empty implementation to make this test pass.
At that stage, we didn’t even need to implement any model classes. The next step is to add a test to create new rooms:
The test fails because there is no Post method described. Let’s add an implementation and make it as simple as possible:
The next step is to add some meat to the bones and provide an actual meaningful implementation for those methods. But first a test: we add an assertion in the “Then” section of the recently created test method, this verifies, that the GET method /api/rooms now returns a list with previously created rooms:
As this new version of the test fails, we’re forced to add some implementation. The simplest solution will be to create a model class for the Room entity and return a static list from the getAllRooms method. For that we have to create a model class by providing it with lombok’s annotations and chain accessors:
And implementing the controller’s method as follows:
This makes our last test method pass, but if we run the entire test class we discover that the first test is now failing:
This is pretty much expected and this forces us to introduce some sort of storage. Let’s switch to the unit level and create a unit test for the controller:
we’ll use the Mockito library, which is part of the spring-boot-starter-test library, and we’ll define an autowired dependency on the RoomsService that doesn’t exist as yet. Using IDE’s helper tool we create a new class for the service as well as the method called “findAll”:
We’ll leave the service’s implementation exactly like that for now. And no, I didn’t forget the @Service annotation; on the contrary — I don’t want to bother about it at the moment. You’ll find out why later on. Let’s run our unit test first and of course, it fails:
This is to be expected because our controller’s implementation doesn’t use any service yet. Let’s go into the controller and fix this problem:
Please note that we’re using lombok’s @RequiresArgsConstructor annotation and declaring our service’s object variable as final. This gracefully makes the controller’s unit test pass and we can move forward. The new test method will verify the interaction between the controller and the service upon the creation of a new Room object:
Here we have to fix two things: add an argument to the “addRoom” method and create “save” method in the RoomsService, keeping them both simple and stupid:
We’re ensuring that our test now is compiling, but not passing:
And providing the needed implementation inside the controller:
The test is green now. Ideally, I should have returned the saved object from the “save” method and constructed a proper URI for the object. But for the sake of simplicity, we’ll now skip this part. Let’s come back to our integration test for the API: both test methods fail as we don’t have any implementation for the storage behind. Let’s create a unit test for the RoomsService class. First, we’re going to check that the findAll method returns an empty list by default:
This isn’t the case yet, so our test fails. We modify the implementation of the method so the test now is green:
The next test will verify that after saving some objects we can get them back with the findAll method:
The test fails, so now we’re forced to implement the simplest version of storage in the service:
The RoomsServiceTest is now fully green. Let’s come back to the API integration test and try to run it again. The test is still red, but the error is different this time:
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.github.andremoniy.springboot3et.service.RoomsService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
The error explicitly says that our service isn’t declared as a service, and now is the right moment to add the needed annotation:
If we run the API integration test again, we’ll discover that the test method for adding a new room is still not passing: the returned list of rooms contains an empty room object:
Why is this? It turns out we’re missing the @RequestBody annotation for the addRoom method’s argument. We’ll fix this problem and get all our tests green again:
Let’s return to the implementation of the web page with the table displaying rooms’ information. Now we can change our test for this page in a similar way as we did our test for API: one test will ensure that the table contains no data initially, and the second test will ensure that after adding a new room into the system the table now displays this information. Let’s move these tests to a new test class and name it RoomsPageSpringBootTest:
Note how we have essentially copied the previously created test method and added a negative predicate around containsString method for the room details. This test fails, so let’s go and modify the rooms.html file:
This modification inverts the results of the test class and now our old test method fails:
The failing test doesn’t make sense in its present form anymore. So let’s modify it again by adding an API call to create a new room. To avoid code duplication we’ll extract this piece of code in a new method:
This leads us to the idea that we might, in fact, want an abstract class for our Spring Boot tests containing mockMvc object:
So we can simplify our Spring Boot tests by making them extend this class:
Ultimately the shouldRenderRoomsPage test method will look like this:
The test fails, so it’s time to add the needed logic in the rooms.html file and the controller, just as we see in the original video:
Here I should make a disclaimer that we could have gone to a slightly higher granularity and first implemented a unit test for the RoomsController that would verify a proper set of operations over the model object in the getAllRooms method. This remains a possibility. However, having a high-level integration test that interacts almost immediately with the controller layer seems to be a fair compromise to save some time on the test’s development without harming overall results.
Let’s finally run the maven test to ensure that all the tests pass and with that we can conclude this chapter and move forward towards the Spring Boot Data topic:
Spring Boot Data
To add the Spring Data layer to our application we’ll start by modifying the existing unit test for the RoomsService class. First, we add a new object variable, annotated with the @Mock annotation, which will represent the future repository class:
The class doesn’t exist as yet, so we’ll create a new interface by placing it into a new package named “repository”:
Note again: we deliberately haven’t added any annotations.
Next, we modify the service’s tests to reflect the usage of this repository. The test method shouldReturnEmptyListOfRooms is renamed as shouldReturnListOfRooms:
The method “findAll” doesn’t exist as yet in the repository class, so we create a new one:
We run the modified test to ensure that it fails:
And then add the needed logic:
The second step: we modify the test for adding rooms as it doesn’t work anymore:
The test is simplified:
The method “save” doesn’t exist, but instead of explicitly adding it to the interface, we make our repository extend JpaRepository:
Our IDE doesn’t recognise this class. Now is the right moment to add the required dependency:
And now we can successfully import the class:
Done. Our test is compilable, however not passing:
We add the right implementation and ensure that our test now passes:
Let’s run the auto-generated SpringBoot3EtApplicationTest. This is a very simple test that validates the integrity of our Spring Boot application:
The test fails, informing us about several problems. Let’s start with the final one:
This signals to us that now is the time to provide a spring data connection property:
We run the test again and pick up the final error again:
This signals to us the need to add the h2 dependency:
We repeat the same procedure as above and discover that our entity isn’t a managed type:
We add the required annotations:
Unlike the original video, we’ll use the default naming strategy by omitting names for the table and the columns. This time the general Spring Boot test passes:
We jump to the API integration test to check if it works. The test method for adding new rooms fails:
So, we add the needed annotation to auto-generate the Room entity’s ID:
And this repairs the test. Let’s run the maven test again to ensure that all our tests are passing and this is indeed the case:
With that, we concluded the integration of Spring Data into our project.
Code Coverage Metric
Code Coverage should never be a purpose in and of itself. It’s just a metric, but a very important metric, which shows us how well we’re doing our job by following the TDD process. If we’re doing everything right, we won’t have any uncovered code other than for a few minor exceptions (for instance, implicitly auto-generated getters and setters for model classes by the Lombok annotations).
In this section, we’ll have a quick look at how to add the jacoco library to our project and generate the metrics for the overall project.
The first step is to define the new plugin in the build section of our pom.xml file:
That’s it. To generate the metrics we’ll run the maven’s goal verification:
mvn clean verify
If everything is done properly, in the target folder of our project we will find the new directory named “site/jacoco” with a bunch of report files in it:
To see the human-readable report we’ll open the index.html file in a browser:
As you can see we’re nearly hitting 100% except for the model class. Upon looking at the problem in greater detail we discover that the auto-generated setter for the ID field of the Room entity is never used:
This isn’t a problem per se and as has been said: the metric itself should not be the ultimate goal. However, having 100% of code coverage is very easy and a quick way to eliminate the possibility of having unneeded code. So, I prefer to remove this unused method. Since we’re using lombok, this can be done by adding the following annotation on top of the object variable’s declaration:
At the end of the day, it is a useful modification because we’re not supposed to assign Room’s IDs explicitly from the code. After running the verification goal again we see that the Room class is fully covered by the tests. There is however one more method that isn’t covered — this is the main method of our Spring Boot Application class:
As I have said previously, I strive to have 100% of code coverage for the sake of not scratching our heads about what causes the missed percentages. Whether to keep going like that and having in between 90 and 100% of code coverage is your personal choice. Again, my personal preference is to get rid of these lines not covered by tests. The simplest way to do this is to replace the general application Spring Boot test with one calling the main method:
This is a good equivalent of the context test, plus we have other Spring Boot tests that tell us about possible configuration problems.
With that we achieve a perfectly green report:
Summary
In the preceding sections, we took a journey from creating a simple Spring Web application to integrating Spring Data into it. Every single step was performed first through the development of a test. We haven’t added a single annotation, dependency, class or line of code before making sure that it is required. Apart from achieving all known benefits by following the TDD process, and presenting this learning material in a somewhat tedious way, we have achieved something equally important, namely, teaching students and learners the TDD technique itself and testing techniques from Spring Boot from the very beginning of their familiarity with the Spring Boot framework. By teaching this somewhat boring, yet utterly important discipline from the outset, students develop a proper understanding of how tests and test-driven development are as equally important as writing clean production code.
This presentation was a sort of pen test for improving the current situation regarding teaching TDD. I invite the society of speakers and teachers to reconsider their approaches to preparing their teaching materials in favour of including TDD as an integral and natural part of the development process for every single stage of the subject being taught.
[1] Robert C. Martin “Clean Agile: Back to Basics”, 2020 Pearson Education, Inc., p. 116
[2] https://www.diffblue.com/DevOps/research_papers/2020-devops-and-testing-report/