Integration Tests with C# and Docker

In this post, we are going to see what integration tests are and what they are used for in our code.

 

This isn't the only post we have on the website about tests in C#. Previously, we've covered what unit tests are and what mocking within tests is. If any of these concepts are unclear, I highly recommend checking them out because they will be crucial to understanding integration tests.

 

In this post, we'll also look at an example with Docker. If you are not sure what Docker is or how it works, I also recommend giving it a read, although it isn't critical, as for this scenario we'll use it solely to contain our database.

Finally, we'll use the code from the web c# course.

 

 

 

1 - What are integration tests?

As we saw in the unit tests post, there is a testing pyramid. At the base of it are the unit tests themselves, and as the second layer, we have integration tests. In this post, we're focusing on that second layer.

piramide de test

Integration tests allow us to test the integrations in our project.

This means we're not testing whether an algorithm works correctly, but rather we're checking that the code works from end to end.

 

Therefore, the main goal of integration tests is to verify that our system (or a part of it) works properly when components are used together.

 

 

2 - Integration tests and unit tests

Integration and unit tests are used together; they are not mutually exclusive. In fact, we should have both types of tests in all projects that require them.

The easiest way to see this is with an example:

 

The main point of a unit test is to verify that a small part of our project, such as a function to check if a user is of age, works as expected.

bool EsMayorDeEdad(int edad) => edad >= 18;

On the other hand, when running an integration test, we want to know that no underage user can access the website. So we are testing that the code executes the right decision based on the result of the EsMayorDeEdad method, without caring how that value is computed.

 

 

2.1 - Why do we need integration and unit tests?

The main reason we need both types of tests is that the combination of scenarios the code can go through is almost infinite. Imagine the entry point takes 10 arguments. Writing tests for every possible combination would require hundreds or even thousands of tests.

 

But if we simply verify in very small scopes, and then all together, focusing on the most relevant or most likely cases, along with the results from unit tests, we can positively assess that our system is working correctly.

 

 

3 - Features of integration tests

Usually, when creating integration tests, we want to reproduce real scenarios, which gives us a view closer to production. Depending on how hard manual testing is, we can focus our development on Test Driven Design.

 

Many companies treat code coverage, or how much of our code is covered, as a stigma. This is both good and bad, because with only one case, you can cover most but not "exceptions". Still, integration tests help us cover the majority of the code.

 

Unfortunately, integration tests can be VERY hard to develop. For our specific case in this post, we'll actually need an additional application in our project to restore the database.

We also need to remember that tests run in parallel, so checking hardcoded IDs is not an option.

 

 

4 - Example: Integration test with C# and Docker

The reason we're implementing integration tests with Docker is that we need a completely clean database for the tests to make sure our cases and scenarios work as expected.

 

This method also allows, in the case of CI/CD, to repeat the process in one of the stages, making sure that the code as a whole also works on the server or in the cloud.

 

4.1 - Implementing the database in Docker

First of all, I do NOT recommend running your production database in Docker, but it's great for tests.

 

The main idea is to replicate our database server in a machine, but thanks to Docker, we don't need a physical machine, just the image we build and run. In it, we specify the database and the password.

FROM mysql:5.6

ENV MYSQL_DATABASE webpersonal
ENV MYSQL_ROOT_PASSWORD=test

## todos los scripts en  docker-entrypoint-initdb.d/ se ejecutan automaticamente
COPY ./Database/ ./docker-entrypoint-initdb.d/

 

With this Docker image running MySQL, we need to execute the SQL files initializing the database.

 

You can either do this step in the Docker image itself, by creating a .NET console application or a small PowerShell script.

 

In my case, I have it in a small PowerShell script that should be run before you start working with the integration tests. This not only runs the SQL files, but also spins up the Docker image.

##Copy database files
$source = "src/Database"
$destino = "Tools/ServerMysql"

Copy-Item -Path $source -Filter "*.sql" -Recurse -Destination $destino -Container -force

##Remove old image
docker rm $(docker stop $(docker ps -a -q --filter ancestor='server-mysql' --format="{{.ID}}"))


##build the image
docker build -t server-mysql Tools\ServerMysql\.

##start the container
docker run -d -p 4306:3306 server-mysql

Here you can see we're specifying which port to listen to. Normally MySQL uses 3306, but if you already have MySQL running locally, you can use any random port, in my case 4306.

 

As we discussed in the application structure post, you should place this file in the right spot, for us that's in the project root inside a Tools folder where we keep things that make our lives easier but aren't directly part of the codebase.

 

It's recommended to run the script directly from the root of the project.

So, we run the following command to build our MySQL server with the required information inside a Docker container:

