Usual production patterns applied to Integration tests

Igor Vlahek
The Startup
Published in
11 min readOct 15, 2019

--

To be honest, the patterns that will be shown in this story are not something new, but people tend to see testing as part of the code where copy-pasting is allowed. They see testing as something that is forced upon. Usual patterns that can be applied in the test code, that would otherwise be applied in the production code, are not applied. In this story, I will present the patterns I use every day to build a test infrastructure that in the end make writing tests easier and faster.

Typical application

In this story, I will try to summarize all the concepts I use when writing integration tests. To be clear, for me integration tests are the ones that are written before UAT (user acceptance tests). Those that are testing single method/API call using the real database (external system). I will also write all the alternatives I came across during my programming career and will elaborate on why those alternatives should be avoided. All the examples will be shown in pseudo-code (influenced with Java) but can be applied in other programming languages.

All of the good practices mentioned in this story I have put in a small project available on Github (written in Java).

Basics: Build, operate, check pattern

Let’s start with the basics.

Anatomy of an integration test

Every integration test should have the following phases:

  • Build — phase in which you prepare the test scenario. In this phase you normally put some data in the database;
  • Operate — phase in which you execute the method on the object/API you are testing;
  • Check — phase in which you check whether the executed method has made an expected impact on the system. In this phase, you normally query the database and check the result of the method execution.

How to get to the data in and out of the database?

Anatomy of a test

If we look at the picture in which the anatomy of a test is shown (picture above), you will see that we are accessing the database in each phase. In the build phase, we somehow need to insert the data. In the operate phase we need to call method to test the system. In the check phase, we need to access the data to check the impact of the method on the system.

Operate phase is simple, you just call the method and the operate phase is finished. But for the build and operate phase we can think out more than one way to insert (BUILD) and select (CHECK) the data from the database. Throughout my career I have seen the following used in BUILD/CHECK phase to access the database:

  • Use exposed methods of the API of the system we are testing;
  • Use pure SQL;
  • Use a Repository layer.

Use exposed methods of the API of the system we are testing

Exposed methods of the system used for build and check phases

I have seen people using methods of the system they are testing to prepare (BUILD ) and get (CHECK) the data from the database. This is for me an anti-pattern. You can not use the exposed method of the system you are testing to prepare/fetch the data for your tests. Why? Because if you call three methods in your test case, one to build test data, one to call the test method, and one to fetch the data from the database, which one of those three are you testing? The one that is preparing the data, or the later ones?

//use exposed API of the system you are testing to prepare a //scenario//BUILD
CreateUserRequest request = buildCreateUserRequest();
UserDto user = client.createUser(request);
UpdateUserRequest updateRequest = createUpdateUserRequest(user);
//OPERATE
client.updateUser(updateRequest);
//CHECK
UserDto userDto = client.getUser(request.getId()):
assertUser(userDto, updateRequest);

Let’s say you have changed something in your application. Your test fails, why does it fail? Is it because the build method call, operate method call, or check method call? If someone reports a bug for the updateUser method call (snippet above), and you look at the test above. Why did the test pass in the first place? Maybe updateUser method is working as expected, but the createUser method has prepared the data wrong? Maybe when you fetched the data using getUser method you mapped something wrong and your test passed? Too many question and no answers.

Imagine that you have a more complicated build phase. For example, you need to create four objects in your system before you can test the target method. If you use the mentioned way, you will call five API methods. Four to build the test scenario, and one that you are actually testing. We change the code, and your tests fail. Which API method call caused this test to fail? Which API method call should we change?

For me, this is an anti-pattern. Don’t use API of the system you are testing to prepare/check the data for your integration tests.

You should write your integration tests in isolation to other parts of the system. Test one method at the time. Treat your test code as production code. A method should do one thing and one thing only. A test method should test one thing and one thing only.

Use pure SQL

Custom db layer used for build and check phases

This approach is not so common but I have seen it. It is better than the previous one and one hundred percent correct but I don’t like it and I don’t use it. Why? Because I have migrated a lot of tests written this way and it is a torment. Why? When dealing with your custom test DB layer you must implement all that is implemented in your ORM or framework (Spring) you are using. This is a part of the code you are using only in your tests. That said, common patterns that exist in production code, doesn’t apply here for most of the people. This custom DB layer is full of copy-pasting. Also, you are not dealing with objects, you are dealing with ResultSet (Java). It is harder to operate with this layer because most commonly you do not invest enough time to create a mapper that will map from ResultSet (Java) to an object. And why would you? You are investing your time to build something that you already have and you use in your application. Yes, I am talking about the repository layer in your production code. Why reinvent the wheel, because it is already invented? And if it is good enough for your production code then it is good enough for your test code.

Use a Repository layer

Use a repository layer for build and check phases

I must admit, this is a similar approach to the first shown. In this approach, we are using a layer of the system under test to insert (BUILD) and fetch (CHECK) data from the database, but only the first layer. In the first approach, Use exposed methods of the API of the system we are testing, we are using the whole application. Our application usually consists of at least two or three layers (repository, service …). Each layer adds complexity to your application. With each added layer, the possibility of a bug increases. When using a repository you are using the first layer in which you are directly accessing the database. The possibility of a bug is much smaller than when using other alternatives.

//use repository layer of the system you are testing to prepare 
//a scenario
//BUILD
Organization organization = createDefault();
organizationRepository.save(organization);
User user = createDefaultUserAccount(organization);
userAccountRepository.save(organisation);
//OPERATE
client.updateUserAccount(updateRequest);
//CHECK
user = userAccountRepository.getUserAccount(user.getId());
assertUser(updateRequest, user);

