-
Notifications
You must be signed in to change notification settings - Fork 267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use InMemoryEventBus as scoped and use InMemoryEventBus in integration tests #99
base: main
Are you sure you want to change the base?
Conversation
...tecture/Src/Fitnet.IntegrationTests/Common/Events/EventBus/InMemory/NotificationDecorator.cs
Outdated
Show resolved
Hide resolved
...cture/Src/Fitnet.IntegrationTests/Common/TestEngine/Configuration/ConfigurationExtensions.cs
Outdated
Show resolved
Hide resolved
...tecture/Src/Fitnet.IntegrationTests/Common/Events/EventBus/InMemory/NotificationDecorator.cs
Outdated
Show resolved
Hide resolved
using Fitnet.Common.Events.EventBus; | ||
using MediatR; | ||
|
||
internal class NotificationDecorator<TNotification>(IEventBus eventBus, INotificationHandler<TNotification>? innerHandler) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It introduces quite a lot of boilerplate code when adding it in TestEngine. Still, we agree that it would make sense to use MediatR but maybe with a slightly different approach. What do you think to use MediatR but fake the method responsible for publishing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem here is that there is a test Given_valid_mark_pass_as_expired_request_Then_should_return_no_content
where:
- The pass is registered in the notification handler (
ContractSignedEvent
is published and needs to be handled byContractSignedEventHandler
, where the pass is persisted) -> here you had this intricateIntegrationEventHandlerScope
flow in previous approach - The expiration endpoint is called which is served IMediator based InMemoryEventBus. The
PassExpiredEvent
is using the bus. PassExpiredEventHandler
is marking the pass as expired in the database. Also publishesOfferPreparedEvent
.
If I am going to fake PublishAsync
instead of using built-in MediatR, I am afraid that:
Ad.1. The ContractSignedEventHandler
won't be called
Ad.3. The PassExpiredEventHandler
won't be called
I believe there are more tests like that.
On top of that, if Mediator won't run its internals, then the bug #96 that sparked this change will hide again, as it surfaced because Mediator actually executed the Publish
method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@anddrzejb to be honest I wouldn't be afraid that it won't be called - this is an internal mechanism of MediatR that should not be tested. IMO it would be enough to know that the event was published and that's it.
@kamilbaczek can you add your opinion on it as well? I do not want to be "that one badass who destroys dreams" :D
If you disagree, there is still a chance to simplify the above code:
internal static WebApplicationFactory<T> WithFakeEventBus<T>(this WebApplicationFactory<T> webApplicationFactory,
IEventBus eventBusMock)
where T : class => webApplicationFactory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
ReplaceNotificationHandlers(eventBusMock, services);
});
});
private static void ReplaceNotificationHandlers(IEventBus eventBusMock, IServiceCollection services)
{
var notificationHandlerTypes = GetNotificationHandlerTypes(services);
foreach (var handlerType in notificationHandlerTypes)
{
var decoratorType = typeof(NotificationDecorator<>).MakeGenericType(handlerType.GetGenericArguments()[0]);
services.AddTransient(handlerType, serviceProvider =>
{
var innerHandler = serviceProvider.GetService(handlerType);
var decorator = Activator.CreateInstance(decoratorType, eventBusMock, innerHandler);
return decorator!;
});
}
}
private static List<Type> GetNotificationHandlerTypes(IServiceCollection services) => services
.Where(descriptor => descriptor.ServiceType.IsGenericType &&
descriptor.ServiceType.GetGenericTypeDefinition() == typeof(INotificationHandler<>))
.Select(descriptor => descriptor.ServiceType)
.ToList();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more comment from my side - in such case it would be acceptable to introduce the E2E test that would check the business process. Unfortunately (on purpose) we do not have it in this project but unfortunately we need to decide for some trade-offs because of that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me first address the example test I was mentioning: Given_valid_mark_pass_as_expired_request_Then_should_return_no_content
.
- This test is expecting a
PassExpiredEvent
to be published. - Based on the code, it should be published once
api/passes/{id}
endpoint is called - given the pass exists. Because this is an integration test, then I presume it should wire the application in such a way, so the event is indeed published in the endpoint handler. - For the endpoint to find the pass, it needs to exist in database. The arrange section of the test is responsible for that.
a. PublishesContractSignedEvent
b. The handler is resolved based on the event and called.
c. The handler creates the pass in the database and publishesPassRegisteredEvent
. We do not want to reallyy publish it, this is not part of the test, after all we are still in the arrange section. - The pass id is retrieved from the database so it can be used later in the test (still in the arrange section of the test)
- The endpoint
api/passes/{id}
, where {id} is the value retrieved in point 4 (act) - The mock is checked if the event
PassExpiredEvent
was indeed called (assert).
Here we have 2 scenarios - I: use mediator or II: fake it.
I. Mediator. The tricky part is to catch the 2nd event published in 3c. Otherwise, decorator does its thing. This is where the fragility of my solution comes out - what if we have a scenario when we need to stop 3rd event, or 4th? Currently, such scenario is not handled, unless manually issued in the arrange/act section. But also seems that at this point is not needed. But it is not handled by II either.
II. Fake the mediator. Then we have to deal with the 3b. We can easily do it using fake, but it adds complexity to the solution by introducing IntegrationEventHandlerScope
, which under the hood seems to be doing what mediator does. Without its shortcomings. Which is not necessarily good given that the test did not discover the bug.
Here is where we come to the bottom of the issue. Integration tests should be testing all the components that are involved in the testing scenario. When they are not tested, then bugs may creep in (I rest my case?). The argument that Mediator is an internal mechanism and should not be tested - if it was a unit test I wholeheartedly agree. But in this case it is not Mediator being tested but its correct/incorrect integration.
Regarding the proposed change - it goes into infinite loop. When mediator is trying to resolve INotification
- it goes into the delegate passed in
services.AddTransient
. Delegate Inside is trying to get from the services the handlerType, that is the same type as the mediator was trying to get. So...- it goes into the delegate passed in
services.AddTransient
. Delegate Inside is trying to get from the services the handlerType, that is the same type as the mediator was trying to get. So...- it goes into the delegate passed in
services.AddTransient
. Delegate Inside is trying to get from the services the handlerType, that is the same type as the mediator was trying to get. So...
(....) surprisingly I never run into stack overflow.
- it goes into the delegate passed in
- it goes into the delegate passed in
"that one badass who destroys dreams"
No worries, I am not that fragile 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's discuss this together on the next sync
…ventbus-in-integration-tests
…ventbus-in-integration-tests
Changes: