使用测试驱动开发工具rhino mocks的一些通用场景

In first part of the Rhino Mock explorations, I've tried to give answers on what is mocking, what are the different recording modes available in Rhino mocks, how to set up some of results etc.

Today post would continue that by focusing on a few typical mocking tasks. To make this post easier to digest I've wrap those tasks into one "real world" example

Use case

ShoppingCart is class defining the Checkout method which is been called in the moment user wants to make a payment for the purchased items in the cart.
It has two properties: a) UserId - represents the id of the shopping cart user, b) CartAmount - represents the total amount to be charged to user for chosen cart items.

Shopping cart checkout process loads user data using a type implementing IUserRepository. Once user data are loaded, user bank account number is been used by a type implementing the IPaymentProcessor  interface and then if a user has enough funds on his account, cart sends directs payment to payment processor type.

Once the payment would be successfully processed in payment processor, payment processor would raise an event which would signalize the cart that the payment is been completed.  When that would happen cart should store the payment in user payment history and reset the cart value to zero

 

Considering the mentioned use case, we could be testing next things:
1) In case user repository couldn't find an user for a given user id, cart should throw argument exception
2) We need to verify that once payment processor raised an event and signalized that the payment is completed the cart value would be reset to zero and the user repository would add that payment to payment history
3) In case payment attempt would failed due to time out, cart component should recover gracefully and the cart value should stay untouched
4) In case user is having less money on account then the cart value, the payment method should not be called and the cart amount should stay unchanged
5) We need to verify that the performance is optimal by verifying that the retrieving of user data occurred only once

Shopping cart static class diagram

Shopping cart class diagram

As we can see from the class diagram shopping cart uses constructor type of dependency injection design pattern to get pointers to instances of types implementing the IUserRepository and IPaymentProcessor which are then used in checkout method. PaymentSucceed method is a event handler  which handles PaymentSucceed event of the Payment processor.

Although I prefer black box testing approach, we would take a peek at the shopping cart implementation because purpose of this post is not TDD test first methodology, but mocking examples

Checkout method

 

   1: public void Checkout()
   2: {
   3:     User user = _userRepository.GetUser(UserName);
   4:     if (user == null)
   5:         throw new ArgumentException();
   6:  
   7:     double bankBalance = _paymentProcessor.GetBankBalance(user.BankAccount);
   8:     bool userHasEnoughMoney = bankBalance > Amount;
   9:     if (userHasEnoughMoney)
  10:     {
  11:         double oldAmount = Amount;
  12:         try
  13:         {
  14:             _paymentProcessor.MakePayment(user.BankAccount);
  15:         }
  16:         catch (TimeoutException e)
  17:         {
  18:             // logger.Error("Payment time out", e);
  19:             Amount = oldAmount;
  20:         }
  21:     }
  22: }

The method first retrieves the user for a given user name. Then, user bank balance amount is been retrieved  and in case of user having enough money on his account, payment is been made

PaymentSucceed method

   1: void PaymentSucceed(object sender, EventArgs e)
   2: {
   3:     _userRepository.AddToUserPaymentHistory(UserName, Amount);
   4:     Amount = 0;
   5: }

A PaymentSucceed event would be processed by adding user payment history information and resetting cart amount to zero

Test 1. How to define expectations on a method accepting unknown parameters

Rhino Mock concepts: IgnoreArguments()

In case user repository couldn't find an user for a given user id, cart should throw argument exception

Test purpose

Purpose of this test is to verify that if a GetUser would get ANY argument which would cause returning of the null user value, shopping cart checkout method should throw an argument exception

Test code

   1: [Test]
   2: [ExpectedException(typeof(ArgumentException))]
   3: public void Checkout_Verify_That_NotKnownUser_Throws_ArgumentException()
   4: {
   5:     //setting up the stage - retrieval of the user would return null
   6:     Expect.Call(_userRepository.GetUser(null))
   7:         .IgnoreArguments()
   8:         .Return(null);
   9:  
  10:    //stop recording
  11:     _mockRepository.ReplayAll();
  12:  
  13:     ShoppingCart cart = new ShoppingCart(_user.Name, _userRepository, _paymentProcessor, 0);
  14:     cart.Checkout();
  15: }

Test interpretation

