Self-contained “real” integration tests — with example in .NET Core
Integration tests often require you to set up several components that interact with each other, such as running a program and testing various output from it. The work is often time consuming and requires effort — so much that, in my experience, many developers tend to skip or neglect them altogether. In this blog post, we will have a look at the concept of running easy-to-set-up self-contained integration tests, with a code-example in .NET Core / C#.
Why and how to do integration testing?
In my opinion, the most important aspect when it comes to testing your application is to have tests verifying the output of the highest level of your program, meaning the “entry point” where the code starts executing. This can be so many different things, for instance HTTP entry points on a web API controller, or a “Main”-method in your entry point program class. Smaller units that the highest level code is assembled from can be replaced during the development cycle, meaning that simple unit-tests will only get you “that far” in testing the real business logic of your application.
So, if the entry point of your program is the “Main”-method in a “Program” class, it should be enough to simply test this code directly, by for instance executing “Program.Main()” in one or more of your tests, right? Well, not exactly; this will simply test that the code is outputting the expecting results when it executes from your test-project, which can have a totally different setup then when you execute the same code for running in production, for instance different application configurations. An example of this is executing the code via your xUnit test-project in .NET Core, whereas the command you would use for running the code in production would be “dotnet run” for the built .dll file in the production code assembly.
Self-contained integration tests
As mentioned earlier, simulating the way the production code would be executed in run-time for doing integration tests can require a great deal of setup, but there are ways of doing this in an easier way where all the code execution is self-contained within the test itself.
In the following code-example we will set up a simple .NET Core app with a class, “Program”, with one method “Main”. The idea of this application is that it should write some text to a file in the temp folder on our computer. Let us go through the steps together:
1 Create a solution with one “src” and one “test” folder
2 Add an empty project in your “src” folder
My project is called “FortumApp”. Fortum Oslo Varme is the customer I work with, so I will stick with that name for the app.
The .csproj file should look like this initially:
3 Add an empty project in your “test” folder
You can use the “xUnit Test Project (.NET Core)” template if you want. I will name my test-project “FortumAppTests”. The .csproj file should look like this initially:
As you can see we included a project reference to the “FortumApp” project from step 2 (change this to your own project name if you have called it something else).
4 Add an integration test
As mentioned previously, our small application should only write some content to a file in the temp-folder; in this case we will write “This is a test.” to the file. Since our example code is written in C# for .NET Core, our integration test should execute the same command as the production code will be executed with in run-time, namely the “dotnet run” command. We would break down the test like this:
- Make sure the expected output file does not exist
- Run the command “dotnet run” from the assembly where the production code is located (src/FortumApp)
- Verify that the expected file exists containing the expected test
Being a huge fan of TDD (Test-Driven Development), we will not write any code before we have a “red bar” — so let us start off with writing a test.
First, let us create the test-method (I will not refactor as we go, but rather show the refactored code from the start, to avoid a too long blog post):
Some of the imports are not used at this moment in time, but keep them; we will need them for later.
Now, let us start off with verifying that the expected file does not exist, a long with the expected text in the file:
The private method “DeleteFileIfExists” should look like this:
Now, let us create a new instance of the “Process” class, and execute “dotnet run” with it:
If you are doing the coding along with me, you may have noticed that the method “DotNetRun” on is not compiling. Well, this is a non-existing method on the “Process” class, which means that we need to write it ourselves.
Add the “ProcessUtils” class in the root of the test-project:
This method will run “dotnet run” from the folder you specify in the “WorkingDirectory” property — obviously you would have to specify your own absolute path to your source code assembly.
Finally, let us write the verification of the output from the program:
The private method “GetFileContent” should look like this:
We ignore any exceptions being thrown when trying to read the text from the expected file, and rather return null when the file content cannot be read.
So, now our test is finished. It will obviously fail badly as we do not have any production code yet, but a non-compiling test, or a “red bar” and a failing test, are what we want prior to writing the actual production code. In this case, we should get an error message from the build output when trying to run the test, stating that “Program does not contain a static ‘Main’ method suitable for an entry point”.
5 Implement the production code
Let us start writing the necessary code for the test to succeed. I will not follow TDD to the full extent now, as I will present refactoring along with getting the test to succeed.
Add a “Program” class with a “Main” method:
Run the test again — it should now compile and run. However, it will be stuck in an infinite loop because the test will never find or read the file we expect to be written, since we have not implemented this functionality yet (in the final code example available on GitHub, we will add a timeout mechanism in the test so that it won’t run forever, but let us keep moving for now).
Add a “config.json” file to the production code project, in my case the “FortumApp” project, with the following content:
Also, specify in the .csproj file that the file should be copied to the output directory, in order for the program to have it available during the code execution:
Also, add the necessary dependency needed for loading the config file into our code:
In the Main method, let us write the file content (the same content we expect in our test) specified in the “config.json” file to the “fortumfile.txt” file, which is the same file we expect in our test:
Now, run the test again and it should succeed with a “green bar”. Great success.
We have looked at how to run an integration test that simulates the exact same run-time command that is used for executing the code in production, and verifying the output of the running application. The code assembly ran the “dotnet run” command from the production code assembly as a separate process, while our test was verifying the expected outcome of the running program.
The code example is simple in order to illustrate the main points of the integration testing, and details such as handling timeout for the test (if the expected file is never written due to a bug in the production code), cancelling the for the “DotNetRun” process, and even killing the “dotnet” process itself, are not taken into consideration in the example in this blog post. It is, however, included in the final code example on GitHub.
The “dotnet run” process executed in the test will not terminate by itself as it is a separate process executed on the machine, but this can be solved by killing the “dotnet” processes executed after the test itself started running (we do not want to kill the test itself obviously).
Also, if we were to have several tests running this setup, we would have many “dotnet” processes running at the same time, which would at some point conflict with each other and not isolate the tests. We could solve this by specifying xUnit to run the tests synchronously with an “xunit.runner.json” file. This is also included in the final code example.
The full code example is available on GitHub here: https://github.com/fortumoslovarme/realintegrationtests .