.NET Steps – Integration Tests

So, what is the industry standard today? Are we still writing tests or was it, just like everything else in this god forsaken industry, a phase? The thing is, I don’t see that many posts as I used to. Maybe I deleted the correct people from social media?

If you still didn’t get the hint, I fucking hate writing tests. And the thing that I hate even more is reading and fixing them. Who knows, maybe it is just my luck, but I honestly don’t remember the time an actual test helped me figure out the business use case. Why? Because instead on focusing on writing good tests, we are focusing on meeting code coverage. And you know where you end up with that approach? With a lot of useless tests that you need to delete.

What are integration tests?

Integration tests verify that your system works well with external systems. So what are external systems? Honestly, it is anything your application communicates with – different microservices, messaging queues, reddis, databases. I like to split those external systems in two categories:

  • You have control over – eg. database/microservice you created, your own instance of reddis, kafka etc. Since you have control over those systems, they are easier to test.
  • You don’t have control over – usually some third-party system – something that other team in your company build or some api that you are paying to use. Testing those is a bit harder since you don’t have a control over. If we take the case “api that you are paying to use”, it gets pretty obvious why we wouldn’t want to ping it directly in our tests.
What are we building

I don’t really like speaking about this part of my career, but a few years ago I worked on Java project. I know, I know, I didn’t like it either. But I have to give credit where credit’s due. Java has one awesome librariy for integration testing – TestContainers. And guess what – there is also a library for .NET as well.
So let’s try to write integration tests for a simple .NET Core Web Api which has 2 simple endpoints.

  • GET api/v1/users -> gets users from the database
  • GET api/v1/pets -> gets pets from some external API that we are “paying to use”
Implementation

In order to start writing integration tests, we need to analyze our application. Luckily, at the moment, it is a really simple application that consists only of two endpoints. The easier one, communicates with database and the other one, with some external api.

Integration testing – database

So after we analyzed our application, we can see a couple of things:

  • we are using Postgres SQL
  • we are using ORM – Entity Framework
  • we are using code first approach
  • we have migrations in our migrations folder
  • everything that we need to tests is placed in Company.Shorts.Infrastructure.Db.Postgres
  • since we decoupled everything, we can just test IUserRepository

Let’s talk about approach I’m going to take:

  • I want to use XUnit for my testing
  • I want to use FluentAssertion for my assertions
  • When I press run tests, I want to start our database in Docker – this is where TestContainers comes in
  • And I want to do this only once – this is where xUnit fixtures comes in.
  • After docker instance is up, I want to run my migrations
  • Before each test, I want to insert some state in the database
  • After every test, I want to clean all data from the database (so that next test starts with fresh state)

So, let’s start

  • In our test folder lets create a new XUnit test project: Company.Shorts.Infrastructure.Db.Postgres.Test
  • Let’s add some dependencies: FluentAssertions, TestContainers, TestContainers.PostgreSql to Company.Shorts.Infrastructure.Db.Postgres.Test.csproj. File should look like this:
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>

    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <ItemGroup>

    <PackageReference Include="FluentAssertions" Version="6.11.0" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.5" />
    <PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.1" />
    <PackageReference Include="Testcontainers" Version="3.2.0" />
    <PackageReference Include="Testcontainers.PostgreSql" Version="3.2.0" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
    <PackageReference Include="xunit" Version="2.4.2" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="coverlet.collector" Version="3.2.0">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

  <!--Copies Files from Resource Folder to bin\debug\net7.0-->
  <!--Easier way to read files in PostgresSeedAttribute-->
  <ItemGroup>
    <ContentWithTargetPath Include="Resources\**">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <TargetPath>Resources\%(RecursiveDir)\%(Filename)%(Extension)</TargetPath>
    </ContentWithTargetPath>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\Application\Company.Shorts.Application.Contracts\Company.Shorts.Application.Contracts.csproj" />
    <ProjectReference Include="..\..\src\Infrastructure\Company.Shorts.Infrastructure.Db.Postgres\Company.Shorts.Infrastructure.Db.Postgres.csproj" />
  </ItemGroup>