In line 6 we define that we expect someone would call  the GetUser method, but because our test is not specific to any constrain related to user name we are passing the null value as the expected userId parameter value but in line 7 we use the IgnoreArguments() method which commands the Rhino Mock framework to disregard what parameters are exactly used as previous method parameters.  In line 8, I've defined that the method would return null value.
Result of lines 6 , 7 and 8 could be read as something like "expect that there would be a call to GetUser method with some unknown parameter for which UserRepository would return null value as result".

Line 11 stops recording mode and line 14 calls the checkout method

Now, if we would take a look at line 3 in Checkout method code snippet we would see that expected call for retrieving user happened and Rhino Mock returned defined null value as result. That null value caused ArgumentException to be thrown in line 5 which is what this test was expecting.

Test 2. How to simulate event raising of mocked object

Rhino Mock concepts: LastCall, IgnoreArguments(), GetEventRaiser(), IEventRaiser

We need to verify that once payment processor raised an event and signalized that the payment is completed the cart value would be reset to zero and the user repository would add that payment to payment history

Test purpose

This test describes:
- how NUnit assert can be used on the non mocked object to verify that mocks made desired effect on it
-  how to simulate event raised on the mock

Test code

   1: [Test]
   2: public void Checkout_Verify_That_Successfull_CheckOut_Would_Make_Cart_Value_Zero()
   3: {
   4:     double cartAmount = 100;
   5:  
   6:     //setting up the stage 
   7:     // 1. Regular user with default data would be retrieved
   8:     Expect.Call(_userRepository.GetUser(_user.Name))
   9:         .Return(_user);
  10:  
  11:     //2. User would have enough money on his account for payment
  12:     Expect.Call(_paymentProcessor.GetBankBalance(_user.BankAccount))
  13:         .Return(cartAmount + 1);
  14:  
  15:     // 3. geting the payment proccessor payment succeed event raiser 
  16:     // which we would use to simulate the async signal that the payment is complete
  17:     _paymentProcessor.PaymentSucceed += null;
  18:     IEventRaiser paymentSucceedEventRaiser = LastCall.IgnoreArguments().GetEventRaiser(); 
  19:  
  20:     // 4. Expect that successfull payment would be stored in user payment history
  21:     _userRepository.AddToUserPaymentHistory(_user.Name, cartAmount);
  22:  
  23:     //stop recording
  24:     _mockRepository.ReplayAll();
  25:  
  26:     ShoppingCart cart = new ShoppingCart(_user.Name, _userRepository, _paymentProcessor, cartAmount);
  27:     cart.Checkout();
  28:     // we now simulate payment suceed event send from payment processor 
  29:     paymentSucceedEventRaiser.Raise(_paymentProcessor,new EventArgs());
  30:     // checking if the cart amount is zero
  31:     Assert.IsTrue(cart.Amount.Equals(0), "Cart amount to be paid not reset to zero.");
  32: }

Test interpretation

This test does next things:

  • in line 8,  we define that the call to GetUser method would return the default user
  • in line 12, we define that user has on his account amount of money greater then the cart amount
  • in line 17, we define expectations that some event handler (null value) would register to the PaymentSucceed event
  • in line 18, we use the LastCall class which is a helper class which points to the last recorded command in the Rhino Mock command stack.
    We can treat that LastCall as "pointer to L17", so the method IgnoreArguments()  can be read as "the null value as subscriber to the PaymentSucceed should be replaced with 'some event handler'".
    Executing GetEventRaiser() method on 'redefined' L17 results with a pointer to PaymentSucceed event which is stored in type implementing the IEventRaiser
  • In line 21, we define expectation that the payed amount would be added to the user payment history
  • in line 24, we stop recording expectations
  • in line 27, we call the checkout method of the shopping cart with valid payment context which should result with completion of the payment
  • In line 29, we use the event pointer retreived in L18 to simulate PaymentSucceed event raising by the PaymentProcessor as the result of successful payment
  • In line 31, we check that the cart value

 

Test 3. How to simulate exception throwing of mocked object

Rhino mock concepts: LastCall, Throw

In case payment attempt would failed due to time out, cart component should recover gracefully and the cart value should stay untouched

Test purpose