PS C:\repos\WebPersonal> .\Tools\ServerMySql\BuildServerAndRun.ps1

 

4.2 - Creating an integration test with Visual Studio

To create an integration test in Visual Studio, you'll want to create a test project called TestDeIntegracion, exactly as we did with unit tests.

Of course, place it in its corresponding folder.

 

The goal of integration tests is to cover the entire system, both front and back end. In our case, there is no front end yet, which doesn't mean we can skip integration tests, once the front end is ready a refactor will be necessary.

 

 

First of all, note that if your project uses dependency injection, you'll need to inject those dependencies into your test. I'll make a video about advanced dependency injection refactoring.

 

In this example, the dependencies are set "as is"

private IServiceCollection BuildDependencies()
{
    IServiceCollection services = new ServiceCollection();
    services.AddScoped<DbConnection>(x
        => new MySqlConnection("Server=127.0.0.1;Port=4306;Database=webpersonal;Uid=root;password=test;Allow User Variables=True"))
        .AddScoped<TransactionalWrapper>()
        .AddScoped<PersonalProfile>()
        .AddScoped<PutPersonalProfile>()
        .AddScoped<IGetPersonalProfileDependencies, GetPersonalProfileDependencies>()
        .AddScoped<IPutPersonalProfileDependencies, PutPersonalProfileDependencies>()
        .AddScoped<PersonalProfileRepository>()
        .AddScoped<SkillRepository>()
        .AddScoped<InterestsRepository>()
        .AddScoped<UserIdRepository>();

    return services;
}

 

You can also see the MySQL connection is pointing at our Docker container. This step will also need refactoring once we look at implementing different environments.

 

Next, we create the test that checks that the flow or process works:

[Fact]
public async Task TestInsertPerfilPersonal_Then_ModifyIt()
{
    IServiceCollection services = BuildDependencies();
    using (ServiceProvider serviceProvider = services.BuildServiceProvider())
    {
        string username = Guid.NewGuid().ToString();

        PersonalProfileDto defaultPRofile = BuildPersonalProfile(username);
        var departmentAppService = serviceProvider.GetRequiredService<PerfilPersonalController>();
        await departmentAppService.Post(defaultPRofile);

        PersonalProfileDto userStep1 = await departmentAppService.Get(username).ThrowAsync();
        Assert.Empty(userStep1.Skills);
        Assert.Equal(defaultPRofile.FirstName, userStep1.FirstName);
        Assert.Equal(defaultPRofile.Website, userStep1.Website);
        Assert.Equal(defaultPRofile.LastName, userStep1.LastName);

        SkillDto skill = new SkillDto()
        {
            Id = null,
            Name = "nombre1",
            Punctuation = 10m
        };
        userStep1.Skills.Add(skill);

        InterestDto interest = new InterestDto()
        {
            Id = null,
            Interest = "interes pero debe contener 15 caracteres"
        };
        userStep1.Interests.Add(interest);
        var _ =await departmentAppService.Put(userStep1);

        PersonalProfileDto userStep2 = await departmentAppService.Get(username).ThrowAsync();

        Assert.Single(userStep2.Skills);
        Assert.Equal(skill.Name, userStep2.Skills.First().Name);
        Assert.Single(userStep2.Interests);
        Assert.Equal(interest.Interest, userStep2.Interests.First().Interest);
        Assert.Equal(defaultPRofile.FirstName, userStep2.FirstName);
        Assert.Equal(defaultPRofile.Website, userStep2.Website);
        Assert.Equal(defaultPRofile.LastName, userStep2.LastName);
    }
}

private PersonalProfileDto BuildPersonalProfile(string uniqueUsername)
{
    return new PersonalProfileDto()
    {
        Description = "Description",
        Email = "email",
        FirstName = "firstName",
        GitHub = "github",
        Id = null,
        Interests = new List<InterestDto>(),
        LastName = "last name",
        Phone = "telefono",
        Skills = new List<SkillDto>(),
        UserId = null,
        UserName = uniqueUsername,
        Website = "web"
    };
}

It's very common to run results directly against the database. In our case, "get" only reads from the database, so the effect and result are the same.

 

 

Conclusion

Integration tests are a process and a step that we must include in all our developments to make sure that our code works as expected when everything is put together.

 

As we have seen, they are complicated to execute because you need to spend time implementing all associated services (when included in the project), such as the database in Docker for this scenario. That's why we should combine integration tests along with unit tests for complete application testing.

 

This post was translated from Spanish. You can see the original one here.
If there is any problem you can add a comment bellow or contact me in the website's contact form

Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2025 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café