</Project>
  • Since our implementation of UserRepository is internal, we also need to make Company.Shorts.Integration.Db.Postgres visible to our test project. Go to Company.Shorts.Integration.Db.Postgres.csproj and add this:
  <ItemGroup>
    <InternalsVisibleTo Include ="Company.Shorts.Infrastructure.Db.Postgres.Tests"/>
  </ItemGroup>
  • In order to share different context between tests, in XUnit we need use something called collections. You can read more about it here. For it to work, we need to create a fixture that implements IDisposable. The “magic” happens in PostgresDatabaseFixture.cs
    • We create our postgres container
    • We run aformentioned container
    • We create an instance of DbContext
    • We run our migrations.
    • At the end, we are setting connection string to environment variable so that we can access it later in our custom PostgresSeed.cs attribute
namespace Company.Shorts.Integration.Db.Postgres.Internal.Fixtures
{
    using Company.Shorts.Infrastructure.Db.Postgres.Internal;
    using Company.Shorts.Integration.Db.Postgres.Internal.Postgres;
    using DotNet.Testcontainers.Builders;
    using Microsoft.EntityFrameworkCore;
    using System;
    using System.Data;
    using Testcontainers.PostgreSql;

    public class PostgresDatabaseFixture : IDisposable
    {
        private bool _disposed;
        private readonly IEnviromentVariableManager eventVariableManager = new PostgresEnviromentVariableManager();

        public PostgresDatabaseFixture()
        {
            this.PqsqlDatabase = new PostgreSqlBuilder()
                .WithImage(PostgreSqlContainerConstants.Image)
                .WithDatabase(PostgreSqlContainerConstants.Database)
                .WithUsername(PostgreSqlContainerConstants.Username)
                .WithPassword(PostgreSqlContainerConstants.Password)
                .WithExposedPort(PostgreSqlContainerConstants.Port)
                .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(PostgreSqlContainerConstants.Port))
                .Build();

            this.PqsqlDatabase.StartAsync().Wait();

            var options = new DbContextOptionsBuilder<PostgresDbContext>()
                .UseNpgsql(this.PqsqlDatabase.GetConnectionString())
                .Options;

            this.DbContext = new PostgresDbContext(options);

            this.DbContext.Database.Migrate();

            this.eventVariableManager.Set(PqsqlDatabase.GetConnectionString());
        }

        internal PostgresDbContext DbContext { get; }

        public PostgreSqlContainer PqsqlDatabase { get; }

        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    PqsqlDatabase.DisposeAsync().AsTask().Wait();
                    this.DbContext.Dispose();
                }

                _disposed = true;
            }
        }

        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }
}
  • Also, let’s add constants
namespace Company.Shorts.Integration.Db.Postgres.Internal.Fixtures
{
    public static class PostgreSqlContainerConstants
    {
        public const string Image = "postgres:15.1-alpine";
        public const string Database = "ShortsUserDb";
        public const string Username = "postgres";
        public const string Password = "postgres";
        public const int Port = 5432;
    }
}
  • If you read the documentation, you know that we need to add one more file in order to mark this fixture as part of our collection. You didn’t read it, right?
namespace Company.Shorts.Integration.Db.Postgres.Internal.Fixtures
{
    using Xunit;

    [CollectionDefinition(CollectionFixtureConstants.Integration)]
    public class IntegrationTestCollection
        : ICollectionFixture<PostgresDatabaseFixture>
    {
    }
}
  • And of course, constants
namespace Company.Shorts.Integration.Db.Postgres.Internal.Fixtures
{
    public static class CollectionFixtureConstants
    {
        public const string Integration = "Integration";
    }
}
  • And that’s it, let’s try to write some tests. I’m not going to explain to you what you need to test, it’s up to you. I just want to show you that we are doing expensive operation once – and for that, put a breakpoint in fixture and start counting.
