If you’ve never done Test Driven Development or aren’t even sure what this "crazy TDD stuff” is all about than this is the series for you. Over the next 30 days this series of posts take you from “I can spell TDD” to being able to consider yourself a “functional” TDD developer. Of course TDD is a very deep topic and truly mastering it will take quite a bit of time, but the rewards are well worth it. Along the way I’ll be showing you how tools like JustCode and JustMock can help you in your practice of TDD.
Previous Posts in this Series: Day 16: Using Parameters in Stubs
We’ll continue developing our e-commerce application today by looking closer at how we will utilize the Order Fulfillment service. This service is provided by a third party and the API requires an ordered set of calls to be executed. In this post I’ll show you how your mocks can enforce this rule and let you know when your test violate it.
As you’ll recall from the last post, we are going to be working with a third party service to provide order fulfillment. They have provided an API which we are going to call our OrderFulfillmentService. The interface to this API has several different calls and a set of rules describing which calls must be made and in which order. An excerpt of the interface (simplified for this post) is listed here:
1: using System;
2: using System.Collections.Generic;
3:
4: namespace TddStore.Core
5: {
6: public interface IOrderFulfillmentService
7: {
8: Guid OpenSession(string user, string password);
9:
10: bool IsInInventory(Guid sessionId, Guid ItemNumber, int quantity);
11:
12: bool PlaceOrder(Guid sessionId, IDictionary<Guid, int> items, string mailingAddress);
13:
14: void CloseSession(Guid sessionId);
15: }
16: }
We can debate about the efficiently or lack thereof of this interface, but it’s the interface that our fulfillment partner is using. So that means we have to consume this interface. There are a few rules around the workflow for this API:
These are a few of the constraints in these rules that we must work within. We won’t try to tackle them all today, but we will work on the order of operations constraints. The first thing we need is a test case:
When placing an order with an item that is in inventory the order fulfillment workflow should complete
Yes, this a very simple case, but remember; we want to test and develop small simple things that we can then combine into larger things. This test case is very focused on the goal of today's post, which is ensuring that our calls to OrderFulfillment are made in order. The first thing I do is start with a test, which I’ve also gone ahead and added the basic Arrange pieces we’ll need:
1: [Test]
2: public void WhenUserPlacesOrderWithItemThatIsInInventoryOrderFulfillmentWorkflowShouldComplete()
3: {
4: //Arrange
5: var shoppingCart = new ShoppingCart();
6: var itemId = Guid.NewGuid();
7: shoppingCart.Items.Add(new ShoppingCartItem { ItemId = itemId, Quantity = 1 });
8: var customerId = Guid.NewGuid();
9: var customer = new Customer { Id = customerId };
10:
11: Mock.Arrange(() => _customerService.GetCustomer(customerId)).Returns(customer).OccursOnce();
12: }
These are the basic things I need to call PlaceOrder on the OrderService. As mentioned in the last post, I will need to call the CustomerService to get the customer so I can get the customer mailing address. I now need to arrange my mock. I’m going make a slight alteration to my existing arrange and create my “Act” section by calling the PlaceOrder method on the OrderService. As for the mock, I’m going to start with the arrangement for the OpenSession method:
1: [Test]
2: public void WhenUserPlacesOrderWithItemThatIsInInventoryOrderFulfillmentWorkflowShouldComplete()
3: {
4: //Arrange
5: var shoppingCart = new ShoppingCart();
6: var itemId = Guid.NewGuid();
7: shoppingCart.Items.Add(new ShoppingCartItem { ItemId = itemId, Quantity = 1 });
8: var customerId = Guid.NewGuid();
9: var customer = new Customer { Id = customerId };
10: var orderFulfillmentSessionId = Guid.NewGuid();
11:
12: Mock.Arrange(() => _customerService.GetCustomer(customerId)).Returns(customer).OccursOnce();
13:
14: Mock.Arrange(() => _orderFulfillmentService.OpenSession(Arg.IsAny<string>(), Arg.IsAny<string>()))
15: .Returns(orderFulfillmentSessionId)
16: .InOrder();
17:
18: //Act
19: _orderService.PlaceOrder(customerId, shoppingCart);
20:
21: //Assert
22: Mock.Assert(_orderFulfillmentService);
23: }
Most of the code I added should be pretty self-explanatory. On line ten I added a value to create/capture an order fulfillment session id, and on line 22 I assert that my mock has been used correctly. The interesting part of this code is that on line 16 I’ve added the InOrder constraint to my mock. What this means is that as I continue to arrange my mock in my test, JustMock will expect the methods on my mock to execute in the specified order. I’ll complete the arrange section of my test to demonstrate this:
1: [Test]
2: public void WhenUserPlacesOrderWithItemThatIsInInventoryOrderFulfillmentWorkflowShouldComplete()
3: {
4: //Arrange
5: var shoppingCart = new ShoppingCart();
6: var itemId = Guid.NewGuid();
7: shoppingCart.Items.Add(new ShoppingCartItem { ItemId = itemId, Quantity = 1 });
8: var customerId = Guid.NewGuid();
9: var customer = new Customer { Id = customerId };
10: var orderFulfillmentSessionId = Guid.NewGuid();
11:
12: Mock.Arrange(() => _customerService.GetCustomer(customerId)).Returns(customer).OccursOnce();
13:
14: Mock.Arrange(() => _orderFulfillmentService.OpenSession(Arg.IsAny<string>(), Arg.IsAny<string>()))
15: .Returns(orderFulfillmentSessionId)
16: .InOrder();
17: Mock.Arrange(() => _orderFulfillmentService.IsInInventory(orderFulfillmentSessionId, itemId, 1))
18: .Returns(true)
19: .InOrder();
20: Mock.Arrange(() =>
21: _orderFulfillmentService.
22: PlaceOrder(orderFulfillmentSessionId, Arg.IsAny<IDictionary<Guid, int>>(), Arg.IsAny<string>()))
23: .Returns(true)
24: .InOrder();
25: Mock.Arrange(() => _orderFulfillmentService.CloseSession(orderFulfillmentSessionId))
26: .InOrder();
27:
28: //Act
29: _orderService.PlaceOrder(customerId, shoppingCart);
30:
31: //Assert
32: Mock.Assert(_orderFulfillmentService);
33: }
In the above code, starting on line 14 and continuing through line 26 I am arranging the mock for my OrderFulfillmentService. Don’t worry or get distracted by the parameter lists for the mocks; we’ll deal with making those a little more specific in a future post. For now we’re focusing on the order of the methods. You’ll notice that I’ve arranged these mocks in the order I want the methods to be called; OpenSession –> IsInInventory –> PlaceOrder –> CloseSession. If I attempt to call these in any other order, my test will fail when I call MockAssert (line 32).
If I try to run this test I will get a compiler error for not declaring the variable _orderFulfillmentService. Up till now I have not needed an OrdeFulfillment service, but now I and and thus I need a mock for it:
1: namespace TddStore.UnitTests
2: {
3: [TestFixture]
4: class OrderServiceTests
5: {
6: private OrderService _orderService;
7: private IOrderDataService _orderDataService;
8: private ICustomerService _customerService;
9: private IOrderFulfillmentService _orderFulfillmentService;
10:
11: [TestFixtureSetUp]
12: public void SetupTestFixture()
13: {
14: _orderDataService = Mock.Create<IOrderDataService>();
15: _customerService = Mock.Create<ICustomerService>();
16: _orderFulfillmentService = Mock.Create<IOrderFulfillmentService>();
17: _orderService = new OrderService(_orderDataService, _customerService);
18: }
OK, time to run our test (Figure 1):
Figure 1 – An excerpt of the error message for our failing test
Our test failed as none of the methods on our OrderFulfillmentService were called. The next step is to implement the code needed for this test:
1: public Guid PlaceOrder(Guid customerId, ShoppingCart shoppingCart)
2: {
3: foreach (var item in shoppingCart.Items)
4: {
5: if (item.Quantity == 0)
6: {
7: throw new InvalidOrderException();
8: }
9: }
10:
11: var customer = _customerService.GetCustomer(customerId);
12:
13: //Open Session
14: var orderFulfillmentSessionId = _orderFulfillmentService.OpenSession(USERNAME, PASSWORD);
15:
16: var firstItemId = shoppingCart.Items[0].ItemId;
17: var firstItemQuantity = shoppingCart.Items[0].Quantity;
18:
19: //Place Order
20: var orderForFulfillmentService = new Dictionary<Guid, int>();
21: orderForFulfillmentService.Add(firstItemId, firstItemQuantity);
22: var orderPlaced = _orderFulfillmentService.PlaceOrder(orderFulfillmentSessionId,
23: orderForFulfillmentService,
24: customer.ShippingAddress.ToString());
25:
26: //Check Inventory Level
27: var itemIsInInventory = _orderFulfillmentService.IsInInventory(orderFulfillmentSessionId, firstItemId, firstItemQuantity);
28:
29:
30: //Close Session
31: _orderFulfillmentService.CloseSession(orderFulfillmentSessionId);
32:
33: var order = new Order();
34: return _orderDataService.Save(order);
35: }
I start working with the OrderFulFillmentService on line 14 and I use comments to outline what I am doing in each section. Again; don’t focus too much on how we’re building the payload for the PlaceOrder method on the OrderFulfillmentService on line 21; that aspect of the functionality is not part of the current test case and we’ll handle building this part of the method in an upcoming post. For now, notice that my steps are not in the correct order; I go from opening the session straight to ordering instead of checking the inventory level first. I can attempt to run this test, but I’ll get a compile error as I don’t have an instance of OrderFulfillmentService declared, and have not updated the constructor to accept an instance through injection. I’ll fix both of those problems now. While I’m at it, I’ll declare my constants for username and password:
1: namespace TddStore.Core
2: {
3: public class OrderService
4: {
5: private IOrderDataService _orderDataService;
6: private ICustomerService _customerService;
7: private IOrderFulfillmentService _orderFulfillmentService;
8: private const string USERNAME = "Bob";
9: private const string PASSWORD = "Foo";
10:
11:
12: public OrderService(IOrderDataService orderDataService, ICustomerService customerService,
13: IOrderFulfillmentService orderFulfillmentService)
14: {
15: _orderDataService = orderDataService;
16: _customerService = customerService;
17: _orderFulfillmentService = orderFulfillmentService;
18: }
Trying to run it results in more compilation errors as my unit test has not been updated to inject the mock of the OrderFulFillmentService as the third argument in the OrderService. I’ll update that as well:
1: [TestFixtureSetUp]
2: public void SetupTestFixture()
3: {
4: _orderDataService = Mock.Create<IOrderDataService>();
5: _customerService = Mock.Create<ICustomerService>();
6: _orderFulfillmentService = Mock.Create<IOrderFulfillmentService>();
7: _orderService = new OrderService(_orderDataService, _customerService, _orderFulfillmentService);
8: }
Our app finally compiles and now it’s time to run our test! And it fails! (Figure 2):
Figure 2 – Our methods are out of order
This was not unexpected; if you’ll recall I purposely called the methods in the OrderFulfillmentService in the wrong order so you could see what a failure looks like. JustMock tells us that the methods were called out of order, and supplies me with the list of methods and the order they were called in. I can see here that it does not like that PlaceOrder was called right after OpenSession. I will swap those steps in the PlaceOrder method of OrderService and we can see if that fixes the problem:
1: public Guid PlaceOrder(Guid customerId, ShoppingCart shoppingCart)
2: {
3: foreach (var item in shoppingCart.Items)
4: {
5: if (item.Quantity == 0)
6: {
7: throw new InvalidOrderException();
8: }
9: }
10:
11: var customer = _customerService.GetCustomer(customerId);
12:
13: //Open Session
14: var orderFulfillmentSessionId = _orderFulfillmentService.OpenSession(USERNAME, PASSWORD);
15:
16: var firstItemId = shoppingCart.Items[0].ItemId;
17: var firstItemQuantity = shoppingCart.Items[0].Quantity;
18:
19: //Check Inventory Level
20: var itemIsInInventory = _orderFulfillmentService.IsInInventory(orderFulfillmentSessionId, firstItemId, firstItemQuantity);
21:
22: //Place Order
23: var orderForFulfillmentService = new Dictionary<Guid, int>();
24: orderForFulfillmentService.Add(firstItemId, firstItemQuantity);
25: var orderPlaced = _orderFulfillmentService.PlaceOrder(orderFulfillmentSessionId,
26: orderForFulfillmentService,
27: customer.ShippingAddress.ToString());
28:
29: //Close Session
30: _orderFulfillmentService.CloseSession(orderFulfillmentSessionId);
31:
32: var order = new Order();
33: return _orderDataService.Save(order);
34: }
Time to run the test again (Figure 3):
Figure 3 – The test passes
By making sure my methods were called in the correct order, I’ve been able to make sure my test passes.
Occasionally we are going to be made to work with systems that have complex APIs. It’s easy to unintentionally change your code in a way that breaks the rules of these APIs. By using the InOrder method of JustMock I was able to create a mock that enforced the same rules as our third party API during testing. This helps me make sure that not only will my code correctly use those APIs today, they will continue to do so in the future.
Continue the TDD journey:
James Bender is a Developer and has been involved in software development and architecture for almost 20 years. He has built everything from small, single-user applications to Enterprise-scale, multi-user systems. His specialties are .NET development and architecture, TDD, Web Development, cloud computing, and agile development methodologies. James is a Microsoft MVP and the author of two books; "Professional Test Driven Development with C#" which was released in May of 2011 and "Windows 8 Apps with HTML5 and JavaScript" which will be available soon. James has a blog at JamesCBender.com and his Twitter ID is @JamesBender.