This test describes two things:
a) it explains LastCall mechanism for handling void returning methods in Rhino Mocks
b) it explains how to simulate mocked object throwing an exception scenario

Test code

   1: [Test]
   2: public void Checkout_Verify_That_In_Case_Of_Payment_Timeout_Cart_Value_Would_Stay_Unchanged()
   3: {
   4:     double cartAmount = 100;
   5:  
   6:     //setting up the stage 
   7:     
   8:     // 1. Regular user would be retrieved
   9:     Expect.Call(_userRepository.GetUser(_user.Name))
  10:         .Return(_user);
  11:  
  12:     //2. Account check would return information that user is having enough money
  13:     Expect.Call(_paymentProcessor.GetBankBalance(_user.BankAccount))
  14:         .Return(cartAmount + 1);
  15:  
  16:     _paymentProcessor.MakePayment(_user.BankAccount);
  17:     LastCall.Throw(new TimeoutException("Pament failed. Please try later again."));
  18:  
  19:     //stop recording
  20:     _mockRepository.ReplayAll();
  21:  
  22:     // cart amount is less then user account balance
  23:     ShoppingCart cart = new ShoppingCart(_user.Name, _userRepository, _paymentProcessor, cartAmount);
  24:     try
  25:     {
  26:         cart.Checkout();
  27:         Assert.IsTrue(cart.Amount == cartAmount, "Cart amount is changed altought there was timeout roolback scenario");
  28:     }
  29:     catch (TimeoutException)
  30:     {
  31:         Assert.Fail("Checkout procedure didn't recover gracefully from timeout exception");
  32:     }

Test interpretation

This test does next things:

  • in line 9  we define that the call to GetUser method would return the default user
  • in line 13, we define that user has on his account amount of money greater then the cart amount
  • in line 16, we define expectations that the MakePayment method of the mocked payment processor would be called with a user bank account.
    We are not using Expect.Call(_paymentProcessor.MakePayment(_user.BankAccount)) because that is not applicable to the methods returning void value due to the limitations of the C#
  • in line 17, we go around the C# limitation related to void methods and use the LastCall class which is a helper class which points to the last recorded command in the Rhino Mock command stack.
    So the result of LastCall would be pointer to L16
  • in line 17, once we would have a pointer defined with LastCall we use a Throw method to get "MakePayment method execution in L16 would throw an TimeoutException"
  • in line 20, we stop recording expectations
  • In line 26, we trigger the checkout method execution (which would receive TimeoutException defined in L17
  • In line 27 we verify that the cart value stayed unchanged after the TimeoutException occured
  • In line 29, we catch the TimeOutExcpetion and make assertation failing because if that woudl happen that would mean that the cart checkout method didn't recover gracefully from exception so the exception bubbled up outside of the checkout method

Test 4. How to verify that some method of the mocked object was never called

Rhino mock concepts: Repeat.Never

In case payment attempt would failed due to time out, cart component should recover gracefully and the cart value should stay untouched

Test purpose

This test describes:

-usage of Repeat.Never() for testing that some method WAS NOT executed in certain scenario

Test code

   1: [Test]
   2: public void Checkout_Verify_That_User_Without_Enough_Money_Cant_Complete_CheckOut()
   3: {
   4:     double cartAmount = 100; 
   5:  
   6:     //setting up the stage 
   7:     // 1. Regular user would be retrieved
   8:     Expect.Call(_userRepository.GetUser(_user.Name))
   9:         .Return(_user); 
  10:  
  11:     //2. Account check would return information that user is having enough money
  12:     Expect.Call(_paymentProcessor.GetBankBalance(_user.BankAccount))
  13:         .Return(cartAmount - 1); 
  14:  
  15:     //expectation: MakePayment won't be called because user is not having enough money on account
  16:     _paymentProcessor.MakePayment(null);
  17:     LastCall.IgnoreArguments()              /* we don't care which user id should be used */
  18:         .Repeat.Never();                    /* We expect zero method execution*/ 
  19:  
  20:     //stop recording
  21:     _mockRepository.ReplayAll(); 
  22:  
  23:     // cart amount is +1 then user account balance
  24:     ShoppingCart cart = new ShoppingCart(_user.Name, _userRepository, _paymentProcessor, cartAmount);
  25:     cart.Checkout();
  26:     Assert.IsTrue(cart.Amount == cartAmount, "Cart value was modified event the payment was not made");
  27: }
  28:  

This test does next things:

  • in line 8 ,  we define that the call to GetUser method would return the default user
  • in line 12, we define that user has on his account amount of money LESSER then the cart amount is (user is not having enough money to pay)
  • in line 16, we define expectations that the MakePayment method of the mocked payment processor would be called with a null value as user bank account.
    The reason why we pass a null value is that this method shouldn't be called at all in this scenario so it is totally irrelevant what user id would be passed
  • in line 17, we use the LastCall class which is a helper class which points to the last recorded command in the Rhino Mock command stack, which is in this case L16
  • in line 18, we define expectation that the method would be called exactly zero time == expectation that the method won't be called.
    If the method which expectation is defined with Repeat.None would be called, the test would fail
  • in line 21, we stop recording expectations
  • in line 25 ,we call the cart checkout method
  • in line 26, we verify that the cart value is not modified in use case of payment not made

Test 5. How to verify that performance are optimal

Rhino mock concepts: Repeat.Once combined with CreateMock mockery method

We need to verify that the performance is optimal by verifying that the retrieving of user data occurred only once

Test purpose

This test describes:

- Repeat.Once - How to test performance related things such as lazy load

Test code

   1: [Test]
   2: public void Checkout_Verify_That_Retreving_Of_User_Data_Occured_Only_Once()
   3: {
   4:     double bankBalance = 100;
   5:     double cartAmount = bankBalance - 1;
   6:  
   7:     //setting up the stage 
   8:     Expect.Call(_userRepository.GetUser(_user.Name))
   9:         .Return(_user)
  10:         .Repeat.Once();
  11:  
  12:     //stop recording
  13:     _mockRepository.ReplayAll();
  14:  
  15:     // cart amount is +1 then user account balance
  16:     ShoppingCart cart = new ShoppingCart(_user.Name, _userRepository, _paymentProcessor, cartAmount);
  17:     cart.Checkout();
  18: }

This test does next things:

  • in line 8 and 9, we define expectation that the user repository would return default user data
  • in line 10, we constrain that expectation with expectation that the method would be called exactly once.
    This simple constrain is a way how to test lazy loads and similar things
  • in line 13, we stop recording expectations
  • in line 17, we call the cart check out method

 

Test 6. How to customize failed expectation messages 

Rhino mock concepts: Message

Test purpose

This test describes:

- Rhino mock Message method purpose

This test would be the only one failing so we could see the error message

Test code

   1: [Test]
   2: public void Checkout_Verify_That_Retreving_Of_User_Data_Occured_Twice()
   3: {
   4:     //seting up the stage 
   5:     Expect.Call(_userRepository.GetUser(_user.Name))
   6:         .Return(_user)
   7:         .Repeat.Twice()
   8:         .Message("Retrieving of the user did not occurred twice");
   9:  
  10:     //stop recording
  11:     _mockRepository.ReplayAll();
  12:  
  13:     // cart amount is +1 then user account balance
  14:     ShoppingCart cart = new ShoppingCart(_user.Name, _userRepository, _paymentProcessor, 0);
  15:     cart.Checkout();
  16: }

This test does next things:

  • in line 5 and 6, we define expectation that the user repository would return default user data
  • in line 7, we constrain that expectation with expectation that the method would be called exactly twice.
    We saw in previous test that expectation won't be met and this expectation failure would cause test failure
  • in line 8, we define what would be the message which would be outputted if the expectation would fail
  • in line 11, we stop recording
  • in line 15, we call cart checkout method

This test would fail with message

Rhino.Mocks.Exceptions.ExpectationViolationException: Message: Retrieving of the user did not occurred twice
IUserRepository.GetUser("Nikola"); Expected #2, Actual #1.

Conclusion

I believe this couple of examples of how easy and intuitive rhino mock usage is would encourage all the mock sceptics to try use them and TDD development process

I plan to continue posting about Rhino mocks in upcoming days, providing the real world answers on real world type of questions one may have about Rhino mocks

Source code of today's example can be found: here.

posted @ 2011-06-30 16:48  ido  阅读(1584)  评论(0编辑  收藏  举报