namespace Company.Shorts.Infrastructure.Db.Postgres.Tests
{
    using Company.Shorts.Application.Contracts.Db;
    using Company.Shorts.Infrastructure.Db.Postgres.Internal.Repositories;
    using Company.Shorts.Integration.Db.Postgres.Internal.Fixtures;
    using Company.Shorts.Integration.Db.Postgres.Internal.Postgres;
    using FluentAssertions;
    using Xunit;

    [Collection(CollectionFixtureConstants.Integration)]
    public class UsersTest
    {
        private readonly IUserRepository userRepository;

        public UsersTest(PostgresDatabaseFixture fixture)
        {
            this.userRepository = new UserRepository(fixture.DbContext);
        }

        [Fact]
        [PostgresSeed("/Resources/Users/get-users.json")]
        public async void Test_Example_One()
        {
            var users = await this.userRepository.GetUsersAsync();

            users.Count.Should().Be(2);
        }

        [Fact]
        [PostgresSeed("/Resources/Users/get-users.json")]
        public async void Test_Example_Two()
        {
            var users = await this.userRepository.GetUsersAsync();

            users.Count.Should().Be(2);
        }
    }
}
namespace Company.Shorts.Infrastructure.Db.Postgres.Tests
{
    using Company.Shorts.Application.Contracts.Db;
    using Company.Shorts.Infrastructure.Db.Postgres.Internal.Repositories;
    using Company.Shorts.Integration.Db.Postgres.Internal.Fixtures;
    using Company.Shorts.Integration.Db.Postgres.Internal.Postgres;
    using FluentAssertions;
    using Xunit;

    [Collection(CollectionFixtureConstants.Integration)]
    public class UsersTwoTest
    {
        private readonly IUserRepository userRepository;

        public UsersTwoTest(PostgresDatabaseFixture fixture)
        {
            this.userRepository = new UserRepository(fixture.DbContext);
        }

        [Fact]
        [PostgresSeed("/Resources/Users/get-users.json")]
        public async void Test_Example_One()
        {
            var users = await this.userRepository.GetUsersAsync();

            users.Count.Should().Be(2);
        }
    }
}
  • Like I already mentioned, before every test I want to seed database with some values, and after every test, I want to clear all values from the database. I’m doing it via custom PostgresSeed attribute
namespace Company.Shorts.Integration.Db.Postgres.Internal.Postgres
{
    using Company.Shorts.Integration.Db.Postgres.Internal;
    using Company.Shorts.Integration.Db.Postgres.Internal.Common;
    using System.Collections.Generic;
    using System.Reflection;
    using Xunit.Sdk;

    public sealed class PostgresSeedAttribute : BeforeAfterTestAttribute
    {
        private GenerationResult generationItem = default!;

        private readonly ISeedDatabaseManager dbConnectionUtility = new PostgresSeedDatabaseManager();

        private readonly ISqlGenerationManager sqlGenerationManager = new PostgresSqlGenerationManager();

        private readonly IFileManager fileManager = new FileManager();

        public PostgresSeedAttribute(string filePath)
        {
            this.FilePath = filePath;
        }

        public string FilePath { get; }

        public override void After(MethodInfo methodUnderTest)
        {
            this.dbConnectionUtility.Delete();

            base.After(methodUnderTest);
        }

        public override void Before(MethodInfo methodUnderTest)
        {
            var obj = this.fileManager.Read<Dictionary<string, object>>(this.FilePath);

            this.generationItem = this.sqlGenerationManager.Generate(obj);

            this.dbConnectionUtility.Execute(generationItem.Insert);

            base.Before(methodUnderTest);
        }
    }
}
  • Implementation of all those interfaces can be found on the repository on branch: feature/integration.
  • Also, I keep my resource files in Resources folder, and in order to avoid funny string replacement when reading the files, I needed to add something in the csproj that will copy resources folder in bin folder. There is probably a better way of doing this…
  <ItemGroup>
    <ContentWithTargetPath Include="Resources\**">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <TargetPath>Resources\%(RecursiveDir)\%(Filename)%(Extension)</TargetPath>
    </ContentWithTargetPath>
  </ItemGroup>
