• Blog
  • Info Support
  • Career
  • Training
  • International Group
  • Info Support
  • Blog
  • Career
  • Training
  • International Group
  • Search
logo InfoSupport
  • Latest blogs
  • Popular blogs
  • Experts
      • Alles
      • Bloggers
      • Speakers
  • Meet us
  • About us
    • nl
    • en
    • .NET
    • Advanced Analytics
    • Agile
    • Akka
    • Alexa
    • Algorithms
    • Api's
    • Architectuur
    • Artificial Intelligence
    • ATDD
    • Augmented Reality
    • AWS
    • Azure
    • Big Data
    • Blockchain
    • Business Intelligence
    • Cloud
    • Code Combat
    • Cognitive Services
    • Communicatie
    • Containers
    • Continuous Delivery
    • CQRS
    • Cyber Security
    • Dapr
    • Data
    • Data & Analystics
    • Data Science
    • Data Warehousing
    • Databricks
    • DevOps
    • Digital Days
    • Docker
    • eHealth
    • Enterprise Architecture
    • Hacking
    • Infrastructure & Hosting
    • Innovatie
    • Integration
    • Internet of Things
    • Java
    • Machine Learning
    • Microservices
    • Microsoft
    • Microsoft Bot Framework
    • Microsoft Data Platform
    • Mobile Development
    • Mutation Testing
    • Open source
    • Pepper
    • Power BI
    • Privacy & Ethiek
    • Python
    • Quality Assistance & Test
    • Quality Assurance & Test
    • Requirements Management
    • Scala
    • Scratch
    • Security
    • SharePoint
    • Software Architecture
    • Software development
    • Software Factory
    • SQL Server
    • SSL
    • Start-up
    • Startup thinking
    • Stryker
    • Test Quality
    • Testing
    • TLS
    • TypeScript
    • Various
    • Web Development
    • Web-scale IT
    • Xamarin
    • Alles
    • Bloggers
    • Speakers
