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.