I’ve been speaking about how sagas are designed to be
testable for a while now,
and several projects that use nServiceBus have been doing
it,
but I’ve never actually sat down and did one myself –
until this past week.
It was a pain in the ass.
And as developers go, I’m fairly proficient with the
tools out there.
It’s as if the mocking tools weren’t designed
with this sort of thing in mind.
So, this past weekend, I decided to do something about it.
And now you can find NServiceBus.Testing (implemented under
/src/core).
When built, the libraries are in /build/testing.
You’ll notice that Rhino.Mocks is there too.
You hardly need to know rhino mocks to test sagas – it
isn’t exposed in the API,
although the API is similar in style to the rhino callback
and Expect.Call semantics.
When your tests fail, though, you will see rhino mocks error
messages –
which aren’t designed for these scenarios, but
are pretty good once you get used to them.
Here’s an example business process (well, part of one),
which can be found under /Samples/Saga:
1.
When we receive a
CreateOrderMessage, whose “Completed” flag is true, we’ll
send 2 AuthorizationRequestMessages to internal systems, 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 an
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
had arrived, and if so, publish an OrderAcceptedMessage, and notify the
initator (again via the OrderStatusUpdatedMessage) this time with a status of “Accepted”.
This is tested as follows (suggestions welcome), the class “Saga”
is that which ties together the expectations into the test scenario.
The delegates you see in the “ExpectXXX” calls
are actually predicates, and are there to report on if the appropriate message
(with the right data) was sent.
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);
}
}
Thoughts? Questions? Comments?
--
Udi Dahan
- The Software Simplist
.NET
Development Expert & SOA Specialist
No virus found in this outgoing message.
Checked by AVG Free Edition.
Version: 7.5.516 / Virus Database: 269.19.18/1255 - Release Date: 01/02/2008 09:59