NBehave Experiment: MonoRail Controllers & Rhino Mocks

posted @ Wednesday, September 19, 2007 12:16 PM

 

...whoever keeps His word, truly the love of God is perfected in him. By this we know that we are in Him. -- 1 John 2:5

Well I decided it was finally time I took a look at NBehave as another one of those tools to have in my toolbox.  NBehave's primary use seems to be in the domain model for implementing stories from the user's perspective, which obviously sounds very powerful. 

But I specifically wanted to see how NBehave would "behave" (sorry...) for driving out my MonoRail controllers along with mock objects for mocking out the back-end services as I do now.  In other words, making the controller itself, the "person" interacting with the system using controller-specific terminology.

So I thought I would take one of my existing controller tests and convert it to a NBehave-type story/scenario.  Here is my original old-school test:

   1: [Test]
   2: public void Should_load_pending_order_and_shopping_cart_and_list_of_acceptable_credit_card_types()
   3: {
   4:     CheckoutOrderDTO checkoutOrderDTO = new CheckoutOrderDTO();
   5:     ShoppingCartDTO shoppingCartDTO = new ShoppingCartDTO();
   6:     IList<LookupDTO> listOfCreditCards = new List<LookupDTO>();
   7:  
   8:     using (mockery.Record())
   9:     {
  10:         PrepareController(controller, "checkout", "index");
  11:         Expect.Call(mockCheckoutService.GetOrderForCheckout()).Return(checkoutOrderDTO);
  12:         Expect.Call(mockBasketService.GetShoppingCart()).Return(shoppingCartDTO);
  13:         Expect.Call(mockCreditCardLookupService.GetAllCreditCardTypes()).Return(listOfCreditCards);
  14:     }
  15:  
  16:     using (mockery.Playback())
  17:     {
  18:         controller.Index();
  19:         Assert.AreEqual(checkoutOrderDTO, controller.PropertyBag[PB.CheckoutOrder]);
  20:         Assert.AreEqual(shoppingCartDTO, controller.PropertyBag[PB.ShoppingCart]);
  21:         Assert.AreEqual(listOfCreditCards, controller.PropertyBag[PB.AllCreditCardTypes]);
  22:     }
  23: }

 

Since I tend to be very verbose in my test naming, sometimes they can become unwieldy.  Of course every time my test names get too long, I start questioning whether the unit of code I'm testing is trying to do too much, thus violating SRP.  But in this case, I'm simply loading up 3 pieces of data from various services.  (Yes, I realize the service communication here is becoming a bit chatty, and a refactoring task is definitely on the radar, but that's not really the focus for this post).  :)

Anyway, I wanted to see what this test would look like as a NBehave story.  Here goes...

(sorry for the code wrappage)

   1: [Test, Story]
   2: public void Load_checkout_information()
   3: {
   4:     CheckoutOrderDTO checkoutOrderDTO = new CheckoutOrderDTO();
   5:     ShoppingCartDTO shoppingCartDTO = new ShoppingCartDTO();
   6:     IList<LookupDTO> listOfCreditCards = new List<LookupDTO>();
   7:  
   8:     Story story = new Story("Load checkout information");
   9:  
  10:     story
  11:         .AsA("controller that is responsible for performing the checkout process")
  12:         .IWant("to load the checkout information based on the current user's pending order")
  13:         .SoThat("I can edit the customer's information before submitting the order");
  14:  
  15:     story
  16:         .WithScenario("Order is ready to be checked out")
  17:  
  18:             .Given("controller is prepared", delegate { PrepareController(controller, "checkout", "index"); })
  19:                 .And("pending order is retrieved from checkout service",
  20:                     delegate { Expect.Call(mockCheckoutService.GetOrderForCheckout()).Return(checkoutOrderDTO); })
  21:                 .And("shopping cart is retrieved from shopping cart service",
  22:                     delegate { Expect.Call(mockBasketService.GetShoppingCart()).Return(shoppingCartDTO); })
  23:                 .And("list of acceptable credit card types are retrieved from credit card lookup service",
  24:                     delegate { Expect.Call(mockCreditCardLookupService.GetAllCreditCardTypes()).Return(listOfCreditCards); mockery.ReplayAll(); })
  25:  
  26:             .When("the index action is executed", delegate { controller.Index(); })
  27:  
  28:             .Then("the pending order should exist in the property bag with key of", PB.CheckoutOrder,
  29:                     delegate(string key) { Assert.AreEqual(checkoutOrderDTO, controller.PropertyBag[key]); })
  30:                 .And("the shopping cart should exist in the property bag with key of", PB.ShoppingCart,
  31:                     delegate(string key) { Assert.AreEqual(shoppingCartDTO, controller.PropertyBag[key]); })
  32:                 .And("the list of accepted credit card types should exist in the property bag with key of", PB.AllCreditCardTypes,
  33:                     delegate(string key) { Assert.AreEqual(listOfCreditCards, controller.PropertyBag[key]); });
  34: }

 

But the real beauty is the generated output:

 

Theme: Checkout Process

    Story: Load checkout information
    
    Narrative:
        As a controller that is responsible for performing the checkout process
        I want to load the checkout information based on the current user's pending order
        So that I can edit the customer's information before submitting the order
    
        Scenario 1: Order is ready to be checked out
            Given controller is prepared
                And pending order is retrieved from checkout service
                And shopping cart is retrieved from shopping cart service
                And list of acceptable credit card types are retrieved from credit card lookup service
            When the index action is executed
            Then the pending order should exist in the property bag with key of: order
                And the shopping cart should exist in the property bag with key of: shoppingCart
                And the list of accepted credit card types should exist in the property bag with key of: allCreditCardTypes

 

As the title of this post states, this is merely an experiment at this point.  But I do think it better expresses exactly what the controller is doing vs the non-NBehave test above. 

One extension I would like to add to the NBehave framework is some built-in support for mock object frameworks.  I don't necessarily think a specific mock object framework should be integrated into NBehave, but for my own purposes, I'm thinking about adding support for the Given scope to implicitly signal to Rhino Mocks to stop recording.  As you can see, for now I had to include it in my last Given block like this:

.And("list of acceptable credit card types are retrieved from credit card lookup service",
                            delegate { Expect.Call(mockCreditCardLookupService.GetAllCreditCardTypes()).Return(listOfCreditCards); mockery.ReplayAll(); })

 

I suppose I could do something silly like this:

.And("mock object framework has stopped recording", delegate { mockery.ReplayAll(); })

 

But that would litter the nicely generated output with "test" specific terminology which I'm not very cool with.  This extension could probably be achieved using a simple decorator/interceptor, but won't know for sure until I peek at the source.  :)

