Sagas and Unit Testing - Business Process Verification Made Easy
Monday, February 4th, 2008.Sagas have always been designed with unit testing in mind. By keeping them disconnected from any communications or persistence technology, it was my belief that it should be fairly easy to use mock objects to test them. I’ve heard back from projects using nServiceBus this way that they were pleased with their ability to test them, and thought all was well.
Not so.
The other day I sat down to implement and test a non-trivial business process, and the testing was far from easy. Now as developers go, I’m not great, or an expert on unit testing or TDD, but I’m above average. It should not have been this hard. And I tried doing it with Rhino.Mocks, TypeMock, and finally Moq. It seemed like I was in a no-mans-land, between trying to do state-based testing, and setting expectations on the messages being sent (as well as correct values in those messages), nothing flowed.
Until I finally stopped trying to figure out how to test, and focused on what needed to be tested. I mean, it’s not like I was trying to build a generic mocking framework like Daniel.
Here’s an example business process, or actually, part of one, and then we’ll see how that can be tested. By the way, there will be a post coming soon which describes how we go about analysing a system, coming up with these message types, and how these sagas come into being, so stay tuned. Either that, or just come to my tutorial at QCon.
On with the process:
1. When we receive a CreateOrderMessage, whose “Completed” flag is true, we’ll send 2 AuthorizationRequestMessages to internal systems (for managers to authorize the order), one OrderStatusUpdatedMessage to the caller with a status “Received”, and a TimeoutMessage to the TimeoutManager requesting to be notified – so that the process doesn’t get stuck if one or both messages don’t get a response.
2. When we receive the first AuthorizationResponseMessage, we notify the initiator of the Order by sending them a OrderStatusUpdatedMessage with a status “Authorized1”.
3. When we get “timed out” from the TimeoutManager, we check if at least one AuthorizationResponseMessage has arrived, and if so, publish an OrderAcceptedMessage, and notify the initator (again via the OrderStatusUpdatedMessage) this time with a status of “Accepted”.
And here’s the test:
public class OrderSagaTests { private OrderSaga orderSaga = null; private string timeoutAddress; private Saga Saga; [SetUp] public void Setup() { timeoutAddress = “timeout”; Saga = Saga.Test(out orderSaga, timeoutAddress); } [Test] public void OrderProcessingShouldCompleteAfterOneAuthorizationAndOneTimeout() { Guid externalOrderId = Guid.NewGuid(); Guid customerId = Guid.NewGuid(); string clientAddress = “client”; CreateOrderMessage createOrderMsg = new CreateOrderMessage(); createOrderMsg.OrderId = externalOrderId; createOrderMsg.CustomerId = customerId; createOrderMsg.Products = new List<Guid>(new Guid[] { Guid.NewGuid() }); createOrderMsg.Amounts = new List<float>(new float[] { 10.0F }); createOrderMsg.Completed = true; TimeoutMessage timeoutMessage = null; Saga.WhenReceivesMessageFrom(clientAddress) .ExpectSend<AuthorizeOrderRequestMessage>( delegate(AuthorizeOrderRequestMessage m) { return m.SagaId == orderSaga.Id; }) .ExpectSend<AuthorizeOrderRequestMessage>( delegate(AuthorizeOrderRequestMessage m) { return m.SagaId == orderSaga.Id; }) .ExpectSendToDestination<OrderStatusUpdatedMessage>( delegate(string destination, OrderStatusUpdatedMessage m) { return m.OrderId == externalOrderId && destination == clientAddress; }) .ExpectSendToDestination<TimeoutMessage>( delegate(string destination, TimeoutMessage m) { timeoutMessage = m; return m.SagaId == orderSaga.Id && destination == timeoutAddress; }) .When(delegate { orderSaga.Handle(createOrderMsg); }); Assert.IsFalse(orderSaga.Completed); AuthorizeOrderResponseMessage response = new AuthorizeOrderResponseMessage(); response.ManagerId = Guid.NewGuid(); response.Authorized = true; response.SagaId = orderSaga.Id; Saga.ExpectSendToDestination<OrderStatusUpdatedMessage>( delegate(string destination, OrderStatusUpdatedMessage m) { return (destination == clientAddress && m.OrderId == externalOrderId && m.Status == OrderStatus.Authorized1); }) .When(delegate { orderSaga.Handle(response); }); Assert.IsFalse(orderSaga.Completed); Saga.ExpectSendToDestination<OrderStatusUpdatedMessage>( delegate(string destination, OrderStatusUpdatedMessage m) { return (destination == clientAddress && m.OrderId == externalOrderId && m.Status == OrderStatus.Accepted); }) .ExpectPublish<OrderAcceptedMessage>( delegate(OrderAcceptedMessage m) { return (m.CustomerId == customerId); }) .When(delegate { orderSaga.Timeout(timeoutMessage.State); }); Assert.IsTrue(orderSaga.Completed); } }
You might notice that this style is a bit similar to the fluent testing found in Rhino Mocks. That’s not coincidence. It actually makes use of Rhino Mocks internally. The thing that I discovered was that in order to test these sagas, you don’t need to actually see a mocking framework. All you should have to do is express how messages get sent, and under what criteria those messages are valid.
If you’re wondering what the OrderSaga looks like, you can find the code right here. It’s not a complete business process implementation, but its enough to understand how one would look like:
using System; using System.Collections.Generic; using ExternalOrderMessages; using NServiceBus.Saga; using NServiceBus; using InternalOrderMessages; namespace ProcessingLogic { [Serializable] public class OrderSaga : ISaga<CreateOrderMessage>, ISaga<AuthorizeOrderResponseMessage>, ISaga<CancelOrderMessage> { #region config info [NonSerialized] private IBus bus; public IBus Bus { set { this.bus = value; } } [NonSerialized] private Reminder reminder; public Reminder Reminder { set { this.reminder = value; } } #endregion private Guid id; private bool completed; public string clientAddress; public Guid externalOrderId; public int numberOfPendingAuthorizations = 2; public List<CreateOrderMessage> orderItems = new List<CreateOrderMessage>(); public void Handle(CreateOrderMessage message) { this.clientAddress = this.bus.SourceOfMessageBeingHandled; this.externalOrderId = message.OrderId; this.orderItems.Add(message); if (message.Completed) { for (int i = 0; i < this.numberOfPendingAuthorizations; i++) { AuthorizeOrderRequestMessage req = new AuthorizeOrderRequestMessage(); req.SagaId = this.id; req.OrderData = orderItems; this.bus.Send(req); } } this.SendUpdate(OrderStatus.Recieved); this.reminder.ExpireIn(message.ProvideBy - DateTime.Now, this, null); } public void Timeout(object state) { if (this.numberOfPendingAuthorizations <= 1) this.Complete(); } public Guid Id { get { return id; } set { id = value; } } public bool Completed { get { return completed; } } public void Handle(AuthorizeOrderResponseMessage message) { if (message.Authorized) { this.numberOfPendingAuthorizations–; if (this.numberOfPendingAuthorizations == 1) this.SendUpdate(OrderStatus.Authorized1); else { this.SendUpdate(OrderStatus.Authorized2); this.Complete(); } } else { this.SendUpdate(OrderStatus.Rejected); this.Complete(); } } public void Handle(CancelOrderMessage message) { } private void SendUpdate(OrderStatus status) { OrderStatusUpdatedMessage update = new OrderStatusUpdatedMessage(); update.OrderId = this.externalOrderId; update.Status = status; this.bus.Send(this.clientAddress, update); } private void Complete() { this.completed = true; this.SendUpdate(OrderStatus.Accepted); OrderAcceptedMessage accepted = new OrderAcceptedMessage(); accepted.Products = new List<Guid>(this.orderItems.Count); accepted.Amounts = new List<float>(this.orderItems.Count); this.orderItems.ForEach(delegate(CreateOrderMessage m) { accepted.Products.AddRange(m.Products); accepted.Amounts.AddRange(m.Amounts); accepted.CustomerId = m.CustomerId; }); this.bus.Publish(accepted); } } }
All this code is online in the subversion repository under /Samples/Saga.
Questions, comments, and general thoughts are always appreciated.
|
If you liked this article, you might also like articles in these categories:
If you've got a minute, you might enjoy taking a look at some of my best articles.I've gone through the hundreds of articles I've written over the past 4 years and put together a list of the best ones as ranked by my 2000+ readers. You won't be disappointed. If you'd like to get new articles sent to you when they're published, it's easy and free.Subscribe right here. Something on your mind? Got a question? I'd be thrilled to hear it. Leave a comment below or email me, whatever works for you. 2 CommentsYour comment... |







February 11th, 2008 at 2:30 am
[…] Testing environment for sagas (example available) […]
September 30th, 2008 at 3:03 pm
[…] Related Posts Business Process Verification […]