Me personally, I use the repository layer for build/check phase. And I use only the basic queries as provided by the ORM (Hibernate) or/and framework (Spring Data) to reduce the possibility of a bug:

Those methods are provided by the framework by themselves and are used in thousands of applications in production. There are no bugs in their implementation. A bug can be only found in your mapping definition and those can be noticed very early in your development process.

Even if you don’t use ORM, you most probably use some kind of framework to access the database. Use their capabilities to access the database. But only the most primitive ones. The possibility to encounter a bug on that layer will be less than by using Use exposed methods of the API of the system we are testing approach.

Further considerations — default entities

In the story so far, we have covered how to insert and fetch the data in our tests. One thing that we did not talk about is how to construct default entities for our tests.

//use repository layer of the system you are testing to prepare 
//a scenario
//BUILD
Organization organization = createDefault();
organizationRepository.save(organization);
User user = createDefaultUserAccount(organization);
userAccountRepository.save(organisation);
//OPERATE
client.updateUserAccount(updateRequest);
//CHECK
user = userAccountRepository.getUserAccount(user.getId());
assertUser(updateRequest, user);

In the pseudo-code provided we have created a method createDefaultUserAccount in which we have defined how our default User will look like. This is a simple method that accepts zero parameters and returns a default user.

There are a few problems associated with the factory-method approach:

  • the more test cases we add the more fields we will want to manipulate using factory method. At some point in time, we will want to make all fields on the entity settable through method parameters. This means we are going to have n parameters in our factory method. This is not a problem if our entity has four fields or less. But if we expose five or more fields through the method parameters then we have the problem. Our method will look something like createDefaultUser(String username, String name, String lastName, Date dateOfBirth, String sex, Organization organization). Six parameters in the method signature indicate no sign.
  • in most of the tests we want a default entity with all fields set. We want to change only the fields in our default entity that are dynamic like relationships to other entities (Organization for example). This means we will need to expose a method like createDefaultUser(Organization organization).
  • as the system evolves, you will need to create more entities in which you will focus only on one or two fields. For example, you have exposed an API that is dealing with the date in your User entity. You are only interested in the date domain so you create method createDefaultUser(Date date, Organization organization); as the system evolves you will have more this specific purpose factory methods, for example, createDefaultUser(String name, Organization organization). These factory methods will pile up. This will add unnecessary complexity to your test infrastructure.
  • the last problem is: where to put all of these factory methods for the User. You can put it in the class where you first needed it. For example in a UserRepositoryTest class. Or maybe you can put in the UserServiceTest class, or maybe in the UserTestFactory class.

We have raised a lot of questions that are all solved with the Builder pattern.

Builder pattern for default entities

Builder pattern

The builder pattern is a solution for every problem that was raised in the section before. You have a class that knows how to build another class. And you know where to find it.

  • In the UserBuilder class, we have all the default values for the User set. If we want, we can change those values using fluent interface methods.
  • In the UserBuilder we have all the default values set for the fields that are not dynamic. We can easily call withOrganization() to add dynamic values that can not be hard-coded.
  • UserBuilder class is located in the same package as the User it is building. All developers working on the solution will know where to find the class that is responsible for building the default entity.

There is one drawback to this solution. If you have 30 entities, you will need to write 30 builders. Well, that is not a problem. Almost every IDE for Java has a plugin that will generate a builder from a target class for you. You will need to map your entity class and this will take time. But your builder class will be done in a matter of seconds. You will only need to set some default values in your builder class.

Why stop there?

Don’t stop there

Builder pattern can be used to “capture” default state for all requests that are exchanged in your system. For example, if you have an internal object CreateUserRequest that is used in your application, why don’t you create a CreateUserRequestBuilder for that class also? It is generated in seconds and all the developers will know which class to call to get a default representation.

Why stop there? Builder pattern can also be used to capture default states for all external request (DTOs) that is accepted by your system.

Also, I have used the builder pattern to create a class BadRequestAsserter. Using this class you can create a test for bad requests to your API in a minute.

Check part of the test extracted to Asserter classes

We have missed one part that each test has. Check part of the test. We use the Repository layer to fetch the data from the database. But how do we check that the data is in the expected state? Well, most of the people will write the assert part for one test and then copy-paste it to other tests. If the developer is consistent enough, it will extract it to methods.

Why don’t we extract the check part of the code in special EntityAsserter classes which are located in the asserter package in the package where Entity class is defined. Other developers can reuse your asserter in their tests if needed. If you need to add one more field to check, you know where to add that check. Capture the assertions in one class so everyone who is contributing to the code base can find and reuse it.

For the most asserter classes, I create a static method assertEntity(RequestObject request, Entity entity). The method accepts two objects: the first one that defines how the object should look like and second, the object that should be checked.

If I need more flexibility, I will create an asserter class as a Builder. Asserter will have assertEntity(RequestObject request, Entity entity) method and fluent interface methods. Entity fields that can not be checked against the RequestObject will be set using fluent interface methods. We will combine value set by the fluent interface methods with the RequestObject to check that the Entity is in the correct state.

Is there more?

Explore

Yes, there is more. For example, I put more complicated build phases in a custom class whose only logic is to build some test scenario prerequisites. I favor composition over inheritance. Just treat your test code as production code. Follow the same rules as for the production code. At first, it will take more time to build test infrastructure, but it will make you a worthwhile at the end.

--

--