What do you think?  Is this over-complicating things? Or could it be an elegant way for expressing what is actually going on in the controllers?

Comments
Joey Beninghove - 9/19/2007 2:14 PM
# re: NBehave Experiment: MonoRail Controllers & Rhino Mocks
"probably it would be better to write the code in another CLR language with less verbose support for delegates."

Well yeah. Why do you think I'm learning Ruby?! RBehave/RSpec, here I come!

But! I do think the NBehave syntax is quite elegant given the constraints of the C# language.
Christian Crowhurst - 9/20/2007 3:21 AM
# re: NBehave Experiment: MonoRail Controllers & Rhino Mocks
Surely the expectation that the controller will retrieve the lists of Dtos are part of the "Then's"? For example:
"Then the controller should retrieve the shopping cart from the shopping cart service"
And "store the shopping cart returned in the property bag with key of: shoppingCart"

Obviously the problem here is the difference between state based and interaction based tests. IBT's invert the natural order of Arrange, Act and Assert (a reason why lots of people just don't like/grok IBT's; for example - http://codebetter.com/blogs/scott.bellware/archive/2007/04/15/161833.aspx)

If you consider a more extreme example where there are no Asserts but all Expectations you would have no Then's!

Here's a thought that could resolve this whole back-to-front nature of IBT's - what if NBehave knows about mocking frameworks (as you suggest), and examines the Then's to see if they are Expectations. Any Expectation that NBehave finds here, it executes as part of the Given's. Obviously, the first problem with this that comes to mind is how to order the execution of the Expectation’s found in the Then's along side the regular Given's.

Christian
Joey Beninghove - 9/20/2007 7:26 AM
# re: NBehave Experiment: MonoRail Controllers & Rhino Mocks
@Christian,
Well after looking at my example some more, I'm not sure a framework like NBehave is best suited to be used at this low of a level. Like I said, it was just a quick experiment. One of those "won't know until I try" moments.

I AM a fan of IBT though because I've found that it helps me focus on the behaviors of the system instead of just the data. Of course you need *some* state based tests, specifically in your domain layer. My service layer & controller tests often are a mix of interaction & state techniques as shown in this example.
Post Comment






Please add 1 and 8 and type the answer here: