Skip to content

Integration Testing

When performing integration tests, we want to simulate as much as possible how our application run without actually running it.

For that, the Suite includes the ITsynch.Suite.Testing project, which includes several utilities for integration and unit testing as well.

Particularly, we will be covering the BaseHostBuildingIntegrationTest and BaseHostStartingIntegrationTest. Both are base classes that our tests can extend from which will include behavior for interacting with the Service Provider and with a built host, respectively.

Testing Dependency Resolution or logic

When we want to test that a module behaves in a certain way, or that some dependencies are resolved properly, or that class mappings / repositories work for our entities, we don't really need to run an application (that is a web host for example), we need access to the IServiceProvider mostly, to resolve and execute methods on our services.

To do that, we can use the BaseHostBuildingIntegrationTest. i.e:

C#
public class HostBuildingTests
    : BaseHostBuildingIntegrationTest<ModuleA>
{
    public HostBuildingTests(ITestOutputHelper helper) : base(helper)
    {
    }

    [Fact]
    public void MyModule_ShouldInclude_MyInterface()
    {
        var symbol = ServiceProvider.GetRequiredService<IMyInterface>();
        Assert.NotNull(symbol;
    }
}

The host is not run, however we do have access to the Root Service Provider, and we can use that to create a new Scope if we would need a scoped service.

Do note that persistence is not enabled on these classes, there are utilities for persistence as well which we'll cover later on.

Testing HTTP Services

To test the complete HTTP pipeline, including serialization and middleware execution, we can use the BaseHostStartingIntegrationTest which includes a host and also starts it. It also wires the TestServer by default, and exposes a Test Client that you can use to query your API.

C#
public class DataAnnotationLocalizationTests
    : BaseHostStartingIntegrationTest<ProblemDetailsTestModule>
{
    public DataAnnotationLocalizationTests(ITestOutputHelper helper) : base(helper)
    {
    }

    [Theory]
    [ClassData(typeof(SupportedCultureProvider))]
    public async Task RequiredValidationAttribute_ForInvalidModel_ShouldBeLocalized(string targetCulture)
    {
        // Arrange
        var testClient = this.TestClient;
        testClient.DefaultRequestHeaders.Add("accept-language", targetCulture);
        var fieldToValidate = nameof(BarDTO.RequiredField);

        // Act
        var response = await testClient.PostAsJsonAsync<BarDTO>(
            "api/testClass/Foo", BarDTO.AllFieldsFaultedInstance);

        var problemDetailsResult = JsonSerializer.Deserialize<ValidationProblemDetails>(await response.Content.ReadAsStringAsync());

        // Assert
        var dataAnnotationLocalizer = this.ServiceProvider.
            GetRequiredService<IStringLocalizer<DataAnnotationLocalizationResources>>();
        using var cultureUsage = CultureUtil.Use(targetCulture);
        var expectedMessage = dataAnnotationLocalizer[
            DataAnnotationLocalizationResources.MSG_REQUIRED_ATTRIBUTE_VALIDATION_ERROR,
            fieldToValidate];

        Assert.Equal(expectedMessage, problemDetailsResult.Errors[fieldToValidate][0]);
    }
}

Persistence Integration Testing

In order to test EFCore persistence layer, the Suite includes the ITsynch.Suite.Testing.Persistence project, which has a base class that can be used to have basic persistence scaffolding.

When using this class, you can use your the module that represents your persistence layer on the generic type, and it will include everything required to test your persistence layer.

You can inject IRepositories for example to test that your persistence work.

You can also use this against your application module, to test that your AppServices are executing correctly.

A complete example can be seen below:

C#
public class EntityTypeConfigurationTests
    : BaseSQLiteIntegrationTest<EntityTypeConfigurationTests.TestModule>
{
    public EntityTypeConfigurationTests(ITestOutputHelper helper) : base(helper)
    {
    }

    private class EntityTypeDbContext: SuiteDbContext<EntityTypeDbContext>
    {
        public EntityTypeDbContext(DbContextOptions<EntityTypeDbContext> options)
        : base(contextOptions)
        {
        }
    }

    private class TestEntityConfiguration
        : IEntityTypeConfiguration<EntityTypeDbContext, TestEntity>
    {
        public void Configure(EntityTypeBuilder<TestEntity> builder)
        {
            builder.ToTable("TestEntities_TestTableName");
            builder.HasKey(x => x.Id);
            builder.Property(x => x.Description);
        }
    }

    public class TestModule : SuiteModule
    {
        public override void SetupModule(IModuleBuilder builder)
        {
            base.SetupModule(builder);
            builder.DependsOn<EntityFrameworkCoreModule, EntityFrameworkCoreModuleOptions>(ops =>
            {
                ops.DisableAllAutoDiscovery();
                ops.AddDbContext<EntityTypeDbContext>();
                ops.AddEntityTypeConfiguration<TestEntityConfiguration, TestEntity, EntityTypeDbContext>();
            });
            builder.DependsOn<EntityFrameworkCoreSQLiteModule>();
        }
    }

    [Fact]
    public void EntityTypeConfigurations_ShouldBeExecuted()
    {
        // Act
        var dbContext = this.ServiceProvider.GetService<EntityTypeDbContext>();
        var entityType = dbContext.Model.FindEntityType(typeof(TestEntity));

        // Assert
        Assert.NotNull(entityType);
        Assert.Equal("TestEntities_TestTableName", entityType.GetTableName());
    }
}

MassTransit Services Integration Testing

To test your services implemented using MassTransit, you can leverage the scaffolding provided by the Suite in ITsynch.Suite.Testing.Services.

By using BaseMassTransitIntegrationTest<TModule> you are bootstrapping the whole Suite composition and infrastructure needed to perform integration tests using the assets defined in your module plus MassTransit's Test Harness with InMemory Transport, and Sqlite persistence at once.

Some things to note here are:

  • MassTransit will behave as it would in production environment: it will configure the entire topology and orchestrate the message delivering so your application will run as it were in a real environment.
  • We encourage you to use your main application's module, to get the most out of the composition and have seamlessly experience among test and production environment in terms of behavior.
  • Any service or component defined in your module can be injected and used during the test.
  • You can particularize your test by overriding base test class methods, for example to register new dependencies and/or configure your module or dependant module options through IOptions pattern and/or IConfiguration.
  • Database migration execution is disabled by default to prevent unwanted execution. You can inject your DbContext and create schema manually.

You can see a quite detailed example here below:

C#
public class AssigneeLifecycleSagaIntegrationTests
    : BaseMassTransitIntegrationTest<ServiceOrdersApplicationModule>
{
    public AssigneeLifecycleSagaIntegrationTests(ITestOutputHelper helper)
        : base(helper)
    {
    }

    public override async Task InitializeAsync()
    {
        await base.InitializeAsync();

        // Make sure the schema is created. No migrations for Sqlite.
        var dbContext = this.ServiceProvider.GetRequiredService<ServiceOrderDbContext>();
        await dbContext.Database.EnsureCreatedAsync().ConfigureAwait(false);
    }

    protected override void ConfigureBootstrapServices(
            HostBuilderContext context,
            IServiceCollection bootstrapServices)
    {
        base.ConfigureBootstrapServices(context, bootstrapServices);
        bootstrapServices.ConfigureSagaHarnessFor<AssigneeLifecycleSaga, AssigneeEntity>();
    }

    internal IStateMachineSagaTestHarness<AssigneeEntity, AssigneeLifecycleSaga> SagaHarness => this.ServiceProvider
        .GetRequiredService<IStateMachineSagaTestHarness<AssigneeEntity, AssigneeLifecycleSaga>>();

    internal IAssigneeAppService Client => this.GetAppServiceProxyFor<IAssigneeAppService>();

    [Fact]
    public async Task ProfileUpdated_WhenEverythingIsOk_ShouldCreateAssignee()
    {
        // Arrange
        var correlationId = NewId.NextGuid();
        var displayName = "Mr. Brown, the painter";
        var loginId = "MrBrown";

        // Act
        await this.PublishEndpoint.Publish<ProfileUpdated>(new
        {
            CorrelationId = correlationId,
            DisplayName = displayName,
            LoginId = loginId
        });

        // Assert
        // did the actual saga consume the message
        Assert.True(await this.SagaHarness
            .Consumed.Any<ProfileUpdated>(
                m => m.Context.Message.CorrelationId == correlationId));

        // Ensure the data was persisted correctly.
        var assignee = await this.Client.GetByIdAsync(correlationId);
        Assert.Equal(correlationId, assignee.CorrelationId);
        Assert.Equal(displayName, assignee.Name);
    }
}

You can leverage SagaTestHarness, ConsumerTestHarness, and all available MassTransit's testing scaffolding to build up your tests checking if determined message's been published/consumed at high topology level such the bus, or by narrowing down the query to SagaHarness or ConsumerHarness.

If you need to make end-to-end testing, you can do so by injecting an AppService proxy to query your AppServices, in case you can assert on the outcome produced by it, or you can also inject any repository to assert on it by querying directly persistence layer through any custom orr auto-generated repository.