Identifying the anti-patterns
Many people have understood it and this technique is now widespread. Unfortunately, we often see that the functional tests used to describe the behavior of an application, are implemented as a http client hitting the endpoints.
The main drawback of this (anti-)pattern is the mix of the testing concerns. With this type of test, we have something that has the responsibility to verify:
- the business logic of the application (functional tests)
- the contract of the external API (contract tests)
- the workflow offered to the consumer – or something similar to it (end to end tests)
- sometimes the mapping between the domain objects and the adapters (unit tests / integration tests)
- the integration of the different sub-components of the application such as controllers, domain services, repositories… (integration tests / component tests)
- and most of the time without knowing it: the living documentation (contract testing)
A simple serialization problem will fail this type of test, and as a developer we will have to determine whether a regression has been made in the business logic or if there is a problem with the configuration of a framework.
What becomes a real hell when upgrading a structural framework such asSpring Boot after playing search-and-destroy with the compilation errors…
Sustainability is not the only victim of this technique. What framework to use when a test has so many responsibilities? Cucumber could be one of the best choices to express the rules and the acceptance criteria of the business.
However, testing the contract of a Web API with it, can be really cu-cumbersome and the developement efficiency might be left behind.
Functional testing close to the business logic
In this article, we will use code samples from this hexagonal architecture example project. This is a simple application that recommends tech talks to a user based on his preferences.
Cucumber is one of the most popular tools for defining the features and the use cases of an application. This is the tool we use in this article to implement the Acceptance Test-Driven Development phase of BDD. If you want to learn the basic concepts of Behavior-Driven Development or Cucumber, you can take a look at Behavior-Driven Development from scratch.
Back to the basics, the objective of a functional test is to “tests the business logic”, so putting it inside the domain of our application seems like a good idea. As a result, instead of calling the endpoints, those tests are plugged on top of the API of the domain (not the Web API one).
Let’s remind here some best practices:
- the scenario must be functional and not technical
- the persona must be defined e.g.
As a frequent userbecause a feature performed by a different persona is really likely to have different acceptance criteria. Think about the differences you may have when an admin or user uses the same feature
- only one
When, you should test one thing at a time, what’s about the acceptance criteria with several
- do not use conjunctions in your
Then, it makes them really difficult to reuse, prefer to split your step into several ones
The step definitions are the fixtures of your test, they implement all the Gherkin’s directives of your features:
Like said above, instead of hitting the endpoints of the application, we are using directly here the domain API, through CreateProfile – a domain service.
So, basically, all the
When of our step definitions call the methods of the domain services exposed through the API i.e. the features. In
Then, we respectively creates and asserts on the domain objects. And if necessary, we can initialize or assert on some data through the SPI.
You can see this in the example above, in a scenario not shown here, the fact that the user already has a profile is a prerequisite.
So a profile is bootstrapped for this user and directly stored in the “database” using the Profile repository in the SPI.
You can also use the same technique when using a side-effect feature to make some assertions in a
Most of the time, developers organize their step definitions in the same way as their scenarios. As you can read in the cucumber documentation itself, it couples everything together and the steps you’ve done cannot be reused for another scenario.
The step definitions should be organized like your domain concepts and, moreover, named after them. This way, your step definitions code will be factorized, easier to find and navigate through. The step definitions will also be easier to maintain and reuse. In other words, the step definitions must be designed with the same processes we use to create our business domain!. In our sample application TalkAdvisor, the main domain concepts are:
- Criteria (ValueType) representing some carasteristic wishes on Talks like duration, format topic…
- Profil (Entity) assigning Criteria to a User (making them Preferences).
- Recommendation (Aggregate) recommending Talks to a User based on his Criteria.
And the fixtures are organized according to those concepts:
You may be wondering how we can share data across the different step definitions, which is mandatory if we need to reuse them. Cucumber offers an integration with the Pico container, a lightweight dependency injection framework.
Once the dependency is added to your pom,
you just need to set a Bean Factory in which you will register the beans you need using a custom pico factory:
Then define a cucumber.properties in your test resources, to register this factory in the Cucumber context.
With this, no need to have a test dependency on the entire Spring Framework! Pico is really easy to set up and easy to use.
You can now inject your Domain Services and Stubs into your step definitions and share a context between them.
Last Tip: Instead of having a single launcher for all the functional tests, you can have one launcher per scenario. This is pretty convenient when you only want to run the scenarios of a single feature without playing with tags and configuration. In a test annotated with
@RunWith(Cucumber::class), you can use
@CucumberOptions and set the list of tags or features files to filter the scenarios that will be lunched. For example: