Ok, so now we have taken a look at how we could unit test a simple Rebus message handler, and that looked pretty simple. When unit testing sagas, however, there’s a bit more to it because of the correlation “magic” going on behind the scenes. Therefore, to help unit testing sagas, Rebus comes with a SagaFixture<TSagaData> which you can use to wrap your saga handler while testing.
I guess an example is in order 🙂
Consider this simple saga that is meant to be put in front of a call to an external web service where the actual web service call is being made somewhere else in a service that allows for using simple request/reply with any correlation ID:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
public class MakeThatCallSaga : Saga<MakeThatCallSagaData>, IAmInitiatedBy<MakeThatCall>, IHandleMessages<SomeReply>, IHandleMessages<TakeAlternativeAction> { public IBus Bus { get; set; } public override void ConfigureHowToFindSaga() { Incoming<MakeThatCall>(m => m.CallId).CorrelatedWith(d => d.CallId); Incoming<SomeReply>(m => m.CorrelationId).CorrelatedWith(d => d.CallId); Incoming<TakeAlternativeAction>(m => m.CorrelationId).CorrelatedWith(d => d.CallId); } public void Handle(MakeThatCall m) { if (!IsNew) return; Data.CallId = m.CallId; // make call to web service facade Bus.Send(new SomeRequest { CorrelationId = Data.CallId }); // order a wake up call to allow us to take alternative action if web service call // could not succeed within timeout of 1 minute Bus.Defer(TimeSpan.FromMinutes(1), new TakeAlternativeAction { CorrelationId = Data.CallId }); } public void Handle(TakeAlternativeAction m) { Bus.Send(new NotifyTechnicalAdministrator { Message = "Call with ID " + Data.CallId + " did not make it!" }); } } |
Let’s say I want to punish this saga in a unit test. A way to do this is to attack the problem like when unit testing an ordinary message handler – but then a good portion of the logic under test is not tested, namely all of the correlation logic going on behind the scenes. Which is why Rebus has the SagaFixture<TSagaData> thing… check this out:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
[TestFixture] public class TestMakeThatCallSaga { [Test] public void IsIdempotentWithRespectToCallId() { // arrange var bus = new FakeBus(); // create the saga handler, but don't touch it directly! var saga new MakeThatCallSaga { Bus = bus }; // just "host" the saga inside the saga fixture var fixture = new SagaFixture<MakeThatCallSagaData>(saga); // arrange that one call has already been made fixture.Handle(new MakeThatCall { CallId = "wut?" }); // act // make an additional call with the same call ID fixture.Handle(new MakeThatCall { CallId = "wut?" }); // assert // exactly one saga data instance shoule have been created Assert.That(fixture.AvailableSagaData.Count, Is.EqualTo(1)); // only one call to the external web service should have been made var sentRequests = fakeBus.SentMessages.OfType<SomeRequest>().ToList(); Assert.That(sentRequests.Count, Is.EqualTo(1);) Assert.That(sentRequests[0].CorrelationId, Is.EqualTo("wut?")); } } |
As you can see, the saga fixture can be used to “host” your saga handler during testing, and it will take care of properly dispatching messages to it, respecting the saga’s correlation setup. Therefore, after handing over the saga to the fixture, you should not touch the saga handler again in that test.
If you want to start out in your test with some premade home-baked saga data, you can pass it to the saga fixture as a second constructor argument like so:
1 2 |
var data = new List<MakeThatCallSagaData>(YieldSomeSagaData()); var fixture = new SagaFixture<MakeThatCallSagaData>(new MakeThatCallSaga(...), data); |
and as you can see, you can access all “live” saga data via fixture.AvailableSagaData, and you can access “dead” saga data as well via fixture.DeletedSagaData.
If you’re interested in logging stuff, failing in case of uncorrelated messages, etc., there’s a couple of events on the saga fixture that you can use: CorrelatedWithExistingSagaData, CreatedNewSagaData, CouldNotCorrelate – so in order to fail in all cases where a message would have been ignored because it could not be correlated, and the message is not allowed to initiate a new saga data instance, you could do something like this:
1 2 |
var fixture = new SagaFixture<MakeThatCallSagaData>(new MakeThatCallSaga(...)); fixture.CouldNotCorrelate += m => Assert.Fail("Oh noes!!1 {0} was ignored!", m); |
Nifty, huh?