Writing automated tests is an essential part of the development process, but it's not unusual to find codebases with little or no test coverage. Compared to unit tests, integration tests bring even more challenges since they get the same results running in different OSs (different developer machines and CI pipelines), but they are necessary in evaluating the effectiveness of different software modules when they are interconnected.

What Are Integration Tests?

In this article, we are considering integration tests as a specialized type of automated test where software functionalities are combined and tested as a group. Integration tests (also called service tests) are at the middle of the test pyramid, and their purpose is to verify that separately developed modules work together properly. Unlike unit tests, integration tests can depend on databases and external APIs.

Integration Tests in .NET 5

The .NET 5 framework makes some great possibilities available for integration tests. One of these is the EF Core In-Memory, which is a very good option and fits in different scenarios, but it doesn't exactly replicate a database behavior. So if you're searching for an approach to use with different ORMs (Entity FrameworkNHibernateDapper, etc.) or an approach that will bring you other possibilities (like running an environment as code), we think docker-compose with integration tests will be a very useful approach for you.

Using Docker and Docker-Compose

We chose a TimeSheet application as an example for this article. In this application, the employee of a fictional company will submit hours he/she worked throughout the week. The API layers are separated by folders, and there are only two .csproj files, one for the API, and the other for the tests. The code is hosted on GitHub.

The docker-compose starts three containers: timesheets-api, sql-server-database, and integration-tests. The first and third ones are the same image (dotnet/sdk:5.0), timesheets-api to run the API inside the container, and integration-tests to set up and run tests. Both services have a configuration map to host volumes to be used inside the containers. This setting allows the container to use the source code implemented on the host machine.

Screenshot (106)

The timesheets-api container is started with a dotnet run. The integration-tests has a more complicated command. The script wait-for-it.sh keeps checking if the provided port of the service (sql-server-database:1433) is available. Once it gets the right response, the dotnet test executes the tests.

The sql-server-database, as the name says, works as the containerized database to run the API pointing to the database (environment as code), or as a test database. The image is an SQL Server (mssql/server). Also, we need to configure the database service in the compose file to expose the 1433 port and bind it in the host's same port.

Screenshot (105)

The database connection string is created on the appsettings.json:

Screenshot (107)

As you can see, our application will point to a localhost database when running. It's important to override the connection string to work on containers network, which was done in the environment step of integration-tests and timesheets-api. We chose an approach that uses only one appsettings.json and many overrides. This approach helps developers to avoid spreading appsettings.json files along with the code (which we consider a code smell), and even if the developer chooses to use different .env files in the docker-compose environment step, these will be centralized in the same place in the folder's hierarchy.

To run the project locally (if you want to debug in your favorite IDE, for example), you only need to use the SQL Server: docker-compose up -d sql-server-database. Otherwise, if you want to use an environment as code approach, run in your bash: docker-compose up -d timesheets-api; that command will initialize the database (SqlServer) and the API on docker. Finally, if you want to run the integration tests, run this command: docker-compose up integration-tests. That command will run the .NET 5.0 and SQL Server containers and then execute the tests.

Setting Up the CI

We use the Github Actions to run the tests when any PR or modification at the master branch is done.

To set up the workflow, create a new file in the .github/workflows named integration-tests.yml

Screenshot (108)This action starts the integration-tests container with all dependencies. The argument --exit-code-from uses the exit code of the selected service container as the result of the action.

When the file is committed to the GitHub repository, the action will be available to be triggered on every commit to the master branch or when a PR to the master branch is created.

To see the workflow results, click Actions.

Screenshot (105)-1

It is also possible to expand the logs to see the details. If a test fails, we can take a look at the results to troubleshoot.

Screenshot (106)-1

Conclusion

Running integration tests with docker-compose is a very helpful option to replace in-memory databases. The article showed how to quickly and easily configure a production-ready integration test approach and demonstrated how to run integration tests using the new .NET 5 containers with docker-compose and github actions for a Continuous Integration. It's helpful to test the application from a request to a database persistence, and this approach can also be applied using technologies other than .NET and github actions.

Check out more about integration tests in ASP.NET Core here, and tell us how this approach works for you in the comments below!

*This post was co-written by Luiz Lelis, Rafael Miranda, and Stefano Bretas.

References

IntegrationTest by Martin Fowler

Engenharia de Software Moderna


Author

Alvaro Kramer

Alvaro Kramer is a .NET Engineer at Avenue Code. He graduated with a degree in Information Systems from PUCRS. His professional experience is focused on .NET technology. Outside work, he is a traveler and a sports, beach and craft beer fan.


How to Build Unit Tests Using Jest

READ MORE

How Heuristics Can Improve Your Tests

READ MORE

How to Use WireMock for Integration Testing

READ MORE

The Best Way to Use Request Validation in Laravel REST API

READ MORE