One of the really nice things about Rhino Service Bus applications is that we have created a structured way to handle inputs and outputs. You have messages coming in and out, as well as the endpoint local state to deal with. You don’t have to worry about how to deal with external integration points, because those are already going over messages.
And when you have basic input/output figured out, you are pretty much done.
For example, let us see the code that handles extending trail licenses in our ordering system:
public class ExtendTrialLicenseConsumer : ConsumerOf<ExtendTrialLicense> { public IDocumentSession Session { get; set; } public IServiceBus Bus { get; set; } public void Consume(ExtendTrialLicense message) { var productId = message.ProductId ?? "products/" + message.Profile; var trial = Session.Query<Trial>() .Where(x => x.Email == message.Email && x.ProductId == productId) .FirstOrDefault(); if (trial == null) return; trial.EndsAt = DateTime.Today.AddDays(message.Days); Bus.Send(new NewTrial { ProductId = productId, Email = trial.Email, Company = trial.Company, FullName = trial.Name, TrackingId = trial.TrackingId }); } }
How do we test something like this? As it turns out, quite easily:
public class TrailTesting : ConsumersTests { protected override void PrepareData(IDocumentSession session) { session.Store(new Trial { Email = "you@there.gov", EndsAt = DateTime.Today, ProductId = "products/nhprof" }); } [Fact] public void Will_update_trial_date() { Consume<ExtendTrialLicenseConsumer, ExtendTrialLicense>(new ExtendTrialLicense { ProductId = "products/nhprof", Days = 30, Email = "you@there.gov", }); using (var session = documentStore.OpenSession()) { var trial = session.Load<Trial>(1); Assert.Equal(DateTime.Today.AddDays(30), trial.EndsAt); } } // more tests here }
All the magic happens in the base class, though:
public abstract class ConsumersTests : IDisposable { protected IDocumentStore documentStore; private IServiceBus Bus = new FakeBus(); protected ConsumersTests() { documentStore = new EmbeddableDocumentStore { RunInMemory = true, Conventions = { DefaultQueryingConsistency = ConsistencyOptions.QueryYourWrites } }.Initialize(); IndexCreation.CreateIndexes(typeof(Products_Stats).Assembly, documentStore); Products.Create(documentStore); using (var session = documentStore.OpenSession()) { PrepareData(session); session.SaveChanges(); } } protected T ConsumeSentMessage<T>() { var fakeBus = ((FakeBus)Bus); object o = fakeBus.Messages.Where(x => x.GetType() == typeof(T)).First(); fakeBus.Messages.Remove(o); return (T) o; } protected void Consume<TConsumer, TMsg>(TMsg msg) where TConsumer : ConsumerOf<TMsg>, new() { var foo = new TConsumer(); using (var documentSession = documentStore.OpenSession()) { Set(foo, documentSession); Set(foo, Bus); Set(foo, documentStore); foo.Consume(msg); documentSession.SaveChanges(); } } private void Set<T,TValue>(T foo, TValue value) { PropertyInfo firstOrDefault = typeof(T).GetProperties().FirstOrDefault(x=>x.PropertyType==typeof(TValue)); if (firstOrDefault == null) return; firstOrDefault.SetValue(foo, value, null); } protected virtual void PrepareData(IDocumentSession session) { } public void Dispose() { documentStore.Dispose(); } }
And here are the relevant details for the FakeBus implementation:
public class FakeBus : IServiceBus { public List<object> Messages = new List<object>(); public void Send(params object[] messages) { Messages.AddRange(messages); } }
Now, admittedly, this is a fairly raw approach and we can probably do better. This is basically hand crafted auto mocking for consumers, and I don’t like the Consume<TConsumer,TMsg>() syntax very much. But it works, it is simple and it doesn’t really gets in the way.
I won’t say it is the way to go about it, but it is certainly easier than many other efforts that I have seen. We just need to handle the inputs & outputs and have a way to look at the local state, and you are pretty much done.