Features
-
Spring Boot
-
Spring Functional Bean Definition Dsl
-
Spring KoFu (no component-scan)
-
Spring container and component testing
-
Multiple test sets
-
Multiple gradle projects
-
Gradle Kotlin Dsl
The project imagines you want to produce pizza. For that you need a catalogue of all possible ingredients and a factory with an inventory that can make pizzas.
The structure follows the ideas from the C4 Model.
-
Components are collections of classes, that have the same overall purpose. For example catalogue or factory.
-
Containers are things that are running, spring containers in our case. They can contain one or more components. In this project they are named after italian cities, naturally.
-
Build complete project
./gradlew build
-
Run individual project
./gradlew :container:rome:bootRun
-
Test only medium tests
./gradlew mediumTest
-
List available tasks
./gradlew tasks
Requires mongo to run locally. Please retrieve mongo via docker.
-
To start mongo type
docker run -p 27017:27017 -d mongo
-
Check the container id and that mongo is running via
docker ps
-
Lets say mongos id is 7f93f02e3a48, then you can view the logs via
docker logs 7f9
ordocker logs -f 7f9
*
The Gradle build tool is an alternative to Maven. It’s built system consists of two major building blocks
-
project
-
task
A Gradle build system is made up of one or more projects and each project contains one or more tasks. A project represents an artifact that needs to be built.
Gradle has a simple command-line interface.
-
View all projects
./gradlew projects
-
View the available tasks
./gradlew tasks
Please make sure you have the newest Gradle (>= v6) version installed if you run the gradle wrapper
task.
BuildSrc is a project in the root of your build. Everything written there will be shared by all your build scripts. We can use buildSrc to write our own gradle tasks, plugins or extensions functions that will be available in all build.gradle.kts
. The gradle team also recommends placing dependency versions there.
BuildSrc Plugins can be applied the same way as regular plugins. We can specify an id alias by placing a properties file in buildSrc/src/main/resources/META-INF.gradle-plugins
.
Specifying test and test options is relatively straightforward using gradle. The JavaTesting Guide and the official docs for the Test Api provide a good starting point. They also help when it comes to defining a more complicated test setup using multiple testsets.
In our case we wrote a custom buildSrc plugin to define multiple testsets that can share code between them. We could have also used the Gradle TestSets plugin, an example code base can be found here.
-
You can find lots of official Gradle Kotlin Dsl Samples online
-
The official docs provide a Migration Guide from Groovy to Kotlin
-
There is also the Gradle Kotlin DSL User Guide to consider.
Plugins can modify your build quite a lot. For example they can add tasks or new extensions to a project. They are usually added in the plugins {}
block of a build.gradle.kts
. The normal way is to supply an id("…")
.
If you want an executable jar, you would usually add the application plugin plugins { id("org.gradle.application") }
. Gradle provides aliases for the common plugin they supply. So id("org.gradle.application")
becomes \`application\`
. JetBrains provides a different alias for all the kotlin plugins and id("org.jetbrains.kotlin.jvm")
becomes kotlin("jvm")
.
The Gradle Kotlin Dsl convention is to declare plugins and their version in the root project build script without applying them. The subprojects can then apply the plugins on demand but should not specify the version.
The Spring Kotlin Bean DSL was introduced with Spring 5. An excellent resource if you want to migrate from component scan to the new dsl is Spring Boot, migrating to functional.
Manual AutoConfiguration Defintions are a thing in Spring.
According to it’s creator Sebastien Deleuze Kofu is a minimalist, efficient and explicit configuration model for Spring Boot using a Kotlin DSL. One of the best talks about actualling using Kofu is Spring Boot with Kotlin, Kofu and Coroutines by Sébastien Deleuze @ Spring I/O 2019.
Kofu examples projects have been created by: * Sebastien Deleuze with Several Kofu Samples * Rodolpho Couto with WebFlux and Coroutines
Testing is a divisive topic. "What is an unit test", "what is an integration test", "how many tests per category do we need" and "how much coverage should they have" are regular questions in most teams and clear answers are hard to agree on. This project suggests one possible answer with multiple testsets that each contain multiple types of tests. The answer works under the assumption that we want feedback as fast as possible, especially for the business logic, and that we want to support refactoring as much as possible.
Reflectoring.io writes that an integration test can a) cover multiple "units", b) cover multiple layers, or c) test multiple paths through the application.
Running the test from different category (a) might take a couple of milliseconds, b) hundreds of milliseconds and c) might even take seconds. In addition to test execution time the purpose of these three categories is also very different. Hence it is not very helpful to describe something as an integration test. We need a better definition to write better tests as a team. Thankfully there is a lot of good material out there which we can build a definition on top of.
A test is not a unit test if:
It talks to the database
It communicates across the network
It touches the file system
It can’t run at the same time as any of your other unit tests
You have to do special things to your environment (such as editing config files) to run it.
Tests that do these things aren’t bad. Often they are worth writing, and they can be written in a unit test harness. However, it is important to be able to separate them from true unit tests so that we can keep a set of tests that we can run fast whenever we make our changes.
Defined in A Set of Unit Testing Rules
The idea is really to have fast developer tests that we can run at any time to ensure we have not broken something essential. They should complete in seconds for each component. If we run the tests and are able to get a cup of coffee, they are too slow. For that to work each individual test needs to complete in a couple of milliseconds.
In object-oriented languages it is sadly common to write a new unit test every time we write a new class. That couples tests directly to their implementation and is not always advisable. The trigger for writing a new test is "implementing a requirement", as Ian Cooper explains in his talk about TDD.
If you couple writing a test to a new requirement, you’ll notice that some requirements require modifying or creating several new classes. At this point another divisive question comes along, "to mock or not to mock?", which brings along another question, "what is a mock?". Let’s see if we can break this down.
Most classes that we create cannot do their job alone. They need peers, other classes for example. That means if we test a class, make it a system-under-test (SUT), we also test the peer(s). If we do not want to do that, we can replace the peer with a test double,
Note
|
A Test Double is the name for a family of "test-specific equivalents" of real objects. We use them when the system-under-test (SUT) is hard to test. Depending on how we use the double we give it a different name to make our intent clear. The book XUnit Test Patterns declares the following variations:
All of these test doubles can be created without a mocking framework and I often prefer to do so. I find that mocking frameworks often hide bad design, inhibit refactoring and mock the wrong things. First, an interface that is too wide with too many methods or a class that has lots of constructor arguments becomes easy to instantiate with mocking frameworks. This can happen over time, I do not notice because the framework hides it and the bad design thrives. Second, it’s very easy to create test doubles with mocking frameworks and give them behavior. So we tend to do it in all places that are convenient. When the interface of the doubled production code now changes we have to update all places where we mocked the behavior or decide not to refactor. If we had used a shared hand-written stub we could have made the refactoring easy by using tricks like inlining methods or similar. Third is when things like repositories, value objects or domain entities are mocked.
Despite all this I think mocking frameworks do have their place in two cases. Sometimes I need to stub a class and cannot change the class to an interface because it’s not my code. At other times I do want a mock and the framework dsl allows me to verify interactions within the the scope of my test. I find these mocks to be most useful at the boundary of my component where I want to verify the code I wrote interacts correctly with the code someone else wrote. Sometimes it makes more sense to use a Spy though because the behavior I want to verify cannot be represented by the mock framework dsl. Much of this text has been inspired by two blog posts. Martin Fowler’s Mocks aren’t Stubs and Robert Martin’s The Little Mocker. |
In his book, Working Effectively with Unit Tests divides unit tests into social and solitary. Social unit tests can
In 2010 Google did not use fuzzily defined test names. What they instead used was the concept of Test Sizes. They grouped tests into small, medium and large depending on which features it is allowed to use.
Feature | Small | Medium | Large |
---|---|---|---|
Network access |
No |
localhost only |
Yes |
Database |
No |
Yes |
Yes |
File system access |
No |
Yes |
Yes |
Use external systems |
No |
Discouraged |
Yes |
Multiple threads |
No |
Yes |
Yes |
Sleep statements |
No |
Yes |
Yes |
System properties |
No |
Yes |
Yes |
Time limit (seconds) |
60 |
300 |
900+ |
The matrix makes it very clear what is allowed to be used in the category and what the total time limit is. They used annotation to classify the tests. That made it possible to police the limits by installing a security manager that disallows certain activities or with tools such as ArchUnit.
A unit test is clearly a small test. Interestingly most other test types can fit into all three categories. For example, a UI test can be a small test if we only test the html or json rendering. It can be a medium test if we only create a web context and it can be a large test if we require the whole spring application to run.
Therefore we think of QA as a level-up, not a gate or a check-point that all work must go through. We’re much better off with QA than without it. But we don’t depend on QA to ship quality features that work as they should.
Defined in the free book Shape Up