diff --git a/README.md b/README.md index 1d9fc877..afafc556 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ NServiceBus.IntegrationTesting allows testing end-to-end business scenarios, exe ## Disclaimer -NServiceBus.IntegrationTesting is not affiliated with Particular Software and thus is not officially supported. It's evolution stage doesn't make it production ready yet. +NServiceBus.IntegrationTesting is not affiliated with Particular Software and thus is not officially supported. It's evolution stage doesn't make it production ready yet. ## tl;dr @@ -99,11 +99,11 @@ Defining an NServiceBus integration test is a multi-step process, composed of: ### Make sure endpoints configuration can be istantiated by tests -One of the goals of end-to-end testing an NServiceBus endpoints is to make sure that what gets tested is the real production code, not a copy of it crafted for the tests. The production endpoint configuration has to be used in tests. To make sure that the testing infrastructure can instantiate the endpoint configuration there are a couple of options, with many variations. +One of the goals of end-to-end testing a NServiceBus endpoint is to make sure that what gets tested is the real production code, not a copy of it crafted for the tests. The production endpoint configuration has to be used in tests. To make sure that the testing infrastructure can instantiate the endpoint configuration there are a couple of options, with many variations. #### Inherit from EndpointConfiguration -It's possible to create a class that inherits from `EndpointConfiguration` and then use it in both the production endpoint and the tests. To make so that the testing infrastructure con automatically instante it, the class must have a parameterless constructor, like in the following snippet: +It's possible to create a class that inherits from `EndpointConfiguration` and then use it in both the production endpoint and the tests. To make so that the testing infrastructure can automatically instantiate it, the class must have a parameterless constructor, like in the following snippet: @@ -155,7 +155,7 @@ public static class MyServiceConfigurationBuilder ### Define endpoints used in each test -To define an endpoint in tests a class inheriting from `EndpointConfigurationBuilder` needs to be created for each endpoint that needs to be used in a test. The best place to define such classes is as nested classes within the test class itself: +To define an endpoint in tests a class inheriting from `NServiceBus.AcceptanceTesting.EndpointConfigurationBuilder` needs to be created for each endpoint that needs to be used in a test. The best place to define such classes is as nested classes within the test class itself: @@ -182,7 +182,7 @@ public class When_sending_AMessage snippet source | anchor -The sample defines two endpoints, `MyServiceEndpoint` and `MyOtherServiceEndpoint`. `MyServiceEndpoint` uses the "inherit from EndpointConfiguration" approach to reference the production endpoint configuration. `MyOtherServiceEndpoint` uses the "builder class" by creating a custom endpoint template: +The sample defines two endpoints, `MyServiceEndpoint` and `MyOtherServiceEndpoint`. `MyServiceEndpoint` uses the "inherit from EndpointConfiguration" approach to reference the production endpoint configuration. `MyOtherServiceEndpoint` uses the "builder class" by inheriting from `NServiceBus.IntegrationTesting.EndpointTemplate`: @@ -201,7 +201,88 @@ class MyOtherServiceTemplate : EndpointTemplate snippet source | anchor -Using both approaches the endpoint configuration can be customized according to the environment needs, if needed. +The "builder class" approach allows specific modifications of the `EndpointConfiguration` for the tests. Although modifications should be kept to a minimum they are reasonable for a few aspects: + +- Retries + - Retries should be reduced or even disabled for tests. + - Otherwise (with the default retry configuration) the IntegrationTests throw a "Some failed messages were not handled by the recoverability feature."-Exception because of a fixed 30 sec timeout in the `NServiceBus.AcceptanceTests.ScenarioRunner`. +- Cleaning up the queues via `PurgeOnStartup(true)` + +#### Generic host support + +NServiceBus endpoints can be [hosted using the generic host](https://docs.particular.net/samples/hosting/generic-host/). When using the generic host the endpoint lifecycle and configuration are controlled by the host. The following is a sample endpoint hosted using the generic host: + + + +```cs +public static void Main(string[] args) +{ + CreateHostBuilder(args).Build().Run(); +} + +public static IHostBuilder CreateHostBuilder(string[] args) +{ + var builder = Host.CreateDefaultBuilder(args); + builder.UseConsoleLifetime(); + + builder.UseNServiceBus(ctx => + { + var config = new EndpointConfiguration("endpoint-name"); + config.UseTransport(); + + return config; + }); + + return builder; +} +``` +snippet source | anchor + + +> For more information about hosting NServiceBus using the generic host refer to the [official documentation](https://docs.particular.net/samples/hosting/generic-host/). + +Before using generic host hosted endpoints with `NServiceBus.IntegrationTesting`, a minor change to the above snippet is required: + + + +```cs +public static IHostBuilder CreateHostBuilder(string[] args, Action configPreview) +{ + var builder = Host.CreateDefaultBuilder(args); + builder.UseConsoleLifetime(); + + builder.UseNServiceBus(ctx => + { + var config = new EndpointConfiguration("endpoint-name"); + config.UseTransport(); + + configPreview?.Invoke(config); + + return config; + }); + + return builder; +} +``` +snippet source | anchor + + +The testing engine needs to access the endpoint configuration before it's initialized to register the needed tests behaviors. The creation of the `IHostBuilder` needs to be tweaked to invoke a callback delegate that the test engine injects at tests runtime. + +Finally, the endpoint can be added to the scenario using the `WithGenericHostEndpoint` configuration method: + + + +```cs +_ = await Scenario.Define() + .WithGenericHostEndpoint("endpoint-name", configPreview => Program.CreateHostBuilder(new string[0], configPreview).Build()) +``` +snippet source | anchor + + +Be sure to pass to the method that creates the `IHostBuilder` the provided `Action` parameter. If the endpoint is not configured correctly the following exception will be raised at test time: + +> Endpoint \ is not correctly configured to be tested. Make sure to pass the EndpointConfiguration instance to the Action provided by WithGenericHostEndpoint tests setup method. ### Define tests and completion criteria @@ -228,9 +309,9 @@ class MyOtherServiceEndpoint : EndpointConfigurationBuilder{ /* omited */ } snippet source | anchor -NOTE: The defined `Scenario` must use the `InterationScenarioContext` or a type that inherits from `InterationScenarioContext`. +NOTE: The defined `Scenario` must use the `IntegrationScenarioContext` or a type that inherits from `IntegrationScenarioContext`. -This tests aims to verify that when "MyService" sends a message to "MyOtherService" a reply is received by "MyService" and finally that a new saga instance is created. Use the `Define` static method to create a scenario and then add endpoints to the created scenario to append as many endpoints as needed for the scenario. Add a `Done` condition to specify when the test has to be considered completed adn finally invoke `Run` to exercise the `Scenario`. +This tests aims to verify that when "MyService" sends a message to "MyOtherService" a reply is received by "MyService" and finally that a new saga instance is created. Use the `Define` static method to create a scenario and then add endpoints to the created scenario to append as many endpoints as needed for the scenario. Add a `Done` condition to specify when the test has to be considered completed and finally invoke `Run` to exercise the `Scenario`. #### Done condition @@ -240,7 +321,7 @@ An end-to-end test execution can only be terminated by 3 events: - the test times out - there are unhandled exceptions -Unhandled exceptions are a sort of problem from the integration testing infrastructure perspecive as most of the times they'll result in messages being retried and eventually ending up in the error queue. based on this it's better to consider failed messages as part of the done condition: +Unhandled exceptions are a sort of problem from the integration testing infrastructure perspecive as most of the times they'll result in messages being retried and eventually ending up in the error queue. Based on this it's better to consider failed messages as part of the done condition: @@ -250,10 +331,10 @@ Unhandled exceptions are a sort of problem from the integration testing infrastr return ctx.HasFailedMessages(); }) ``` -snippet source | anchor +snippet source | anchor -Such a done condition has to be read has: "If there are one or more failed messages the test is done, proceed to evaulate the assertions". Obviously this is not enough. In the identified test case scenario the test is done when a saga is invoked (specifically is created, more on this later). A saga invokation can be expressed as a done condition in the following way: +Such a done condition has to be read as: "If there are one or more failed messages the test is done, proceed to evaulate the assertions". Obviously this is not enough. In the identified test case scenario the test is done when a saga is invoked (specifically is created, more on this later). A saga invokation can be expressed as a done condition in the following way: @@ -263,7 +344,7 @@ Such a done condition has to be read has: "If there are one or more failed messa return ctx.SagaWasInvoked() || ctx.HasFailedMessages(); }) ``` -snippet source | anchor +snippet source | anchor The integration scenario context, the `c` argument, can be "queried" to gather the status of the test, in this case the done condition is augmented to make so that the test is considered done when a saga of type `ASaga` has been invoked or there are failed messages. @@ -279,10 +360,10 @@ In the defined callback it's possible to define one or more "when" conditions th var context = await Scenario.Define() .WithEndpoint(builder => builder.When(session => session.Send(new AMessage()))) ``` -snippet source | anchor +snippet source | anchor -The above code snippet makes so that when "MyServiceEndpoint" is started `AMessage` is sent. `When` has multiple overloads to accommodate many different scenarios. +The above code snippet makes so that when "MyServiceEndpoint" is started `AMessage` is sent. `When` has multiple overloads (including one with a condition-parameter) to accommodate many different scenarios. ### Assert on tests results @@ -327,7 +408,7 @@ var context = await Scenario.Define(ctx => snippet source | anchor -The above sample test shows how to inject an NServiceBus Timeout reschedule rule. When the production code, in this case the `ASaga` saga, schedules the `ASaga.MyTimeout` message, the registered NServiceBus Timeout reschedule rule will be invoked and a new delivery constraint is created, in this sample, to make so that the NServiceBus Timeout expires in 5 seconds insted of the default production value. The NServiceBus Timeout reschedule rule receives as arguments the current NServiceBus Timeout message and the current delivery constraint. +The above sample test shows how to inject an NServiceBus Timeout reschedule rule. When the production code, in this case the `ASaga` saga, schedules the `ASaga.MyTimeout` message, the registered NServiceBus Timeout reschedule rule will be invoked and a new delivery constraint is created, in this sample, to make so that the NServiceBus Timeout expires in 5 seconds instead of the default production value. The NServiceBus Timeout reschedule rule receives as arguments the current NServiceBus Timeout message and the current delivery constraint. ## Limitations @@ -339,7 +420,7 @@ NServiceBus.IntegrationTesting is built on top of the NServiceBus.AcceptanceTest ### Assembly scanning setup -By default NServiceBus endpoints scan and load all assemblies found in the bin directory. This means that if more than one endpoints is loaded into the same process all endpoints will scan the same bin directory and all types related to NServiceBus, such as message handlers and/or sagas, are loaded by all endpoints. This can issues to endpoints running in end-to-end tests. It's suggested to configure the endpoint configuration to scan only a limited set of assemblies, and exclude those not related to the current endpoint. The assembly scanner configuration can be applied directly to the production endpoint configuration or as a customization in the test endpoint template setup. +By default NServiceBus endpoints scan and load all assemblies found in the bin directory. This means that if more than one endpoint is loaded into the same process all endpoints will scan the same bin directory and all types related to NServiceBus, such as message handlers and/or sagas, are loaded by all endpoints. This can issues to endpoints running in end-to-end tests. It's suggested to configure the endpoint configuration to scan only a limited set of assemblies, and exclude those not related to the current endpoint. The assembly scanner configuration can be applied directly to the production endpoint configuration or as a customization in the test endpoint template setup. diff --git a/README.source.md b/README.source.md index ffc3cac0..d1b5471f 100644 --- a/README.source.md +++ b/README.source.md @@ -6,7 +6,7 @@ NServiceBus.IntegrationTesting allows testing end-to-end business scenarios, exe ## Disclaimer -NServiceBus.IntegrationTesting is not affiliated with Particular Software and thus is not officially supported. It's evolution stage doesn't make it production ready yet. +NServiceBus.IntegrationTesting is not affiliated with Particular Software and thus is not officially supported. It's evolution stage doesn't make it production ready yet. ## tl;dr @@ -66,11 +66,11 @@ Defining an NServiceBus integration test is a multi-step process, composed of: ### Make sure endpoints configuration can be istantiated by tests -One of the goals of end-to-end testing an NServiceBus endpoints is to make sure that what gets tested is the real production code, not a copy of it crafted for the tests. The production endpoint configuration has to be used in tests. To make sure that the testing infrastructure can instantiate the endpoint configuration there are a couple of options, with many variations. +One of the goals of end-to-end testing a NServiceBus endpoint is to make sure that what gets tested is the real production code, not a copy of it crafted for the tests. The production endpoint configuration has to be used in tests. To make sure that the testing infrastructure can instantiate the endpoint configuration there are a couple of options, with many variations. #### Inherit from EndpointConfiguration -It's possible to create a class that inherits from `EndpointConfiguration` and then use it in both the production endpoint and the tests. To make so that the testing infrastructure con automatically instante it, the class must have a parameterless constructor, like in the following snippet: +It's possible to create a class that inherits from `EndpointConfiguration` and then use it in both the production endpoint and the tests. To make so that the testing infrastructure can automatically instantiate it, the class must have a parameterless constructor, like in the following snippet: snippet: inherit-from-endpoint-configuration @@ -84,15 +84,42 @@ snippet: use-builder-class ### Define endpoints used in each test -To define an endpoint in tests a class inheriting from `EndpointConfigurationBuilder` needs to be created for each endpoint that needs to be used in a test. The best place to define such classes is as nested classes within the test class itself: +To define an endpoint in tests a class inheriting from `NServiceBus.AcceptanceTesting.EndpointConfigurationBuilder` needs to be created for each endpoint that needs to be used in a test. The best place to define such classes is as nested classes within the test class itself: snippet: endpoints-used-in-each-test -The sample defines two endpoints, `MyServiceEndpoint` and `MyOtherServiceEndpoint`. `MyServiceEndpoint` uses the "inherit from EndpointConfiguration" approach to reference the production endpoint configuration. `MyOtherServiceEndpoint` uses the "builder class" by creating a custom endpoint template: +The sample defines two endpoints, `MyServiceEndpoint` and `MyOtherServiceEndpoint`. `MyServiceEndpoint` uses the "inherit from EndpointConfiguration" approach to reference the production endpoint configuration. `MyOtherServiceEndpoint` uses the "builder class" by inheriting from `NServiceBus.IntegrationTesting.EndpointTemplate`: snippet: my-other-service-template -Using both approaches the endpoint configuration can be customized according to the environment needs, if needed. +The "builder class" approach allows specific modifications of the `EndpointConfiguration` for the tests. Although modifications should be kept to a minimum they are reasonable for a few aspects: + +- Retries + - Retries should be reduced or even disabled for tests. + - Otherwise (with the default retry configuration) the IntegrationTests throw a "Some failed messages were not handled by the recoverability feature."-Exception because of a fixed 30 sec timeout in the `NServiceBus.AcceptanceTests.ScenarioRunner`. +- Cleaning up the queues via `PurgeOnStartup(true)` + +#### Generic host support + +NServiceBus endpoints can be [hosted using the generic host](https://docs.particular.net/samples/hosting/generic-host/). When using the generic host the endpoint lifecycle and configuration are controlled by the host. The following is a sample endpoint hosted using the generic host: + +snippet: basic-generic-host-endpoint + +> For more information about hosting NServiceBus using the generic host refer to the [official documentation](https://docs.particular.net/samples/hosting/generic-host/). + +Before using generic host hosted endpoints with `NServiceBus.IntegrationTesting`, a minor change to the above snippet is required: + +snippet: basic-generic-host-endpoint-with-config-previewer + +The testing engine needs to access the endpoint configuration before it's initialized to register the needed tests behaviors. The creation of the `IHostBuilder` needs to be tweaked to invoke a callback delegate that the test engine injects at tests runtime. + +Finally, the endpoint can be added to the scenario using the `WithGenericHostEndpoint` configuration method: + +snippet: with-generic-host-endpoint + +Be sure to pass to the method that creates the `IHostBuilder` the provided `Action` parameter. If the endpoint is not configured correctly the following exception will be raised at test time: + +> Endpoint \ is not correctly configured to be tested. Make sure to pass the EndpointConfiguration instance to the Action provided by WithGenericHostEndpoint tests setup method. ### Define tests and completion criteria @@ -102,9 +129,9 @@ Once endpoints are defined, the test choreography can be implemented, the first snippet: scenario-skeleton -NOTE: The defined `Scenario` must use the `InterationScenarioContext` or a type that inherits from `InterationScenarioContext`. +NOTE: The defined `Scenario` must use the `IntegrationScenarioContext` or a type that inherits from `IntegrationScenarioContext`. -This tests aims to verify that when "MyService" sends a message to "MyOtherService" a reply is received by "MyService" and finally that a new saga instance is created. Use the `Define` static method to create a scenario and then add endpoints to the created scenario to append as many endpoints as needed for the scenario. Add a `Done` condition to specify when the test has to be considered completed adn finally invoke `Run` to exercise the `Scenario`. +This tests aims to verify that when "MyService" sends a message to "MyOtherService" a reply is received by "MyService" and finally that a new saga instance is created. Use the `Define` static method to create a scenario and then add endpoints to the created scenario to append as many endpoints as needed for the scenario. Add a `Done` condition to specify when the test has to be considered completed and finally invoke `Run` to exercise the `Scenario`. #### Done condition @@ -114,11 +141,11 @@ An end-to-end test execution can only be terminated by 3 events: - the test times out - there are unhandled exceptions -Unhandled exceptions are a sort of problem from the integration testing infrastructure perspecive as most of the times they'll result in messages being retried and eventually ending up in the error queue. based on this it's better to consider failed messages as part of the done condition: +Unhandled exceptions are a sort of problem from the integration testing infrastructure perspecive as most of the times they'll result in messages being retried and eventually ending up in the error queue. Based on this it's better to consider failed messages as part of the done condition: snippet: simple-done-condition -Such a done condition has to be read has: "If there are one or more failed messages the test is done, proceed to evaulate the assertions". Obviously this is not enough. In the identified test case scenario the test is done when a saga is invoked (specifically is created, more on this later). A saga invokation can be expressed as a done condition in the following way: +Such a done condition has to be read as: "If there are one or more failed messages the test is done, proceed to evaulate the assertions". Obviously this is not enough. In the identified test case scenario the test is done when a saga is invoked (specifically is created, more on this later). A saga invokation can be expressed as a done condition in the following way: snippet: complete-done-condition @@ -131,7 +158,7 @@ In the defined callback it's possible to define one or more "when" conditions th snippet: kick-off-choreography -The above code snippet makes so that when "MyServiceEndpoint" is started `AMessage` is sent. `When` has multiple overloads to accommodate many different scenarios. +The above code snippet makes so that when "MyServiceEndpoint" is started `AMessage` is sent. `When` has multiple overloads (including one with a condition-parameter) to accommodate many different scenarios. ### Assert on tests results @@ -149,7 +176,7 @@ NServiceBus.IntegrationTesting provides a way to reschedule NServiceBus Timeouts snippet: timeouts-reschedule -The above sample test shows how to inject an NServiceBus Timeout reschedule rule. When the production code, in this case the `ASaga` saga, schedules the `ASaga.MyTimeout` message, the registered NServiceBus Timeout reschedule rule will be invoked and a new delivery constraint is created, in this sample, to make so that the NServiceBus Timeout expires in 5 seconds insted of the default production value. The NServiceBus Timeout reschedule rule receives as arguments the current NServiceBus Timeout message and the current delivery constraint. +The above sample test shows how to inject an NServiceBus Timeout reschedule rule. When the production code, in this case the `ASaga` saga, schedules the `ASaga.MyTimeout` message, the registered NServiceBus Timeout reschedule rule will be invoked and a new delivery constraint is created, in this sample, to make so that the NServiceBus Timeout expires in 5 seconds instead of the default production value. The NServiceBus Timeout reschedule rule receives as arguments the current NServiceBus Timeout message and the current delivery constraint. ## Limitations @@ -161,7 +188,7 @@ NServiceBus.IntegrationTesting is built on top of the NServiceBus.AcceptanceTest ### Assembly scanning setup -By default NServiceBus endpoints scan and load all assemblies found in the bin directory. This means that if more than one endpoints is loaded into the same process all endpoints will scan the same bin directory and all types related to NServiceBus, such as message handlers and/or sagas, are loaded by all endpoints. This can issues to endpoints running in end-to-end tests. It's suggested to configure the endpoint configuration to scan only a limited set of assemblies, and exclude those not related to the current endpoint. The assembly scanner configuration can be applied directly to the production endpoint configuration or as a customization in the test endpoint template setup. +By default NServiceBus endpoints scan and load all assemblies found in the bin directory. This means that if more than one endpoint is loaded into the same process all endpoints will scan the same bin directory and all types related to NServiceBus, such as message handlers and/or sagas, are loaded by all endpoints. This can issues to endpoints running in end-to-end tests. It's suggested to configure the endpoint configuration to scan only a limited set of assemblies, and exclude those not related to the current endpoint. The assembly scanner configuration can be applied directly to the production endpoint configuration or as a customization in the test endpoint template setup. snippet: assembly-scanner-config diff --git a/src/MyService/ASaga.cs b/src/MyService/ASaga.cs index ca242e35..f2bd6f07 100644 --- a/src/MyService/ASaga.cs +++ b/src/MyService/ASaga.cs @@ -1,4 +1,5 @@ -using MyMessages.Messages; +using Microsoft.Extensions.Logging; +using MyMessages.Messages; using NServiceBus; using System; using System.Threading.Tasks; @@ -10,6 +11,11 @@ public class ASaga : Saga, IHandleMessages, IHandleTimeouts { + public ASaga(ILogger logger) + { + logger.LogInformation("ASaga instance created successfully"); + } + public Task Handle(StartASaga message, IMessageHandlerContext context) { return RequestTimeout(context, DateTime.UtcNow.AddDays(10)); diff --git a/src/MyService/MyService.csproj b/src/MyService/MyService.csproj index c2bcbca0..135da28d 100644 --- a/src/MyService/MyService.csproj +++ b/src/MyService/MyService.csproj @@ -8,8 +8,12 @@ + + + + diff --git a/src/MyService/MyServiceConfiguration.cs b/src/MyService/MyServiceConfiguration.cs index 9f778898..80c52eb0 100644 --- a/src/MyService/MyServiceConfiguration.cs +++ b/src/MyService/MyServiceConfiguration.cs @@ -3,7 +3,7 @@ namespace MyService { - public class MyServiceConfiguration : EndpointConfiguration + class MyServiceConfiguration : EndpointConfiguration { public MyServiceConfiguration() : base("MyService") diff --git a/src/MyService/Program.cs b/src/MyService/Program.cs index aae3ae55..98ba1848 100644 --- a/src/MyService/Program.cs +++ b/src/MyService/Program.cs @@ -1,22 +1,36 @@ -using NServiceBus; +using Microsoft.Extensions.Hosting; +using NServiceBus; using System; -using System.Threading.Tasks; +using Serilog; namespace MyService { - class Program + public class Program { - static async Task Main(string[] args) + public static void Main(string[] args) { - Console.Title = typeof(Program).Namespace; + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args, Action configPreview = null) + { + var builder = Host.CreateDefaultBuilder(args); + builder.UseConsoleLifetime(); + + builder.UseSerilog((context, services, loggerConfiguration) => loggerConfiguration + .ReadFrom.Configuration(context.Configuration) + .Enrich.FromLogContext() + .WriteTo.Console()); - var endpointConfiguration = new MyServiceConfiguration(); - var endpointInstance = await Endpoint.Start(endpointConfiguration); + builder.UseNServiceBus(ctx => + { + var config = new MyServiceConfiguration(); + configPreview?.Invoke(config); - Console.WriteLine($"{typeof(Program).Namespace} started. Press any key to stop."); - Console.ReadLine(); + return config; + }); - await endpointInstance.Stop(); + return builder; } } -} +} \ No newline at end of file diff --git a/src/MySystem.AcceptanceTests/When_requesting_a_timeout.cs b/src/MySystem.AcceptanceTests/When_requesting_a_timeout.cs index cd013f1e..08419a74 100644 --- a/src/MySystem.AcceptanceTests/When_requesting_a_timeout.cs +++ b/src/MySystem.AcceptanceTests/When_requesting_a_timeout.cs @@ -31,7 +31,7 @@ public async Task It_should_be_rescheduled_and_handled() { ctx.RegisterTimeoutRescheduleRule((msg, delay) => new DoNotDeliverBefore(DateTime.UtcNow.AddSeconds(5))); }) - .WithEndpoint(behavior => + .WithGenericHostEndpoint("MyService", configPreview => Program.CreateHostBuilder(new string[0], configPreview).Build(), behavior => { behavior.When(session => session.Send("MyService", new StartASaga() {AnIdentifier = Guid.NewGuid()})); }) @@ -42,13 +42,5 @@ public async Task It_should_be_rescheduled_and_handled() Assert.False(context.HasFailedMessages()); Assert.False(context.HasHandlingErrors()); } - - class MyServiceEndpoint : EndpointConfigurationBuilder - { - public MyServiceEndpoint() - { - EndpointSetup>(); - } - } } } diff --git a/src/MySystem.AcceptanceTests/When_sending_AMessage.cs b/src/MySystem.AcceptanceTests/When_sending_AMessage.cs index a28bdf8b..10c52a7d 100644 --- a/src/MySystem.AcceptanceTests/When_sending_AMessage.cs +++ b/src/MySystem.AcceptanceTests/When_sending_AMessage.cs @@ -1,5 +1,4 @@ using MyMessages.Messages; -using MyOtherService; using MyService; using NServiceBus; using NServiceBus.AcceptanceTesting; @@ -30,9 +29,9 @@ public async Task AReplyMessage_is_received_and_ASaga_is_started() { var theExpectedIdentifier = Guid.NewGuid(); var context = await Scenario.Define() - .WithEndpoint(behavior => + .WithGenericHostEndpoint("MyService", configPreview => Program.CreateHostBuilder(new string[0], configPreview).Build(), behavior => { - behavior.When(session => session.Send(new AMessage() {AnIdentifier = theExpectedIdentifier})); + behavior.When(session => session.Send(new AMessage() { AnIdentifier = theExpectedIdentifier })); }) .WithEndpoint() .Done(c => c.SagaWasInvoked() || c.HasFailedMessages()) @@ -47,14 +46,6 @@ public async Task AReplyMessage_is_received_and_ASaga_is_started() Assert.False(context.HasHandlingErrors()); } - class MyServiceEndpoint : EndpointConfigurationBuilder - { - public MyServiceEndpoint() - { - EndpointSetup>(); - } - } - class MyOtherServiceEndpoint : EndpointConfigurationBuilder { public MyOtherServiceEndpoint() diff --git a/src/MySystem.AcceptanceTests/When_sending_CompleteASaga.cs b/src/MySystem.AcceptanceTests/When_sending_CompleteASaga.cs index dc09365d..262c843a 100644 --- a/src/MySystem.AcceptanceTests/When_sending_CompleteASaga.cs +++ b/src/MySystem.AcceptanceTests/When_sending_CompleteASaga.cs @@ -29,11 +29,11 @@ public async Task ASaga_is_completed() { var theExpectedIdentifier = Guid.NewGuid(); var context = await Scenario.Define() - .WithEndpoint(behavior => + .WithGenericHostEndpoint("MyService", configPreview => Program.CreateHostBuilder(new string[0], configPreview).Build(), behavior => { behavior.When(session => { - return session.Send("MyService", new StartASaga() {AnIdentifier = theExpectedIdentifier}); + return session.SendLocal(new StartASaga() {AnIdentifier = theExpectedIdentifier}); }); behavior.When(condition: ctx => { @@ -59,13 +59,5 @@ public async Task ASaga_is_completed() Assert.False(context.HasFailedMessages()); Assert.False(context.HasHandlingErrors()); } - - class MyServiceEndpoint : EndpointConfigurationBuilder - { - public MyServiceEndpoint() - { - EndpointSetup>(); - } - } } } diff --git a/src/NServiceBus.IntegrationTesting.Tests.TestEndpoint/NServiceBus.IntegrationTesting.Tests.TestEndpoint.csproj b/src/NServiceBus.IntegrationTesting.Tests.TestEndpoint/NServiceBus.IntegrationTesting.Tests.TestEndpoint.csproj new file mode 100644 index 00000000..2a9d94f6 --- /dev/null +++ b/src/NServiceBus.IntegrationTesting.Tests.TestEndpoint/NServiceBus.IntegrationTesting.Tests.TestEndpoint.csproj @@ -0,0 +1,19 @@ + + + + Exe + netcoreapp3.1 + latest + + + + + + + + + + + + + diff --git a/src/NServiceBus.IntegrationTesting.Tests.TestEndpoint/Program.cs b/src/NServiceBus.IntegrationTesting.Tests.TestEndpoint/Program.cs new file mode 100644 index 00000000..d26e5c66 --- /dev/null +++ b/src/NServiceBus.IntegrationTesting.Tests.TestEndpoint/Program.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Hosting; + +namespace NServiceBus.IntegrationTesting.Tests.TestEndpoint +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + var builder = Host.CreateDefaultBuilder(args); + builder.UseConsoleLifetime(); + + builder.UseNServiceBus(ctx => + { + var config = new EndpointConfiguration("NServiceBus.IntegrationTesting.Tests.TestEndpoint"); + config.UseTransport(); + + config.AssemblyScanner().ExcludeAssemblies("NServiceBus.IntegrationTesting.Tests.dll"); + + return config; + }); + + return builder; + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.IntegrationTesting.Tests/API/APIApprovals.Approve_API.approved.txt b/src/NServiceBus.IntegrationTesting.Tests/API/APIApprovals.Approve_API.approved.txt index 1e8095e6..3a742822 100644 --- a/src/NServiceBus.IntegrationTesting.Tests/API/APIApprovals.Approve_API.approved.txt +++ b/src/NServiceBus.IntegrationTesting.Tests/API/APIApprovals.Approve_API.approved.txt @@ -32,6 +32,18 @@ namespace NServiceBus.IntegrationTesting public EndpointTemplate() { } protected override System.Threading.Tasks.Task OnGetConfiguration(NServiceBus.AcceptanceTesting.Support.RunDescriptor runDescriptor, NServiceBus.AcceptanceTesting.Support.EndpointCustomizationConfiguration endpointConfiguration, System.Action configurationBuilderCustomization) { } } + public class GenericHostEndpointBehaviorBuilder + where TContext : NServiceBus.AcceptanceTesting.ScenarioContext + { + public GenericHostEndpointBehaviorBuilder() { } + public System.Collections.Generic.IList Whens { get; } + public NServiceBus.IntegrationTesting.GenericHostEndpointBehaviorBuilder When(System.Func action) { } + public NServiceBus.IntegrationTesting.GenericHostEndpointBehaviorBuilder When(System.Func action) { } + public NServiceBus.IntegrationTesting.GenericHostEndpointBehaviorBuilder When(System.Func> condition, System.Func action) { } + public NServiceBus.IntegrationTesting.GenericHostEndpointBehaviorBuilder When(System.Func> condition, System.Func action) { } + public NServiceBus.IntegrationTesting.GenericHostEndpointBehaviorBuilder When(System.Predicate condition, System.Func action) { } + public NServiceBus.IntegrationTesting.GenericHostEndpointBehaviorBuilder When(System.Predicate condition, System.Func action) { } + } public class HandlerInvocation : NServiceBus.IntegrationTesting.Invocation { public HandlerInvocation() { } @@ -100,6 +112,11 @@ namespace NServiceBus.IntegrationTesting public NServiceBus.IContainSagaData SagaData { get; } public System.Type SagaType { get; } } + public static class ScenarioWithEndpointBehaviorExtensions + { + public static NServiceBus.AcceptanceTesting.Support.IScenarioWithEndpointBehavior WithGenericHostEndpoint(this NServiceBus.AcceptanceTesting.Support.IScenarioWithEndpointBehavior scenarioWithEndpoint, string endpointName, System.Func, Microsoft.Extensions.Hosting.IHost> hostBuilder, System.Action> behavior = null) + where TContext : NServiceBus.AcceptanceTesting.ScenarioContext { } + } public class SendOperation : NServiceBus.IntegrationTesting.OutgoingMessageOperation { public SendOperation() { } diff --git a/src/NServiceBus.IntegrationTesting.Tests/API/ApiApprovals.cs b/src/NServiceBus.IntegrationTesting.Tests/API/ApiApprovals.cs index d2d951dc..31513c33 100644 --- a/src/NServiceBus.IntegrationTesting.Tests/API/ApiApprovals.cs +++ b/src/NServiceBus.IntegrationTesting.Tests/API/ApiApprovals.cs @@ -1,7 +1,6 @@ using System.Runtime.CompilerServices; using ApprovalTests; using ApprovalTests.Reporters; -using NServiceBus.IntegrationTesting; using NUnit.Framework; using PublicApiGenerator; diff --git a/src/NServiceBus.IntegrationTesting.Tests/NServiceBus.IntegrationTesting.Tests.csproj b/src/NServiceBus.IntegrationTesting.Tests/NServiceBus.IntegrationTesting.Tests.csproj index 7f90f6b3..3fa6491d 100644 --- a/src/NServiceBus.IntegrationTesting.Tests/NServiceBus.IntegrationTesting.Tests.csproj +++ b/src/NServiceBus.IntegrationTesting.Tests/NServiceBus.IntegrationTesting.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/src/NServiceBus.IntegrationTesting.Tests/Publish_Operation_Interceptor.cs b/src/NServiceBus.IntegrationTesting.Tests/Publish_Operation_Interceptor.cs index 30e188ce..fcdf06a9 100644 --- a/src/NServiceBus.IntegrationTesting.Tests/Publish_Operation_Interceptor.cs +++ b/src/NServiceBus.IntegrationTesting.Tests/Publish_Operation_Interceptor.cs @@ -2,7 +2,6 @@ using NServiceBus.Pipeline; using NServiceBus.Testing; using NUnit.Framework; -using System; using System.Linq; using System.Threading.Tasks; diff --git a/src/NServiceBus.IntegrationTesting.Tests/Reply_Operation_Interceptor.cs b/src/NServiceBus.IntegrationTesting.Tests/Reply_Operation_Interceptor.cs index 90e3bb4c..415ce708 100644 --- a/src/NServiceBus.IntegrationTesting.Tests/Reply_Operation_Interceptor.cs +++ b/src/NServiceBus.IntegrationTesting.Tests/Reply_Operation_Interceptor.cs @@ -2,7 +2,6 @@ using NServiceBus.Pipeline; using NServiceBus.Testing; using NUnit.Framework; -using System; using System.Linq; using System.Threading.Tasks; diff --git a/src/NServiceBus.IntegrationTesting.Tests/Send_Operation_Interceptor.cs b/src/NServiceBus.IntegrationTesting.Tests/Send_Operation_Interceptor.cs index e4b328b6..6fa52456 100644 --- a/src/NServiceBus.IntegrationTesting.Tests/Send_Operation_Interceptor.cs +++ b/src/NServiceBus.IntegrationTesting.Tests/Send_Operation_Interceptor.cs @@ -1,11 +1,7 @@ using MyMessages.Messages; -using NServiceBus.DelayedDelivery; -using NServiceBus.DeliveryConstraints; using NServiceBus.Pipeline; using NServiceBus.Testing; using NUnit.Framework; -using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/src/NServiceBus.IntegrationTesting.Tests/When_capturing_handlers_invocations.cs b/src/NServiceBus.IntegrationTesting.Tests/When_capturing_handlers_invocations.cs index b0dc9306..7e85dbab 100644 --- a/src/NServiceBus.IntegrationTesting.Tests/When_capturing_handlers_invocations.cs +++ b/src/NServiceBus.IntegrationTesting.Tests/When_capturing_handlers_invocations.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using NUnit.Framework; namespace NServiceBus.IntegrationTesting.Tests diff --git a/src/NServiceBus.IntegrationTesting.Tests/When_capturing_sagas_invocations.cs b/src/NServiceBus.IntegrationTesting.Tests/When_capturing_sagas_invocations.cs index ba31d646..2aae6e62 100644 --- a/src/NServiceBus.IntegrationTesting.Tests/When_capturing_sagas_invocations.cs +++ b/src/NServiceBus.IntegrationTesting.Tests/When_capturing_sagas_invocations.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using NUnit.Framework; namespace NServiceBus.IntegrationTesting.Tests diff --git a/src/NServiceBus.IntegrationTesting.Tests/When_using_generic_host.cs b/src/NServiceBus.IntegrationTesting.Tests/When_using_generic_host.cs new file mode 100644 index 00000000..111eb41a --- /dev/null +++ b/src/NServiceBus.IntegrationTesting.Tests/When_using_generic_host.cs @@ -0,0 +1,31 @@ +using NServiceBus.AcceptanceTesting; +using NUnit.Framework; +using System; +using NServiceBus.IntegrationTesting.Tests.TestEndpoint; + +namespace NServiceBus.IntegrationTesting.Tests +{ + public class When_using_generic_host + { + [Test] + public void Ensure_endpoint_is_correctly_configured() + { + var endpointName = "NServiceBus.IntegrationTesting.Tests.TestEndpoint"; + var expectedMessage = $"Endpoint {endpointName} is not correctly configured to be tested. " + + $"Make sure to pass the EndpointConfiguration instance to the Action " + + $"provided by WithGenericHostEndpoint tests setup method."; + + var ex = _ = Assert.ThrowsAsync(async () => + { + await Scenario.Define() + .WithGenericHostEndpoint( + endpointName, + configPreview => Program.CreateHostBuilder(new string[0]).Build(), + _ => { }) + .Run(); + }); + + Assert.AreEqual(expectedMessage, ex.Message); + } + } +} diff --git a/src/NServiceBus.IntegrationTesting.sln b/src/NServiceBus.IntegrationTesting.sln index 733920d9..cb936d29 100644 --- a/src/NServiceBus.IntegrationTesting.sln +++ b/src/NServiceBus.IntegrationTesting.sln @@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NServiceBus.IntegrationTest EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snippets", "Snippets\Snippets.csproj", "{927BC675-5940-43C6-BB97-96DA64CBA6D0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NServiceBus.IntegrationTesting.Tests.TestEndpoint", "NServiceBus.IntegrationTesting.Tests.TestEndpoint\NServiceBus.IntegrationTesting.Tests.TestEndpoint.csproj", "{A830D9C2-07D2-49F3-996A-9AE9EE85B38B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,6 +59,10 @@ Global {927BC675-5940-43C6-BB97-96DA64CBA6D0}.Debug|Any CPU.Build.0 = Debug|Any CPU {927BC675-5940-43C6-BB97-96DA64CBA6D0}.Release|Any CPU.ActiveCfg = Release|Any CPU {927BC675-5940-43C6-BB97-96DA64CBA6D0}.Release|Any CPU.Build.0 = Release|Any CPU + {A830D9C2-07D2-49F3-996A-9AE9EE85B38B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A830D9C2-07D2-49F3-996A-9AE9EE85B38B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A830D9C2-07D2-49F3-996A-9AE9EE85B38B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A830D9C2-07D2-49F3-996A-9AE9EE85B38B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/NServiceBus.IntegrationTesting/EnsureEndpointIsConfiguredForTests.cs b/src/NServiceBus.IntegrationTesting/EnsureEndpointIsConfiguredForTests.cs new file mode 100644 index 00000000..e2dd9b3e --- /dev/null +++ b/src/NServiceBus.IntegrationTesting/EnsureEndpointIsConfiguredForTests.cs @@ -0,0 +1,4 @@ +namespace NServiceBus.IntegrationTesting +{ + class EnsureEndpointIsConfiguredForTests{ } +} \ No newline at end of file diff --git a/src/NServiceBus.IntegrationTesting/GenericHostEndpointBehavior.cs b/src/NServiceBus.IntegrationTesting/GenericHostEndpointBehavior.cs new file mode 100644 index 00000000..72993793 --- /dev/null +++ b/src/NServiceBus.IntegrationTesting/GenericHostEndpointBehavior.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using NServiceBus.AcceptanceTesting.Support; + +namespace NServiceBus.IntegrationTesting +{ + class GenericHostEndpointBehavior : IComponentBehavior + { + readonly Func, IHost> hostBuilder; + readonly IList whens; + readonly string endpointName; + + public GenericHostEndpointBehavior(string endpointName, Func, IHost> hostBuilder, IList whens) + { + this.endpointName = endpointName; + this.hostBuilder = hostBuilder; + this.whens = whens; + } + + public Task CreateRunner(RunDescriptor runDescriptor) + { + var host = hostBuilder(configuration => + { + configuration.RegisterRequiredPipelineBehaviors(endpointName, (IntegrationScenarioContext)runDescriptor.ScenarioContext); + configuration.RegisterScenarioContext(runDescriptor.ScenarioContext); + configuration.RegisterComponents(r => { r.RegisterSingleton(new EnsureEndpointIsConfiguredForTests()); }); + }); + var runner = new GenericHostEndpointRunner(runDescriptor, endpointName, host, whens); + return Task.FromResult((ComponentRunner)runner); + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.IntegrationTesting/GenericHostEndpointBehaviorBuilder.cs b/src/NServiceBus.IntegrationTesting/GenericHostEndpointBehaviorBuilder.cs new file mode 100644 index 00000000..7862196f --- /dev/null +++ b/src/NServiceBus.IntegrationTesting/GenericHostEndpointBehaviorBuilder.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using NServiceBus.AcceptanceTesting; +using NServiceBus.AcceptanceTesting.Support; + +namespace NServiceBus.IntegrationTesting +{ + public class GenericHostEndpointBehaviorBuilder where TContext : ScenarioContext + { + public GenericHostEndpointBehaviorBuilder When(Func action) + { + return When(c => true, action); + } + + public GenericHostEndpointBehaviorBuilder When(Func action) + { + return When(c => true, action); + } + + public GenericHostEndpointBehaviorBuilder When(Func> condition, Func action) + { + Whens.Add(new WhenDefinition(condition, action)); + + return this; + } + + public GenericHostEndpointBehaviorBuilder When(Predicate condition, Func action) + { + Whens.Add(new WhenDefinition(ctx => Task.FromResult(condition(ctx)), action)); + + return this; + } + + public GenericHostEndpointBehaviorBuilder When(Func> condition, Func action) + { + Whens.Add(new WhenDefinition(condition, action)); + + return this; + } + + public GenericHostEndpointBehaviorBuilder When(Predicate condition, Func action) + { + Whens.Add(new WhenDefinition(ctx => Task.FromResult(condition(ctx)), action)); + + return this; + } + + public IList Whens { get; } = new List(); + } +} \ No newline at end of file diff --git a/src/NServiceBus.IntegrationTesting/GenericHostEndpointRunner.cs b/src/NServiceBus.IntegrationTesting/GenericHostEndpointRunner.cs new file mode 100644 index 00000000..82ff902d --- /dev/null +++ b/src/NServiceBus.IntegrationTesting/GenericHostEndpointRunner.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NServiceBus.AcceptanceTesting.Support; +using NServiceBus.Logging; + +namespace NServiceBus.IntegrationTesting +{ + class GenericHostEndpointRunner : ComponentRunner + { + static ILog Logger = LogManager.GetLogger(); + readonly RunDescriptor runDescriptor; + readonly IHost host; + readonly IList whens; + + public GenericHostEndpointRunner(RunDescriptor runDescriptor, string endpointName, IHost host, IList whens) + { + Name = endpointName; + this.runDescriptor = runDescriptor; + this.host = host; + this.whens = whens; + } + + public override string Name { get; } + + public override async Task Start(CancellationToken token) + { + await host.StartAsync(token); + + EnsureEndpointIsConfiguredForTests(); + + var messageSession = host.Services.GetRequiredService(); + + //TODO: How to access ScenarioContext.CurrentEndpoint + // ScenarioContext.CurrentEndpoint = Name; + try + { + if (whens.Count != 0) + { + await Task.Run(async () => + { + var executedWhens = new HashSet(); + + while (!token.IsCancellationRequested) + { + if (executedWhens.Count == whens.Count) + { + break; + } + + if (token.IsCancellationRequested) + { + break; + } + + foreach (var when in whens) + { + if (token.IsCancellationRequested) + { + break; + } + + if (executedWhens.Contains(when.Id)) + { + continue; + } + + if (await when.ExecuteAction(runDescriptor.ScenarioContext, messageSession).ConfigureAwait(false)) + { + executedWhens.Add(when.Id); + } + } + + await Task.Yield(); // enforce yield current context, tight loop could introduce starvation + } + }, token).ConfigureAwait(false); + } + } + catch (Exception ex) + { + Logger.Error($"Failed to execute Whens on endpoint{Name}", ex); + + throw; + } + } + + private void EnsureEndpointIsConfiguredForTests() + { + var check = host.Services.GetService(); + if (check == null) + { + throw new Exception($"Endpoint {Name} is not correctly configured to be tested. " + + $"Make sure to pass the EndpointConfiguration instance to the " + + $"Action provided by WithGenericHostEndpoint " + + $"tests setup method."); + } + } + + public override async Task Stop() + { + //TODO: How to access ScenarioContext.CurrentEndpoint + // ScenarioContext.CurrentEndpoint = Name; + try + { + await host.StopAsync(); + host.Dispose(); + } + catch (Exception ex) + { + Logger.Error("Failed to stop endpoint " + Name, ex); + throw; + } + + ThrowOnFailedMessages(); + } + + void ThrowOnFailedMessages() + { + foreach (var failedMessage in runDescriptor.ScenarioContext.FailedMessages.Where(kvp => kvp.Key == Name)) + { + throw new MessageFailedException(failedMessage.Value.First(), runDescriptor.ScenarioContext); + } + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.IntegrationTesting/IntegrationScenarioContext.cs b/src/NServiceBus.IntegrationTesting/IntegrationScenarioContext.cs index e6c74e12..1b57dd8a 100644 --- a/src/NServiceBus.IntegrationTesting/IntegrationScenarioContext.cs +++ b/src/NServiceBus.IntegrationTesting/IntegrationScenarioContext.cs @@ -4,7 +4,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Reflection; namespace NServiceBus.IntegrationTesting { diff --git a/src/NServiceBus.IntegrationTesting/InterceptSendOperations.cs b/src/NServiceBus.IntegrationTesting/InterceptSendOperations.cs index a3019f2e..b047074e 100644 --- a/src/NServiceBus.IntegrationTesting/InterceptSendOperations.cs +++ b/src/NServiceBus.IntegrationTesting/InterceptSendOperations.cs @@ -1,10 +1,5 @@ -using NServiceBus.DelayedDelivery; -using NServiceBus.DeliveryConstraints; -using NServiceBus.Pipeline; -using NUnit.Framework; +using NServiceBus.Pipeline; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; namespace NServiceBus.IntegrationTesting diff --git a/src/NServiceBus.IntegrationTesting/NServiceBus.IntegrationTesting.csproj b/src/NServiceBus.IntegrationTesting/NServiceBus.IntegrationTesting.csproj index f2dbde00..0dfe3004 100644 --- a/src/NServiceBus.IntegrationTesting/NServiceBus.IntegrationTesting.csproj +++ b/src/NServiceBus.IntegrationTesting/NServiceBus.IntegrationTesting.csproj @@ -20,12 +20,13 @@ - + - - + + + @@ -33,7 +34,7 @@ all runtime; build; native; contentfiles; analyzers - + diff --git a/src/NServiceBus.IntegrationTesting/ScenarioWithEndpointBehaviorExtensions.cs b/src/NServiceBus.IntegrationTesting/ScenarioWithEndpointBehaviorExtensions.cs new file mode 100644 index 00000000..3c3ef31e --- /dev/null +++ b/src/NServiceBus.IntegrationTesting/ScenarioWithEndpointBehaviorExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.Extensions.Hosting; +using NServiceBus.AcceptanceTesting; +using NServiceBus.AcceptanceTesting.Support; + +namespace NServiceBus.IntegrationTesting +{ + public static class ScenarioWithEndpointBehaviorExtensions + { + public static IScenarioWithEndpointBehavior WithGenericHostEndpoint(this IScenarioWithEndpointBehavior scenarioWithEndpoint, + string endpointName, Func, IHost> hostBuilder, Action> behavior = null) where TContext : ScenarioContext + { + var behaviorBuilder = new GenericHostEndpointBehaviorBuilder(); + behavior?.Invoke(behaviorBuilder); + + scenarioWithEndpoint.WithComponent(new GenericHostEndpointBehavior(endpointName, hostBuilder, behaviorBuilder.Whens)); + + return scenarioWithEndpoint; + } + } +} \ No newline at end of file diff --git a/src/Snippets/DoneSnippets.cs b/src/Snippets/DoneSnippets.cs index 44a6d91c..cd92d5aa 100644 --- a/src/Snippets/DoneSnippets.cs +++ b/src/Snippets/DoneSnippets.cs @@ -2,7 +2,6 @@ using MyService; using NServiceBus.AcceptanceTesting; using NServiceBus.IntegrationTesting; -using NUnit.Framework; namespace DoneSnippets { diff --git a/src/Snippets/GenericHostSnippets.cs b/src/Snippets/GenericHostSnippets.cs new file mode 100644 index 00000000..8dbc45cc --- /dev/null +++ b/src/Snippets/GenericHostSnippets.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using NServiceBus; +using NServiceBus.AcceptanceTesting; +using NServiceBus.IntegrationTesting; +using NUnit.Framework; + +namespace GenericHostSnippets +{ + public class When_sending_AMessage + { + [Test] + public async Task AReplyMessage_is_received_and_ASaga_is_started() + { + // begin-snippet: with-generic-host-endpoint + _ = await Scenario.Define() + .WithGenericHostEndpoint("endpoint-name", configPreview => Program.CreateHostBuilder(new string[0], configPreview).Build()) + // end-snippet + .Done(ctx=>false) + .Run(); + } + } + + class Program + { + // begin-snippet: basic-generic-host-endpoint + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + var builder = Host.CreateDefaultBuilder(args); + builder.UseConsoleLifetime(); + + builder.UseNServiceBus(ctx => + { + var config = new EndpointConfiguration("endpoint-name"); + config.UseTransport(); + + return config; + }); + + return builder; + } + // end-snippet + + // begin-snippet: basic-generic-host-endpoint-with-config-previewer + public static IHostBuilder CreateHostBuilder(string[] args, Action configPreview) + { + var builder = Host.CreateDefaultBuilder(args); + builder.UseConsoleLifetime(); + + builder.UseNServiceBus(ctx => + { + var config = new EndpointConfiguration("endpoint-name"); + config.UseTransport(); + + configPreview?.Invoke(config); + + return config; + }); + + return builder; + } + // end-snippet + } +} \ No newline at end of file diff --git a/src/Snippets/KickOffSnippets.cs b/src/Snippets/KickOffSnippets.cs index 09a63fc8..1dbfed09 100644 --- a/src/Snippets/KickOffSnippets.cs +++ b/src/Snippets/KickOffSnippets.cs @@ -1,10 +1,8 @@ using System.Threading.Tasks; using MyMessages.Messages; -using MyService; using NServiceBus; using NServiceBus.AcceptanceTesting; using NServiceBus.IntegrationTesting; -using NUnit.Framework; namespace KickOffSnippets { diff --git a/src/Snippets/Snippets.csproj b/src/Snippets/Snippets.csproj index 6e0132b3..aa72f3cc 100644 --- a/src/Snippets/Snippets.csproj +++ b/src/Snippets/Snippets.csproj @@ -4,6 +4,10 @@ netcoreapp3.1 + + + +