Home » Automating end-to-end NServiceBus tests with NServiceBus.AcceptanceTesting
  • Automating end-to-end NServiceBus tests with NServiceBus.AcceptanceTesting

    • By Roy Cornelissen
    • Various 7 years ago
    • Various 0 comments
    • Various Various
    Automating end-to-end NServiceBus tests with NServiceBus.AcceptanceTesting

    Photo Credit: LoveInTheWinter via Compfight cc

    Photo Credit: LoveInTheWinter via Compfight cc

    Most of you will agree that automating software tests is a good idea. Writing unit tests is almost a no brainer nowadays, and I’m a big fan of Behavior Driven Development and the use of Cucumber to bring together system analysts, programmers and testers more closely. The closer your tests and documentation are to the actual software, the better, IMO.

    Repeatable and automated functional tests are paramount to guarantee the quality of a constantly evolving software system. Especially when things become more and more complex, like in distributed systems. As you may know I’m a fan of NServiceBus, and testing our NServiceBus message based systems end-to-end has always been a bit cumbersome. The fine folks at Particular Software – creators of NServiceBus – have found a nice way to do their own integration and acceptance tests, and you can use that too!

    The framework that supports this is somewhat of a hidden gem in the NServiceBus stack, and I know that the Particular team is still refining the ideas. Nonetheless, you can use it yourself. It’s called NServiceBus.AcceptanceTesting. Unfortunately it’s somewhat undocumented so it’s not easily discovered and not very easy to get started with. You’ll need to dive into the acceptance tests in the NServiceBus source code to find out how it works. This can be a little bit hairy because there’s a lot going on in these tests to validate all the different transports, persistence, behavior pipeline and messaging scenarios that NServiceBus supports. This means that there is a lot of infrastructure code in the NServiceBus acceptance test suite as well to facilitate all the different scenarios. How to distinguish between what’s in the AcceptanceTesting framework and what’s not?

    As a sample, I created a simpler scenario with two services and added a couple of acceptance tests to offer a stripped down application of the AcceptanceTesting framework. You can find the full solution on GitHub, but I’ll give a step by step breakdown below.

    The scenario
    The sample scenario consists of two services: Sales and Shipping. When the Sales service receives a RegisterOrder command &#8211; say from a web front end &#8211; it does some business logic (e.g. validate if the amount <= 500) and decides whether the order is accepted or refused. Sales will publish an event accordingly: either OrderAccepted or OrderReceived. The Shipping service subscribes to the OrderAccepted event. It will ship the order as soon as it is accepted and publish an OrderShipped event. Like so:

    NServiceBusAcceptanceTestScenario

    I&#8217;m sure it won&#8217;t bring me the Nobel prize for software architecture, but that&#8217;s not the point. From a testing perspective, we&#8217;d like to know if a valid order actually gets shipped, and if an invalid order is refused (and not shipped).

    Project setup
    Once you have your solution set up with a Messages library, and the implementation projects for your message handlers, we&#8217;ll add a test project for our acceptance tests. You can use your favourite unit test framework, I chose MSTest in my sample.

    Next, in your test project, add a reference to the NServiceBus.AcceptanceTesting package via the Package Manager Console:

    Install-Package NServiceBus.AcceptanceTesting
    

    This will pull down the necessary dependencies for you to start writing acceptance tests.

    Writing a test
    Let&#8217;s have a look at one of the tests I have implemented in my sample:

    [TestMethod]
    public void Order_of_500_should_be_accepted_and_shipped()
    {
        Scenario.Define(() => new Context { })
            .WithEndpoint<Sales>(b => 
                b.Given((bus, context) =>
                    // The SubscriptionBehavior will monitor for incoming subscription messages
                    // Here we want to track if Shipping is subscribing to our the OrderAccepted event
                    SubscriptionBehavior.OnEndpointSubscribed(s => 
                    {
                        if (s.SubscriberReturnAddress.Queue.Contains(&quot;Shipping&quot;))
                        {
                            context.ShippingIsSubscribed = true;
                        }
                    }))
                    // As soon as ShippingIsSubscribed (guarded by the first expression), we'll
                    // fire off the test by sending a RegisterOrder command to the Sales endpoint
                .When(c => c.ShippingIsSubscribed, bus => bus.Send<RegisterOrder>(m =>
                    {
                        m.Amount = 500;
                        m.CustomerName = &quot;John&quot;;
                        m.OrderId = 1;
                    }))
             )
            // No special actions for this endpoint, it just has to do its work
            .WithEndpoint<Shipping>() 
            // The test succeeds when the order is accepted by the Sales endpoint,
            // and subsequently the order is shipped by the Shipping endpoint
            .Done(context => context.OrderIsAccepted && context.OrderIsShipped && !context.OrderIsRefused)
            .Run();
    }
    

    Whoa, that&#8217;s a lot of fluent API shizzle! That&#8217;s just one statement with a bunch of lambda&#8217;s, mind you. Let&#8217;s break it down to see what we have here&#8230;

    The AcceptanceTesting harness runs a scenario, as denoted by the Scenario class. The basic skeleton looks like this:

    [TestMethod]
    public void Order_of_500_should_be_accepted_and_shipped()
    {
        Scenario.Define(() => new Context { })
    
            .WithEndpoint<Sales>()
    
            .WithEndpoint<Shipping>() 
    
            .Done(context => context.OrderIsAccepted && context.OrderIsShipped && !context.OrderIsRefused)
    
            .Run();
    }
    

    A scenario is defined using the Define method, which receives an instance of a class named Context. Next, the WithEndpoint() generic methods help us setup the different endpoints that participate in the current test scenario. In this case: Sales and Shipping. We&#8217;ll have a look at the types used here later.

    Before the scenario is kicked off with the Run() method, we define a condition that indicates when the test has succeeded and pass that to the Done() method.

    The expression looks like this:

    context.OrderIsAccepted && context.OrderIsShipped && !context.OrderIsRefused
    

    We&#8217;re evaluating a bunch of properties on an object named context. This is actually the instance of the Context class we saw being passed to the Scenario.Define() method. The context class looks like this:

    class Context : ScenarioContext
    {
      public bool OrderIsAccepted { get; set; }
      public bool OrderIsRefused { get; set; }
      public bool OrderIsShipped { get; set; }
      public bool ShippingIsSubscribed { get; set; }
    }
    

    It inherits from ScenarioContext, a base class in the NServiceBus.AcceptanceTesting framework, and it&#8217;s just a bunch of properties that get passed around throughout our test scenarios to keep track of the progress. The trick is to set these properties at specific moments as your test runs and as soon as the conditions are met, the test is considered a success.

    In the example above, we expect that the order is accepted and shipped, and we also double check that it wasn&#8217;t refused. We can assert this by tracking the events being published.

    The next piece of the puzzle is the definition of the endpoints that participate in the test:

    .WithEndpoint<Sales>()
    

    The type parameter in this case is a class called Sales. This class represents the Sales endpoint, but is actually defined in the test code. This is what it looks like:

    public class Sales : EndpointConfigurationBuilder
    {
      public Sales()
      {
        EndpointSetup<DefaultServer>()
        // Makes sure that the RegisterOrder command is mapped to the Sales endpoint
          .AddMapping<RegisterOrder>(typeof(Sales));
      }
    
      class SalesInspector : IMutateOutgoingMessages, INeedInitialization
      {
        // Will be injected via DI
        public Context TestContext { get; set; }
    
        public object MutateOutgoing(object message)
        {
          if (message is OrderAccepted)
          {
            TestContext.OrderIsAccepted = true;
          }
    
          if (message is OrderRefused)
          {
            TestContext.OrderIsRefused = true;
          }
    
          return message;
        }
    
        public void Customize(BusConfiguration configuration)
        {
           configuration.RegisterComponents(c => c.ConfigureComponent<SalesInspector>(DependencyLifecycle.InstancePerCall));
        }
      }
    }
    

    The Sales class derives from EndpointConfigurationBuilder, and is our bootstrap for this particular endpoint. The class itself doesn&#8217;t do much, except bootstrapping the endpoint by specifying an endpoint setup template &#8211; a class named DefaultServer &#8211; and making sure that the RegisterOrder message is mapped to its endpoint.

    We also see a nested class called SalesInspector, which is an NServiceBus MessageMutator. We are using the extensibility of NServiceBus to plug in hooks that help us track the progress of the test. In this case, the mutator listens for outgoing messages &#8211; which would be OrderAccepted or OrderRefused for the Sales endpoint &#8211; and sets the flags on the scenario context accordingly.

    This is all wired up through the magic of type scanning and the use of the INeedInitialization interface. This happens through the endpoint setup template class: DefaultServer. I actually borrowed most of this code from the original NServiceBus code base, but stripped it down to just use the default stuff:

    /// <summary>
    /// Serves as a template for the NServiceBus configuration of an endpoint.
    /// You can do all sorts of fancy stuff here, such as support multiple transports, etc.
    /// Here, I stripped it down to support just the defaults (MSMQ transport).
    /// </summary>
    public class DefaultServer : IEndpointSetupTemplate
    {
      public BusConfiguration GetConfiguration(RunDescriptor runDescriptor, 
                                    EndpointConfiguration endpointConfiguration,
                                    IConfigurationSource configSource, 
                                    Action<BusConfiguration> configurationBuilderCustomization)
      {
        var settings = runDescriptor.Settings;
    
        var types = GetTypesToUse(endpointConfiguration);
    
        var config = new BusConfiguration();
        config.EndpointName(endpointConfiguration.EndpointName);
        config.TypesToScan(types);
        config.CustomConfigurationSource(configSource);
        config.UsePersistence<InMemoryPersistence>();
        config.PurgeOnStartup(true);
    
        // Plugin a behavior that listens for subscription messages
        config.Pipeline.Register<SubscriptionBehavior.Registration>();
        config.RegisterComponents(c => c.ConfigureComponent<SubscriptionBehavior>(DependencyLifecycle.InstancePerCall));
    
        // Important: you need to make sure that the correct ScenarioContext class is available to your endpoints and tests
        config.RegisterComponents(r =>
        {
          r.RegisterSingleton(runDescriptor.ScenarioContext.GetType(), runDescriptor.ScenarioContext);
          r.RegisterSingleton(typeof(ScenarioContext), runDescriptor.ScenarioContext);
        });
    
        // Call extra custom action if provided
        if (configurationBuilderCustomization != null)
        {
          configurationBuilderCustomization(config);
        }
    
        return config;
      }
    
      static IEnumerable<Type> GetTypesToUse(EndpointConfiguration endpointConfiguration)
      {
        // Implementation details can be found on GitHub
      }
    }
    

    Most of this code will look familiar: it uses the BusConfiguration options to define the endpoint. In this case, the type scanner will look through all referenced assemblies to find handlers and other NServiceBus stuff that may participate in the tests.

    Most notable is the use of the SubscriptionBehavior class, which is plugged into the NServiceBus pipeline that comes with NServiceBus 5.0 &#8211; watch the NServiceBus Lego Style talk by John and Indu at NSBCon London for more info. This behavior simply listens for subscription messages from endpoints and raises events that you can hook into. This is necessary for our tests to run successfully because the test can only start once all endpoints are running and subscribed to the correct events. The behavior class is not part of the NServiceBus.AcceptanceTesting framework though. IMO, it would be handy if Particular moved this one to the AcceptanceTesting framework as I think you&#8217;ll be needing this one a lot. Again, I borrowed the implementation from the NServiceBus code base:

    class SubscriptionBehavior : IBehavior<IncomingContext>
    {
      public void Invoke(IncomingContext context, Action next)
      {
        next();
        var subscriptionMessageType = GetSubscriptionMessageTypeFrom(context.PhysicalMessage);
        if (EndpointSubscribed != null && subscriptionMessageType != null)
        {
          EndpointSubscribed(new SubscriptionEventArgs
          {
            MessageType = subscriptionMessageType,
            SubscriberReturnAddress = context.PhysicalMessage.ReplyToAddress
          });
        }
      }
    
      static string GetSubscriptionMessageTypeFrom(TransportMessage msg)
      {
        return (from header in msg.Headers where header.Key == Headers.SubscriptionMessageType select header.Value).FirstOrDefault();
      }
    
      public static Action<SubscriptionEventArgs> EndpointSubscribed;
    
      public static void OnEndpointSubscribed(Action<SubscriptionEventArgs> action)
      {
        EndpointSubscribed = action;
      }
    
      internal class Registration : RegisterStep
      {
        public Registration()
          : base(&quot;SubscriptionBehavior&quot;, typeof(SubscriptionBehavior), &quot;So we can get subscription events&quot;)
        {
          InsertBefore(WellKnownStep.CreateChildContainer);
        }
      }
    }
    

    Okay, almost done. We have our endpoint templates set up, message mutators listening to the relevant outgoing messages and SubscriptionBehavior to make sure the test is ready to run. Let&#8217;s get back to the part that actually makes the whole scenario go:

        Scenario.Define(() => new Context { })
            .WithEndpoint<Sales>(b => 
                b.Given((bus, context) =>
                    // The SubscriptionBehavior will monitor for incoming subscription messages
                    // Here we want to track if Shipping is subscribing to our the OrderAccepted event
                    SubscriptionBehavior.OnEndpointSubscribed(s => 
                    {
                        if (s.SubscriberReturnAddress.Queue.Contains(&quot;Shipping&quot;))
                        {
                            context.ShippingIsSubscribed = true;
                        }
                    }))
                    // As soon as ShippingIsSubscribed (guarded by the first expression), we'll
                    // fire off the test by sending a RegisterOrder command to the Sales endpoint
                .When(context => context.ShippingIsSubscribed, bus => bus.Send<RegisterOrder>(m =>
                    {
                        m.Amount = 500;
                        m.CustomerName = &quot;John&quot;;
                        m.OrderId = 1;
                    }))
             )
       ...
    

    For the Sales endpoint, we specified a whole bunch of extra stuff. First, there&#8217;s the event handler for the SubscriptionBehavior.OnEndpointSubscribed event. Here, the Sales endpoint basically waits for the Shipping endpoint to subscribe to the events. The context is available here as well, part of the lambda that&#8217;s passed to the Given() method, so we can flag the subscription by setting a boolean.

    The final piece is the guard passed to the When() method. This is monitored by the AcceptanceTesting framework as the test runs and as soon as the specified condition is met, we can use the bus instance available there to send a message to the Sales endpoint: the RegisterOrder command will trigger the whole process we&#8217;re testing here. We&#8217;re sending an order of $500, which we expect to be accepted and shipped. There&#8217;s a test that checks the refusal of an order > 500 in the sample as well.

    Some tips
    For your end-to-end tests, you will be pulling together DLL&#8217;s from all of your endpoints and with all of your message definitions. So it makes sense to setup a separate solution or project structure for these tests instead of adding it to an existing solution.

    If your handlers are in the same DLL as your EndpointConfig class, the assembly scanner will run into trouble, because it will find multiple classes that implement IConfigureThisEndpoint. While you can intervene in how the assembly scanner does its work (e.g. manually filtering out specific DLL&#8217;s per endpint definition), it might be better to keep your handlers in separate assemblies to make acceptance testing easier.

    As you see, you need to add some infrastructural stuff to your tests, such as the EndpointConfigurationBuilder classes and the IEndpointSetupTemplate class for everything to work properly. You can implement this infrastructure stuff per test or per test suite, but you might want to consider creating some more generic implementations that you can reuse across different test suites. IMO the DefaultServer implementation from the NServiceBus tests is also a nice candidate for becoming a part of the NServiceBus.AcceptanceTesting package to simplify your test code as this is already a very flexible implementation.

    keep-calm-you-passed-the-test

    Conclusion
    As you see, the NServiceBus.AcceptanceTesting framework takes some time to get to know properly. There&#8217;s more advanced stuff in there which I didn&#8217;t cover in this introduction. Now if you dive into the scenarios implemented by Particular themselves, you&#8217;ll find inspiration for testing other bits of your system.

    I like the model a lot, and I think this can save a lot of time retesting many end-to-end scenarios.

    Share this

View profile

IT Training at Info Support

Which training fits you?

Consultancy

Consultancy

Related blogs

  • Video Conferencing en OBS Studio koppelen: online prese…

    Video Conferencing en OBS Studio koppelen: online prese… Maaike Brouwer - 1 year ago

  • Verantwoordelijkheid pakken in jouw eigen persoonlijke …

    Verantwoordelijkheid pakken in jouw eigen persoonlijke … Stephan Versteegh - 1 year ago

  • Tips voor als je gaat afstuderen

    Tips voor als je gaat afstuderen Bart Renders - 2 years ago

Related downloads

  • Beslisboom voor een rechtmatig ‘kopietje productie’

  • Klantreferentie: Remmicom zet wetgeving om in intellige…

  • Klantreferentie RDW: Samenwerken voor veilig en vertrou…

  • Klantreferentie BeFrank: Strategische IT voor een innov…

  • Wie durft te experimenteren met data in de zorg?

Related videos

  • mijnverzekeringenopeenrij.nl

    mijnverzekeringenopeenrij.nl

  • Winnaar | Innovation Projects 2017

    Winnaar | Innovation Projects 2017

  • Explore | Info Support & HAN & Poliskluis

    Explore | Info Support & HAN & Poliskluis

  • LifeApps bij HagaZiekenhuis

    LifeApps bij HagaZiekenhuis

  • Info Support | Bedrijfsfilm

    Info Support | Bedrijfsfilm

Nieuwsbrief

* verplichte velden

Contact

  • Head office NL
  • Kruisboog 42
  • 3905 TG Veenendaal
  • T +31 318 552020
  • Call
  • Mail
  • Directions
  • Head office BE
  • Generaal De Wittelaan 17
  • bus 30 2800 Mechelen
  • T +32 15 286370
  • Call
  • Mail
  • Directions

Follow us

  • Twitter
  • Facebook
  • Linkedin
  • Youtube

Newsletter

Sign in

Extra

  • Media Library
  • Disclaimer
  • Algemene voorwaarden
  • ISHBS Webmail
  • Extranet
Beheer cookie toestemming
Deze website maakt gebruik van Functionele en Analytische cookies voor website optimalisatie en statistieken.
Functioneel
Altijd actief
De technische opslag of toegang is strikt noodzakelijk voor het legitieme doel het gebruik mogelijk te maken van een specifieke dienst waarom de abonnee of gebruiker uitdrukkelijk heeft gevraagd, of met als enig doel de uitvoering van de transmissie van een communicatie over een elektronisch communicatienetwerk.
Voorkeuren
De technische opslag of toegang is noodzakelijk voor het legitieme doel voorkeuren op te slaan die niet door de abonnee of gebruiker zijn aangevraagd.
Statistieken
De technische opslag of toegang die uitsluitend voor statistische doeleinden wordt gebruikt. De technische opslag of toegang die uitsluitend wordt gebruikt voor anonieme statistische doeleinden. Zonder dagvaarding, vrijwillige naleving door uw Internet Service Provider, of aanvullende gegevens van een derde partij, kan informatie die alleen voor dit doel wordt opgeslagen of opgehaald gewoonlijk niet worden gebruikt om je te identificeren.
Marketing
De technische opslag of toegang is nodig om gebruikersprofielen op te stellen voor het verzenden van reclame, of om de gebruiker op een website of over verschillende websites te volgen voor soortgelijke marketingdoeleinden.
Beheer opties Beheer diensten Beheer leveranciers Lees meer over deze doeleinden
Voorkeuren
{title} {title} {title}