Integration Test – Compendium

It’s second time we’re going to talk about Integration tests, previously I wrote about basics. This time more about how to write IT. Before we start writing tests we need to decide what parts of application we’re going to test. There are 3 possibilities. It’s important to mention that IT like that are very similar to UT, thus developers can write those tests as well.

Testing raw SQL

First of all we have to use IT when we want to test database access. If we’re using raw SQL it is only option to test that code. Tests will use physical database, but it’s easy to setup with different connection string, so test will not use production database.

Fixture

    public class RepositoryFixture
    {
        public DbContextOptions<BlogPostContext> Options { get; }
        public IEnumerable<StudentCourse> ExpectedMissingGrades { get; private set; }
        public RepositoryFixture()
        {
            var configuration = new ConfigurationBuilder()
                .SetBasePath(PathExtension.GetFullPathToTestConfigFile())
                .AddJsonFile("appsettings.json")
                .Build();

            Options = new DbContextOptionsBuilder<BlogPostContext>()
                .UseSqlServer(configuration.GetConnectionString("DefaultConnection"))
                .EnableSensitiveDataLogging()
                .Options;

            using var context = new BlogPostContext(Options);
            var assessments = context.Assessments.ToArray();
            var students = context.Students.ToArray();
            var courses = context.Courses.ToArray();
            ExpectedMissingGrades = GetMissingGrades(assessments, courses, students);

            if (!context.StudentCourses.Any())
            {
                SeedDatabase(context, assessments, courses, students);
            }
        }

This is common Fixture class, but additionally, here, we can set up Db connection. First thing we do is load configuration file, it is copy of appsettings.json from Web API project. I’m using copy to reflect environment during tests.

Next we need to setup DbContext. I’m using DbContextOptions so I can create DbContext in Test class with using keyword to make sure it will be disposed after test’s finished, but if you want you can setup DbContext here and pass initiated DbContext to your tests.

And finally I’m seeding Db if it’s empty and get expected values. With that we’re ready to write test.

    public class StudentRepositoryTests : IClassFixture<RepositoryFixture>
    {
        private readonly RepositoryFixture fixture;

        public StudentRepositoryTests(RepositoryFixture fixture)
        {
            this.fixture = fixture;
        }

        [Fact]
        [Trait("Category", "Integration")]
        [Trait("Category", "UsingDatabase")]
        public async Task Should_ReturnMissingGrades_For_EveryStudent()
        {
            // arrange
            using var context = new BlogPostContext(fixture.Options);
            var repository = new StudentRepository(context);

            // act
            var result = await repository.GetMissingGradesAsync();

            // assert
            result.Should().BeEquivalentTo(fixture.ExpectedMissingGrades, opt => opt.Excluding(x => x.RuntimeType == typeof(Assessment))
                                                                                    .Excluding(x => x.RuntimeType == typeof(Course))
                                                                                    .Excluding(x => x.RuntimeType == typeof(Student)));
        }
    }

As you can see this test looks like unit test. I’ve only added traits to this one, because I wanted to distinguish unit tests and integration tests in CI. It’s good practice, because it’s common thing to treat IT and UT different, for example running IT only at night and UT every time someone push to repository.

Testing partially mocked

Next use case is similar to first, but this time we will test integration between some components and mock rest of them. Thanks to that our test will test only integration between components that are important for us. Keep in mind that creating big test that will check everything is hard to maintain and most of the time it won’t help you at all, because there’s so much thing that could break and you’ll end up checking it manually in debug mode or in logs. I didn’t prepare example for that, because test would look like mixture of UT and IT, i.e integration tests with mocks for some of dependencies but not all of them like in UT.

So to speak, if you didn’t mock every dependency in UT you’ve created IT.

Testing whole application – Smoke tests

Last type is Smoke test. Name is taken from electronics. After electronic circuit was finished engineer connected it to power to check if something smoke (I’m not sure if they really do that, but that’s what I was told when I asked :P). In informatics it means pretty much the same. It’s test to check if application’s happy path pass.

I said before that creating huge tests is bad practice because it won’t tell you anything. In that case it’s ok, all we want to know from this kind of test is, if application works. Most of the time I’m using smoke tests just to run my code after changes to make sure it still works as a whole. It’s great to test regression as well. And if something is wrong you can run your unit tests.

In example we’ll setup in-memory test server and inside test we’ll create http clients just for that server. It means we can pretend we call our code from web browser via http and we don’t need released instance of our application anywhere.

    public class ControllerFixture
    {
        public readonly IHost Host;
        public readonly IHostBuilder HostBuilder;
        public readonly DbContextOptionsBuilder<BlogPostContext> OptionsBuilder;
        public IConfiguration Configuration;
        private const string configFileName = "appsettings.json";
        public ControllerFixture()
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(PathExtension.GetFullPathToTestConfigFile())
                .AddJsonFile(configFileName, optional: false, reloadOnChange: true);
            Configuration = builder.Build();

            OptionsBuilder = new DbContextOptionsBuilder<BlogPostContext>();
            OptionsBuilder.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));

            HostBuilder = new HostBuilder()
                .ConfigureWebHost(webHost =>
                {
                    webHost.UseTestServer();
                    webHost.UseStartup<Startup>();
                    webHost.UseConfiguration(Configuration);
                })
                .UseServiceProviderFactory(new AutofacServiceProviderFactory());

            Host = HostBuilder.Start();
        }
    }

Only difference is setting up HostBuilder. Thanks to that we can run Test Server. We’re using our Startup class. Additionally our Test Server will use Autofac the same way web API does. Test will use HttpClient created from TestServer to call our Api.

        [Fact]
        [Trait("Category", "Integration")]
        public async Task Should_GetAllStudents()
        {
            // arrange
            var request = $"api/students";
            IEnumerable<StudentTestResponse> response;
            var expected = await PrepareGetAllExpectedResponse();

            // act
            var client = fixture.Host.GetTestClient();
            httpResponse = await client.GetAsync(request);
            response = await httpResponse.Content.ReadAsAsync<List<StudentTestResponse>>();

            // assert
            httpResponse.EnsureSuccessStatusCode();
            response.Should().BeEquivalentTo(expected);
        }

As you can see its straightforward. You may have notice that we’re using StudentTestResponse. I didn’t use my dto from application. It’s good idea to create copy of models to make sure we’ll see that breaking changes in contracts (in this case dto) were made. Thanks to that we won’t forget to inform our clients, change our consuming APIs, because our test will fail.

Leave a Reply