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 Framework, NHibernate, Dapper, 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.
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.
The database connection string is created on the appsettings.json
:
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
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.
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.
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.