Integration testing – external REST API

When it comes down to testing external API, things get a bit more complicated. Ideally, you would like to test integration with that external API but sometimes that is just not possible.

For example

  • you are paying to use that API
  • there is no testing environment provided
  • it’s hard to manage state of that API
    • imagine you are testing GET method and you expect 2 pets as return.
    • what happens when your next test adds one more pet to that API? Will the previous step pass next time? It will if you deleted the pet you inserted. But what if there is no DELETE endpoint? What if that endpoint doesn’t work correctly?

What I think we can do instead? Mock it as closely as we can. To the point that we are actually calling a service that acts as a mock of that API. And it would be ideal that we can somehow manage state of aformentioned service.

How can we do it? With Mock Server.

Let’s get back to our application. After we analyzed it, we can see

  • it uses pings GET /pets endpoint
  • everything that we need to tests is placed in Company.Shorts.Infrastructure.Http.ExternalApi
  • since we decoupled everything, we can just test IExternalApi

Let’s talk about approach I’m going to take

  • I want to use XUnit for my testing
  • I want to use FluentAssertion for my assertions
  • When I press run tests, I want to start our mock service in Docker – this is where TestContainers comes in
  • And I want to do this only once – this is where xUnit fixtures comes in.
  • Before each test, I want to insert some state in service
  • After every test, I want to clean all data from service (so that next test starts with fresh state)

So, let’s start

  • Create new xUnit project in test folder called Company.Shorts.Infrastructure.Http.ExternalApi.Test
  • Let’s add some dependencies: FluentAssertions, TestContainers
  • Company.Shorts.Infrastructure.Http.ExternalApi.Test.csproj should look like this:
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>

    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <ItemGroup>

    <PackageReference Include="FluentAssertions" Version="6.11.0" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.5" />
    <PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.1" />
    <PackageReference Include="Testcontainers" Version="3.2.0" />
    <PackageReference Include="Testcontainers.PostgreSql" Version="3.2.0" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
    <PackageReference Include="xunit" Version="2.4.2" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="coverlet.collector" Version="3.2.0">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

  <!--Copies Files from Resource Folder to bin\debug\net7.0-->
  <!--Easier way to read files in PostgresSeedAttribute-->
  <ItemGroup>
    <ContentWithTargetPath Include="Resources\**">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <TargetPath>Resources\%(RecursiveDir)\%(Filename)%(Extension)</TargetPath>
    </ContentWithTargetPath>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\Application\Company.Shorts.Application.Contracts\Company.Shorts.Application.Contracts.csproj" />
    <ProjectReference Include="..\..\src\Infrastructure\Company.Shorts.Infrastructure.Db.Postgres\Company.Shorts.Infrastructure.Db.Postgres.csproj" />
  </ItemGroup>

</Project>
  • Since ExternalApiService is internal, we need to add something to Company.Shorts.Infrastructure.Http.ExternalApi.csproj
...
  <ItemGroup>
    <InternalsVisibleTo Include ="Company.Shorts.Infrastructure.Http.ExternalApi.Tests"/>
  </ItemGroup>
...
  • Let’s write our fixture. Pretty similar to the database one.
namespace Company.Shorts.Infrastructure.Http.ExternalApi.Tests.Internal.Fixtures
{
    using Company.Shorts.Application.Contracts.Http;
    using Company.Shorts.Infrastructure.Http.ExternalApi.Internal;
    using Company.Shorts.Infrastructure.Http.ExternalApi.Tests.Internal.MockWebServer;
    using DotNet.Testcontainers.Builders;
    using DotNet.Testcontainers.Containers;
    using Microsoft.Extensions.DependencyInjection;
    using System;

