Wednesday, February 20, 2008

BDD and Outside-In Design

I've started using NBehave specifications to try and work out how BDD can affect the way I work.

As I touched on in my previous post. I'm hoping to try and find answers to my questions and the first one I wanted to deal with is how to best use BDD as a way of getting outside-in design.

I'm going to give an example of two approaches to using outside-in design with BDD, first though I'll explain what I'm testing:

SUT
I'm writing a Windows Service that will do the following (initially):

  1. Pick up unprocessed messages describing events related to domain classes.
  2. Go through each message in turn.
  3. Take the message and convert it to an XML document
  4. Send the XML message to our integration platform (BizTalk).
  5. Mark the original message as processed.
  6. Look for steps 3-5 until done.
This is a very much a technical piece of work but I don't see why BDD can't be used for such a task.

Story Definition
I'll define this overall story in NBehave syntax as:

story
.AsA("...")
.IWant("all domain event messages to be processed")
.SoThat("we send them to the integration platform");
Simple Test
To get going I want to start with a single simple test:

  1. Load the single unprocessed domain event message
  2. Create the XmlDocument describing the event and domain object.
  3. Send the XmlDocument.
Mock Style
The first way I think you can use BDD for high-level testing is something like the approach from Mock Roles, Not Objects:


[Test]
public void We_process_change_X_to_Y()
{
DomainEventMessage message = CreateMessageForAccountActivation();
_testMessages.Add(message);

#region Setup Expectations

XmlDocument emptyDocument = new XmlDocument();

Mock mockMapper = MockManager.Mock(typeof(AccountEventMessageMapper));
mockMapper.ExpectAndReturn("ConvertToXmlMessage", emptyDocument).Args(message);

Mock mockMapperFactory = MockManager.Mock(typeof(DomainEventMessageMapperFactory));
mockMapperFactory.ExpectAndReturn("GetMapper", new AccountEventMessageMapper()).Args(message);

Mock mockMessageSender = MockManager.Mock(typeof(XmlMessageSender));
mockMessageSender.ExpectCall("Send").Args(emptyDocument);

#endregion

story
.WithScenario("processing single event message")
.Given("there is a single unprocessed message relating to an X", MockRepositoryToReturnMessages)
.When("we process the event messages", ProcessMessages)
.Then("an appropriate Xml message is sent", VerifyExpectedCallsWereMade);
}
Its worth noting here that I'm using TypeMock (and only features from the free community edition) and not injecting in the dependencies so I'm not strictly sticking to the approach that the mockobjects guys push for. Having said that I am thinking that this is certainly a situation where I will be bringing in DI/IoC.

So what do I think of it? I guess this test is useful, it definitely made me think about the collaborators. To get the test to pass I have to put the followinge code into the SUT:

IList unprocessedMessages = new DomainEventMessageRepository().GetAll();

AccountEventMessageMapper mapper = DomainEventMessageMapperFactory.GetMapper(unprocessedMessages[0]);

XmlDocument xmlDocument = mapper.ConvertToXmlMessage(unprocessedMessages[0]);

new XmlMessageSender().Send(xmlDocument);
This seems like a good initial design (ignoring names of classes/members and possibility of DI). Having said that I'm not convinced that I wouldn't have come up with a design as good or better if I'd just relied on normal state based testing. Mind you at least with this approach I have come up with a list of requried collaborators early which is useful.

Now down to a problem though, although this test now passes my SUT does nothing because the collaborators all have do nothing implementations. This means that we're out of luck if we're expecting this specification to act as our acceptance criteria.

State Testing
Lets look at what I might do for a state based test:

[Test]
public void We_process_change_X_to_Y()
{
story
.WithScenario("processing single event message")
.Given("there is a single unprocessed message relating to an X", CreateAndSaveMessage)
.When("we process the event messages", ProcessMessages)
.Then("an appropriate Xml message is sent", AssertCorrectXmlDocumentProduced);
}
In this case the methods being called would probably be real implementations or at worst would use test stubs/spies. So I'd be thinking that CreateAndSaveMessage would create a real DomainEventMessage and would save it to the database.

So how do I get this test to pass, the truth is it will take me a while. I'll need to implement all the collaborators fully enough to handle this simple case (single message being processed) and I need to define the expected XML document before starting the work.

The good bit is that once I'll know that the system is handling the one simple case, which makes it a useful acceptance test.

Summary
I actually think that for the problem at hand both tests are good.

I don't rate the way most people use mocks, it often just ends up in unreadable over-coupled tests that are difficult to follow. However in this case we're showing high level collaborations that are indeed meaningful. I question the value of always thinking about all of the collaborators upfront but in this case it could be useful.

How do I apply both types of tests though, the options I'm going to try are:

  1. Write Both - Since I'm not sure I need that many tests that show the collaborations this could work.
  2. Write Mock Ones And As I Implement Remove The Mocks - I'd write a mocking test and as I write the real collaborators I'll go back and replace the mock implementation with the real one. Once there is no mocking in the test I'm done.
  3. Avoid Mocking - This is the approach discussed in this NBehave thread.

Initially I might go for the first approach, writing a few mock/behavior style tests but mainly writing state ones.

Share This - Digg It Save to del.icio.us Stumble It! Kick It DZone

1 comment:

  1. Hi Colin,

    Just a quick note - when I'm using NBehave's .AsA method at a lower level then I'm generally using the name of the class (or classes) that are consumers of the functionality I'm testing.

    Not sure if this is correct - but it works for me - makes me think about it from consumers viewpoint.

    Cheers,
    Gordon

    ReplyDelete