To complete the unit testing for the CartController class, we need to test the behavior of the new overloaded version of the Checkout method. Although the method looks short and simple, the use of MVC Framework model binding means that there is a lot going on behind the scenes that needs to be tested.
We should process an order only if there are items in the cart and the customer has provided us with valid shipping details. Under all other circumstances, the customer should be shown an error.
Here is the first test method:
[TestMethod]
public void Cannot_Checkout_Empty_Cart() { // Arrange - create a mock order processor
Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();
// Arrange - create an empty cart Cart cart = new Cart();
// Arrange - create shipping details
ShippingDetails shippingDetails = new ShippingDetails();
// Arrange - create an instance of the controller
CartController target = new CartController(null, mock.Object);
// Act
ViewResult result = target.Checkout(cart, shippingDetails);
// Assert - check that the order hasn't been passed on to the processor
mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()), Times.Never());
// Assert - check that the method is returning the default view Assert.AreEqual("", result.ViewName);
// Assert - check that we are passing an invalid model to the view Assert.AreEqual(false, result.ViewData.ModelState.IsValid);
}
This test ensures that we can’t check out with an empty cart. We check this by ensuring that the ProcessOrder of the mock IOrderProcessor implementation is never called, that the view that the method returns is the default view (which will redisplay the data entered by customers and give them a chance to correct it), and that the model state being passed to the view has been marked as invalid. This may seem like a belt-and-braces set of assertions, but we need all three to be sure that
we have the right behavior. The next test method works in much the same way, but injects an error into the view model to simulate a problem reported by the model binder (which would happen in production when the customer enters invalid shipping data):
[TestMethod]
public void Cannot_Checkout_Invalid_ShippingDetails() { // Arrange - create a mock order processor
Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();
// Arrange - create a cart with an item Cart cart = new Cart();
cart.AddItem(new Product(), 1);
// Arrange - create an instance of the controller
CartController target = new CartController(null, mock.Object);
// Arrange - add an error to the model
target.ModelState.AddModelError("error", "error");
// Act - try to checkout
ViewResult result = target.Checkout(cart, new ShippingDetails());
// Assert - check that the order hasn't been passed on to the processor
mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()), Times.Never());
// Assert - check that the method is returning the default view Assert.AreEqual("", result.ViewName);
// Assert - check that we are passing an invalid model to the view Assert.AreEqual(false, result.ViewData.ModelState.IsValid);
}
Having established that an empty cart or invalid details will prevent an order from being processed, we need to ensure that we do process orders when appropriate. Here is the test:
[TestMethod]
public void Can_Checkout_And_Submit_Order() { // Arrange - create a mock order processor
Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();
// Arrange - create a cart with an item Cart cart = new Cart();
cart.AddItem(new Product(), 1);
// Arrange - create an instance of the controller
CartController target = new CartController(null, mock.Object);
// Act - try to checkout
ViewResult result = target.Checkout(cart, new ShippingDetails());
// Assert - check that the order has been passed on to the processor
mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()),
Times.Once());
// Assert - check that the method is returning the Completed view Assert.AreEqual("Completed", result.ViewName);
// Assert - check that we are passing a valid model to the view Assert.AreEqual(true, result.ViewData.ModelState.IsValid);
}
Notice that we didn’t need to test that we can identify valid shipping details. This is handled for us automatically by the model binder using the attributes we applied to the properties of the
ShippingDetails class.
Displaying Validation Errors
If users enter invalid shipping information, the individual form fields that contain the problems will be highlighted, but no message will be displayed. Worse, if users try to check out an empty cart, we don’t let them complete the order, but they won’t see any error message at all. To address this, we need to add a validation summary to the view, much as we did back in Chapter 3. Listing 8-38 shows the addition to Checkout.cshtml view.
38. Listing 8-38. Adding a Validation Summary ...
<h2>Check out now</h2>
Please enter your details, and we'll ship your goods right away!
@using (Html.BeginForm()) {
@Html.ValidationSummary()
<h3>Ship to</h3>
<div>Name: @Html.EditorFor(x => x.Name)</div>
...
Now when customers provide invalid shipping data or try to check out an empty cart, they are shown useful error messages, as shown in Figure 8-17.
Figure 8-17. Displaying validation messages
Displaying a Summary Page
To complete the checkout process, we will show customers a page that confirms the order has been processed and thanks them for their business. Right-click either of the Checkout methods in the CartController class and select Add View from the pop-up menu. Set the name of the view to Completed, as shown in Figure 8-18.
Figure 8-18. Creating the Completed view
We don’t want this view to be strongly typed because we are not going to pass any view models between the controller and the view. We do want to use a layout, so that the summary page will be consistent with the rest of the application. Click the Add button to create the view and edit the content so that it matches Listing 8-39.
39. Listing 8-39. The Completed.cshtml View
@{
ViewBag.Title = "SportsStore: Order Submitted";
}
<h2>Thanks!</h2>
Thanks for placing your order. We'll ship your goods as soon as possible.
Now customers can go through the entire process, from selecting products to checking out. If they provide valid shipping details (and have items in their cart), they will see the summary page when they click the Complete order button, as shown in Figure 8-19.
Figure 8-19. The thank-you page
Summary
We’ve completed all the major parts of the customer-facing portion of SportsStore. It might not be enough to worry Amazon, but we have a product catalog that can be browsed by category and page, a neat shopping cart, and a simple checkout process.
The well-separated architecture means we can easily change the behavior of any piece of the application without worrying about causing problems or inconsistencies elsewhere. For example, we could process orders by storing them in a database, and it wouldn’t have any impact on the shopping cart, the product catalog, or any other area of the application.
In the next chapter, we’ll complete the SportsStore application by adding the administration features, which will let us manage the product catalog and upload, store, and display images for each product.
n n n
SportsStore: Administration
In this final chapter on building the SportsStore application, we will give the site administrator a way of managing the product catalog. We will add support for creating, editing, and removing items from the product repository, as well as for uploading and displaying images alongside products in the catalog. And, since these are administrative functions, we’ll show you how to use authentication and filters to secure access to controllers and action methods, and to prompt users for credentials when needed.
Adding Catalog Management
The convention for managing collections of items is to present the user with two types of pages: a list page and an edit page, as shown in Figure 9-1.
Figure 9-1. Sketch of a CRUD UI for the product catalog
Together, these pages allow a user to create, read, update, and delete items in the collection. As noted in Chapter 7, collectively, these actions are known as CRUD. Developers need to implement CRUD so often that Visual Studio tries to help by offering to generate MVC controllers that have action methods for CRUD operations and view templates that support them.
Creating a CRUD Controller
We will create a new controller to handle our administration functions. Right-click the Controllers folder of the SportsStore.WebUI project and select Add Controller from the pop-up menu. Set the name of the controller to AdminController and select Controller with empty read/write actions from the Template drop-down list, as shown in Figure 9-2.
Figure 9-2. Creating a controller using the Add Controller dialog box
Click the Add button to create the controller. You can see the code that the template produces in Listing 9-1.
1. Listing 9-1. The Visual Studio CRUD Template using System.Web.Mvc;
namespace SportsStore.WebUI.Controllers { public class AdminController : Controller {
public ActionResult Index() { return View(); } public ActionResult Details(int id) { return View();}
public ActionResult Create() { return View();}
[HttpPost]
public ActionResult Create(FormCollection collection) {
try {
// TODO: Add insert logic here return RedirectToAction("Index");
} catch { return View();
} }
public ActionResult Edit(int id) { return View();}
[HttpPost]
public ActionResult Edit(int id, FormCollection collection) { try {
// TODO: Add update logic here return RedirectToAction("Index");
} catch { return View();
} }
public ActionResult Delete(int id) { return View();}
[HttpPost]
public ActionResult Delete(int id, FormCollection collection) { try {
// TODO: Add delete logic here return RedirectToAction("Index");
} catch { return View();
} } } }
This is Visual Studio’s default CRUD template. However, we aren’t going to use it for our SportsStore
application because it isn’t ideal for our purposes. We want to demonstrate how to build up the controller and explain each step as we go. So, remove all of the methods in the controller and edit the code so that it matches Listing 9-2.
2. Listing 9-2. Starting Over with the AdminController Class using System.Web.Mvc;
using SportsStore.Domain.Abstract;
namespace SportsStore.WebUI.Controllers { public class AdminController : Controller { private IProductRepository repository;
public AdminController(IProductRepository repo) {
repository = repo;
} } }
Rendering a Grid of Products in the Repository
To support the list page shown in Figure 9-1, we need to add an action method that will display all of the products in the repository. Following the MVC Framework conventions, we’ll call this method Index. Add the action method to the controller, as shown in Listing 9-3.
3. Listing 9-3. The Index Action Method using System.Web.Mvc;
using SportsStore.Domain.Abstract;
namespace SportsStore.WebUI.Controllers { public class AdminController : Controller { private IProductRepository repository;
public AdminController(IProductRepository repo) { repository = repo;
}
public ViewResult Index() {
return View(repository.Products);
} } }