    public static class MockWebServerConstants
    {
        public const string Image = "mockserver/mockserver";
        public const int Port = 1080;
    }

    public class MockWebServerFixture : IDisposable
    {
        private bool _disposed;

        private readonly IEnviromentVariableManager enviromentVariableManager = new MockWebServerEnvironmentVariableManager();

        public MockWebServerFixture()
        {
            this.MockWebServerContainer = new ContainerBuilder()
                .WithImage(MockWebServerConstants.Image)
                .WithPortBinding(MockWebServerConstants.Port)
                //.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(MockWebServerConstants.Port))
                // For some reason, we are not able to use port strategy. This is a workaround
                .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("INFO 1080 started on port: 1080"))
                .Build();

            this.MockWebServerContainer.StartAsync().Wait();

            this.Url = $"http://{this.MockWebServerContainer.Hostname}:{this.MockWebServerContainer.GetMappedPublicPort(MockWebServerConstants.Port)}";

            var services = new ServiceCollection();

            services.AddHttpClient<IExternalApi, ExternalApiService>(opt => opt.BaseAddress = new Uri(this.Url));

            this.Provider = services.BuildServiceProvider();

            this.enviromentVariableManager.Set(this.Url);
        }

        public ServiceProvider Provider { get; }

        public IContainer MockWebServerContainer { get; }

        public string Url { get; }

        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    this.MockWebServerContainer.DisposeAsync().AsTask().Wait();
                    this.Provider.Dispose();
                }

                _disposed = true;
            }
        }

        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }
}
namespace Company.Shorts.Infrastructure.Http.ExternalApi.Tests.Internal.Fixtures
{
    using Xunit;

    [CollectionDefinition(CollectionFixtureConstants.Integration)]
    public class IntegrationTestCollection:
         ICollectionFixture<MockWebServerFixture>
    {
    }
}
namespace Company.Shorts.Infrastructure.Http.ExternalApi.Tests.Internal.Fixtures
{
    public static class CollectionFixtureConstants
    {
        public const string Integration = "Integration";
    }
}
  • And finally, our tests
namespace Company.Shorts.Infrastructure.Http.ExternalApi.Tests
{
    using Company.Shorts.Application.Contracts.Http;
    using Company.Shorts.Infrastructure.Http.ExternalApi.Tests.Internal.Fixtures;
    using Company.Shorts.Infrastructure.Http.ExternalApi.Tests.Internal.MockWebServer;
    using FluentAssertions;
    using Microsoft.Extensions.DependencyInjection;
    using Newtonsoft.Json;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    using Xunit;

    [Collection(CollectionFixtureConstants.Integration)]
    public class PetsTest
    {
        private readonly IExternalApi externalApi;

        public PetsTest(MockWebServerFixture fixture)
        {
            this.externalApi = fixture.Provider.GetRequiredService<IExternalApi>();
        }

        [Fact]
        [MockWebServerSeed("/Resources/ExternalApi/get-pets-expectation.json")]
        public async Task Test_One()
        {
            var response = await this.externalApi.GetAsync(default);

            response?.Count.Should().Be(2);
        }

        [Fact]
        [MockWebServerSeed("/Resources/ExternalApi/get-pets-expectation.json")]
        public async Task Test_Two()
        {
            var response = await this.externalApi.GetAsync(default);

            response?.Count.Should().Be(2);
        }
    }
}
namespace Company.Shorts.Infrastructure.Http.ExternalApi.Tests
{
    using Company.Shorts.Application.Contracts.Http;
    using Company.Shorts.Infrastructure.Http.ExternalApi.Tests.Internal.Fixtures;
    using Company.Shorts.Infrastructure.Http.ExternalApi.Tests.Internal.MockWebServer;
    using FluentAssertions;
    using Microsoft.Extensions.DependencyInjection;
    using System.Threading.Tasks;
    using Xunit;

