Skip to content

Commit

Permalink
System Integration Testing (SIT)
Browse files Browse the repository at this point in the history
  • Loading branch information
kgrzybek committed Mar 28, 2020
1 parent 5c14e03 commit 79439f5
Show file tree
Hide file tree
Showing 26 changed files with 727 additions and 28 deletions.
168 changes: 168 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ Full Modular Monolith .NET application with Domain-Driven Design approach.

  [3.13 Integration Tests](#313-integration-tests)

  [3.14 System Integration Testing](#314-system-integration-testing)

[4. Technology](#4-technology)

[5. How to Run](#5-how-to-run)
Expand Down Expand Up @@ -1052,6 +1054,170 @@ public class MeetingPaymentTests : TestBase

Each Command/Query processing is a separate execution (with different object graph resolution, context, database connection etc.) thanks to Composition Root of each module. This behavior is important and desirable.

### 3.14 System Integration Testing

#### Definition
[System Integration Testing (SIT)](https://en.wikipedia.org/wiki/System_integration_testing) is performed to verify the interactions between the modules of a software system. It involves the overall testing of a complete system of many subsystem components or elements.

#### Implementation

Implementation of system integration tests is based on approach of integration testing of modules in isolation (invoking commands and queries) described in the previous section.

The problem is that in this case we are dealing with **asynchronous communication**. Due to asynchrony, our **test must wait for the result** at certain times.

To correctly implement such tests, the **Sampling** technique and implementation described in the [Growing Object-Oriented Software, Guided by Tests](https://www.amazon.com/Growing-Object-Oriented-Software-Guided-Tests/dp/0321503627) book was used:

>An asynchronous test must wait for success and use timeouts to detect failure. This implies that every tested activity must have an observable effect: a test must affect the system so that its observable state becomes different. This sounds obvious but it drives how we think about writing asynchronous tests. If an activity has no observable effect, there is nothing the test can wait for, and therefore no way for the test to synchronize with the system it is testing. There are two ways a test can observe the system: by sampling its observable state or by listening for events that it sends out.
![](docs/Images/SystemIntegrationTests.jpg)

Test below:
1. Creates Meeting Group Proposal in Meetings module
2. Waits until Meeting Group Proposal to verification will be available in Administration module with 10 seconds timeout
3. Accepts Meeting Group Proposal in Administration module
4. Waits until Meeting Group is created in Meetings module with 15 seconds timeout

```csharp
public class CreateMeetingGroupTests : TestBase
{
[Test]
public async Task CreateMeetingGroupScenario_WhenProposalIsAccepted()
{
var meetingGroupId = await MeetingsModule.ExecuteCommandAsync(
new ProposeMeetingGroupCommand("Name",
"Description",
"Location",
"PL"));

AssertEventually(
new GetMeetingGroupProposalFromAdministrationProbe(meetingGroupId, AdministrationModule),
10000);

await AdministrationModule.ExecuteCommandAsync(new AcceptMeetingGroupProposalCommand(meetingGroupId));

AssertEventually(
new GetCreatedMeetingGroupFromMeetingsProbe(meetingGroupId, MeetingsModule),
15000);
}

private class GetCreatedMeetingGroupFromMeetingsProbe : IProbe
{
private readonly Guid _expectedMeetingGroupId;

private readonly IMeetingsModule _meetingsModule;

private List<MeetingGroupDto> _allMeetingGroups;

public GetCreatedMeetingGroupFromMeetingsProbe(
Guid expectedMeetingGroupId,
IMeetingsModule meetingsModule)
{
_expectedMeetingGroupId = expectedMeetingGroupId;
_meetingsModule = meetingsModule;
}

public bool IsSatisfied()
{
return _allMeetingGroups != null &&
_allMeetingGroups.Any(x => x.Id == _expectedMeetingGroupId);
}

public async Task SampleAsync()
{
_allMeetingGroups = await _meetingsModule.ExecuteQueryAsync(new GetAllMeetingGroupsQuery());
}

public string DescribeFailureTo()
=> $"Meeting group with ID: {_expectedMeetingGroupId} is not created";
}

private class GetMeetingGroupProposalFromAdministrationProbe : IProbe
{
private readonly Guid _expectedMeetingGroupProposalId;

private MeetingGroupProposalDto _meetingGroupProposal;

private readonly IAdministrationModule _administrationModule;

public GetMeetingGroupProposalFromAdministrationProbe(Guid expectedMeetingGroupProposalId, IAdministrationModule administrationModule)
{
_expectedMeetingGroupProposalId = expectedMeetingGroupProposalId;
_administrationModule = administrationModule;
}

public bool IsSatisfied()
{
if (_meetingGroupProposal == null)
{
return false;
}

if (_meetingGroupProposal.Id == _expectedMeetingGroupProposalId &&
_meetingGroupProposal.StatusCode == MeetingGroupProposalStatus.ToVerify.Value)
{
return true;
}

return false;
}

public async Task SampleAsync()
{
try
{
_meetingGroupProposal =
await _administrationModule.ExecuteQueryAsync(
new GetMeetingGroupProposalQuery(_expectedMeetingGroupProposalId));
}
catch
{
// ignored
}
}

public string DescribeFailureTo()
=> $"Meeting group proposal with ID: {_expectedMeetingGroupProposalId} to verification not created";
}
}
```

Poller class implementation (based on example in the book):

```csharp
public class Poller
{
private readonly int _timeoutMillis;

private readonly int _pollDelayMillis;

public Poller(int timeoutMillis)
{
_timeoutMillis = timeoutMillis;
_pollDelayMillis = 1000;
}

public void Check(IProbe probe)
{
var timeout = new Timeout(_timeoutMillis);
while (!probe.IsSatisfied())
{
if (timeout.HasTimedOut())
{
throw new AssertErrorException(DescribeFailureOf(probe));
}

Thread.Sleep(_pollDelayMillis);
probe.SampleAsync();
}
}

private static string DescribeFailureOf(IProbe probe)
{
return probe.DescribeFailureTo();
}
}
```

## 4. Technology

List of technologies, frameworks and libraries used for implementation:
Expand Down Expand Up @@ -1132,6 +1298,7 @@ List of features/tasks/approaches to add:
| Architecture Decision Log update | High | Completed | 2019-11-09 |
| Integration automated tests | Normal | Completed | 2020-02-24 |
| Migration to .NET Core 3.1 | Low | Completed | 2020-03-04 |
| System Integration Testing | Normal | Completed | 2020-03-28 |
| API automated tests | Normal | | |
| FrontEnd SPA application | Normal | | |
| Meeting comments feature | Low | | |
Expand Down Expand Up @@ -1218,6 +1385,7 @@ The project is under [MIT license](https://opensource.org/licenses/MIT).
- ["The Art of Unit Testing: with examples in C#"](https://www.amazon.com/Art-Unit-Testing-examples/dp/1617290890) book, Roy Osherove
- ["Unit Test Your Architecture with ArchUnit"](https://blogs.oracle.com/javamagazine/unit-test-your-architecture-with-archunit) article, Jonas Havers
- ["Unit Testing Principles, Practices, and Patterns"](https://www.amazon.com/Unit-Testing-Principles-Practices-Patterns/dp/1617296279) book, Vladimir Khorikov
- ["Growing Object-Oriented Software, Guided by Tests"](https://www.amazon.com/Growing-Object-Oriented-Software-Guided-Tests/dp/0321503627) book, Steve Freeman, Nat Pryce

### UML
- ["UML Distilled: A Brief Guide to the Standard Object Modeling Language (3rd Edition)"](https://www.amazon.com/UML-Distilled-Standard-Modeling-Language/dp/0321193687) book, Martin Fowler
Expand Down
Binary file added docs/Images/SystemIntegrationTests.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 14 additions & 2 deletions src/API/CompanyName.MyMeetings.API/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,27 @@ private IServiceProvider CreateAutofacServiceProvider(IServiceCollection service

var emailsConfiguration = new EmailsConfiguration(_configuration["EmailsConfiguration:FromEmail"]);

MeetingsStartup.Initialize(this._configuration[MeetingsConnectionString], executionContextAccessor, _logger, emailsConfiguration);
AdministrationStartup.Initialize(this._configuration[MeetingsConnectionString], executionContextAccessor, _logger);
MeetingsStartup.Initialize(
this._configuration[MeetingsConnectionString],
executionContextAccessor,
_logger,
emailsConfiguration,
null);

AdministrationStartup.Initialize(
this._configuration[MeetingsConnectionString],
executionContextAccessor,
_logger,
null);

UserAccessStartup.Initialize(
this._configuration[MeetingsConnectionString],
executionContextAccessor,
_logger,
emailsConfiguration,
this._configuration["Security:TextEncryptionKey"],
null);

PaymentsStartup.Initialize(
this._configuration[MeetingsConnectionString],
executionContextAccessor,
Expand Down
7 changes: 7 additions & 0 deletions src/CompanyName.MyMeetings.sln
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modu
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.Modules.Administration.IntegrationTests", "Modules\Administration\Tests\IntegrationTests\CompanyName.MyMeetings.Modules.Administration.IntegrationTests.csproj", "{3D7FDF4A-6B8B-49CA-8E28-5B292DC0954F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompanyName.MyMeetings.IntegrationTests", "Tests\IntegrationTests\CompanyName.MyMeetings.IntegrationTests.csproj", "{586DB9FA-CBBF-4867-A57D-E2359925D09A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -245,6 +247,10 @@ Global
{3D7FDF4A-6B8B-49CA-8E28-5B292DC0954F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D7FDF4A-6B8B-49CA-8E28-5B292DC0954F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D7FDF4A-6B8B-49CA-8E28-5B292DC0954F}.Release|Any CPU.Build.0 = Release|Any CPU
{586DB9FA-CBBF-4867-A57D-E2359925D09A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{586DB9FA-CBBF-4867-A57D-E2359925D09A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{586DB9FA-CBBF-4867-A57D-E2359925D09A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{586DB9FA-CBBF-4867-A57D-E2359925D09A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -292,6 +298,7 @@ Global
{396A6C3F-74BA-4D85-8BF3-1182F9DE9D95} = {0FF699EF-8156-43CB-8D18-8EA28F30E9EE}
{B448FDC3-5F85-47EE-9F4A-2654E8CC67E1} = {53E4F002-E708-45F7-8444-19EB8977B5C9}
{3D7FDF4A-6B8B-49CA-8E28-5B292DC0954F} = {F544D6DA-740A-4313-9542-D989666EA9DE}
{586DB9FA-CBBF-4867-A57D-E2359925D09A} = {8B08A9EE-CE27-4CC3-ACB3-3BD9628E5479}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6B94C21A-AA6D-4D82-963E-C69C0353B938}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Autofac;
using CompanyName.MyMeetings.BuildingBlocks.Application;
using CompanyName.MyMeetings.BuildingBlocks.Infrastructure.EventBus;
using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.Authentication;
using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.DataAccess;
using CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configuration.EventsBus;
Expand All @@ -20,11 +21,12 @@ public class AdministrationStartup
public static void Initialize(
string connectionString,
IExecutionContextAccessor executionContextAccessor,
ILogger logger)
ILogger logger,
IEventsBus eventsBus)
{
var moduleLogger = logger.ForContext("Module", "Administration");

ConfigureContainer(connectionString, executionContextAccessor, moduleLogger);
ConfigureContainer(connectionString, executionContextAccessor, moduleLogger, eventsBus);

QuartzStartup.Initialize(moduleLogger);

Expand All @@ -33,7 +35,8 @@ public static void Initialize(

private static void ConfigureContainer(string connectionString,
IExecutionContextAccessor executionContextAccessor,
ILogger logger)
ILogger logger,
IEventsBus eventsBus)
{
var containerBuilder = new ContainerBuilder();

Expand All @@ -43,7 +46,7 @@ private static void ConfigureContainer(string connectionString,
containerBuilder.RegisterModule(new DataAccessModule(connectionString, loggerFactory));

containerBuilder.RegisterModule(new ProcessingModule());
containerBuilder.RegisterModule(new EventsBusModule());
containerBuilder.RegisterModule(new EventsBusModule(eventsBus));
containerBuilder.RegisterModule(new MediatorModule());
containerBuilder.RegisterModule(new AuthenticationModule());
containerBuilder.RegisterModule(new OutboxModule());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,27 @@ namespace CompanyName.MyMeetings.Modules.Administration.Infrastructure.Configura
{
internal class EventsBusModule : Autofac.Module
{
private readonly IEventsBus _eventsBus;

public EventsBusModule(IEventsBus eventsBus)
{
_eventsBus = eventsBus;
}

protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<InMemoryEventBusClient>()
.As<IEventsBus>()
.SingleInstance();
if (_eventsBus != null)
{
builder.RegisterInstance(_eventsBus).SingleInstance();

}
else
{
builder.RegisterType<InMemoryEventBusClient>()
.As<IEventsBus>()
.SingleInstance();
}

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal static void Initialize(ILogger logger)
TriggerBuilder
.Create()
.StartNow()
.WithCronSchedule("0/15 * * ? * *")
.WithCronSchedule("0/2 * * ? * *")
.Build();

scheduler
Expand All @@ -42,7 +42,7 @@ internal static void Initialize(ILogger logger)
TriggerBuilder
.Create()
.StartNow()
.WithCronSchedule("0/15 * * ? * *")
.WithCronSchedule("0/2 * * ? * *")
.Build();

scheduler
Expand All @@ -54,7 +54,7 @@ internal static void Initialize(ILogger logger)
TriggerBuilder
.Create()
.StartNow()
.WithCronSchedule("0/15 * * ? * *")
.WithCronSchedule("0/2 * * ? * *")
.Build();
scheduler.ScheduleJob(processInternalCommandsJob, triggerCommandsProcessing).GetAwaiter().GetResult();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ public async Task BeforeEachTest()
AdministrationStartup.Initialize(
ConnectionString,
ExecutionContext,
Logger);
Logger,
null);

AdministrationModule = new AdministrationModule();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,19 @@ protected InternalCommandBase(Guid id)
this.Id = id;
}
}

public abstract class InternalCommandBase<TResult> : ICommand<TResult>
{
public Guid Id { get; }

protected InternalCommandBase()
{
this.Id = Guid.NewGuid();
}

protected InternalCommandBase(Guid id)
{
this.Id = id;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts;
using System;
using CompanyName.MyMeetings.Modules.Meetings.Application.Contracts;

namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingGroupProposals.ProposeMeetingGroup
{
public class ProposeMeetingGroupCommand : CommandBase
public class ProposeMeetingGroupCommand : CommandBase<Guid>
{
public ProposeMeetingGroupCommand(string name, string description, string locationCity, string locationCountryCode)
{
Expand Down
Loading

0 comments on commit 79439f5

Please sign in to comment.