    [Collection(CollectionFixtureConstants.Integration)]
    public class PetsTwoTest
    {
        private readonly IExternalApi externalApi;

        public PetsTwoTest(MockWebServerFixture fixture)
        {
            this.externalApi = fixture.Provider.GetRequiredService<IExternalApi>();
        }

        [Fact]
        [MockWebServerSeed("/Resources/ExternalApi/get-pets-expectation.json")]
        public async Task Test_One()
        {
            var response = await this.externalApi.GetAsync(default);

            response?.Count.Should().Be(2);
        }

        [Fact]
        [MockWebServerSeed("/Resources/ExternalApi/get-pets-expectation.json")]
        public async Task Test_Two()
        {
            var response = await this.externalApi.GetAsync(default);

            response?.Count.Should().Be(2);
        }
    }
}
  • Like I said, it would be nice if we could:
    • Before every test, somehow set state of that mock service
    • After each test, reset that state
  • And that’s exactly what we are doing with our MockWebServerSeedAttribute.cs.
  • Before each test we are going to create an “expectation”. With that expectation we are going to make a PUT request to our mock server. This tells our mock server that he needs to return X when Y happens.
{
  "httpRequest": {
    "path": "/pets",
    "method": "GET"
  },
  "httpResponse": {
    "body": [
      {
        "id": 1,
        "name": "Test",
        "tag": "Test"
      },
      {
        "id": 2,
        "name": "Test2",
        "tag": "Test2"
      }
    ]
  }
}
  • After every test, we are going to reset state by making a request to /reset on our mock server.
namespace Company.Shorts.Infrastructure.Http.ExternalApi.Tests.Internal.MockWebServer
{
    using Company.Shorts.Infrastructure.Http.ExternalApi.Tests.Internal;
    using Company.Shorts.Infrastructure.Http.ExternalApi.Tests.Internal.Common;
    using System;
    using System.Net.Http;
    using System.Reflection;
    using Xunit.Sdk;

    public sealed class MockWebServerSeedAttribute : BeforeAfterTestAttribute
    {
        private readonly IFileManager fileManager = new FileManager();

        private readonly HttpClient httpClient;

        private readonly IEnviromentVariableManager enviromentVariableManager = new MockWebServerEnvironmentVariableManager();

        public MockWebServerSeedAttribute(string filePath)
        {
            this.FilePath = filePath;
            this.httpClient = new HttpClient
            {
                BaseAddress = new Uri(this.enviromentVariableManager.Get())
            };
        }

        public string FilePath { get; }

        public override void After(MethodInfo methodUnderTest)
        {
            var message = new HttpRequestMessage(HttpMethod.Put, "/mockserver/reset");

            var response = this.httpClient.Send(message);

            response.EnsureSuccessStatusCode();

            base.After(methodUnderTest);
        }

        public override void Before(MethodInfo methodUnderTest)
        {
            var json = this.fileManager.Read(this.FilePath);

            var message = new HttpRequestMessage(HttpMethod.Put, "/mockserver/expectation")
            {
                Content = new StringContent(json)
            };

            var response = this.httpClient.Send(message);

            response.EnsureSuccessStatusCode();

            base.Before(methodUnderTest);
        }
    }
}
  • Implementation of all those interfaces can be found on the repository on branch: feature/integration.
  • Also, I keep my resource files in Resources folder, and in order to avoid funny string replacement when reading the files, I needed to add something in the csproj that will copy resources folder in bin folder. There is probably a better way of doing this…
  <ItemGroup>
    <ContentWithTargetPath Include="Resources\**">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <TargetPath>Resources\%(RecursiveDir)\%(Filename)%(Extension)</TargetPath>
    </ContentWithTargetPath>
  </ItemGroup>
Conclusion

If you plan on writing integration tests, start early. The more complex application gets, the more things you need to set up. You know what they say, the more you fuck around the more you are gonna find out.

Github