您的位置:首页 > 编程语言 > ASP

【Pro ASP.NET MVC 3 Framework】.学习笔记.7.SportsStore:购物车

2013-08-30 17:26 711 查看
3 创建购物车

每个商品旁边都要显示Add to cart按钮。点击按钮后,会显示客户已经选中的商品的摘要,包括总金额。在购物车里,用户可以点击继续购物按钮返回product目录。也可以点击Checkout now按钮,完成订单和购物会话。

3.1 定义Cart Entity

购物车是程序业务域的一部分,在我们的领域模型中创建实体。添加一个Cart类到Entities文件夹。

namespace SportsStore.Domain.Entities { publicclass Cart { private List<CartLine> lineCollection =new List<CartLine>(); publicvoid AddItem(Product product, int quantity) { //检查购物车中是否已经有该产品 CartLine line = lineCollection .Where(p => p.Product.ProductID == product.ProductID) .FirstOrDefault(); if (line ==null) { lineCollection.Add(new CartLine { Product = product, Quantity = quantity }); } else { line.Quantity +=quantity; } } publicvoid RemoveLine(Product product) { lineCollection.RemoveAll(l => l.Product.ProductID == product.ProductID); } publicdecimal ComputeTotalValue() { return lineCollection.Sum(e => e.Product.Price * e.Quantity); } publicvoid Clear() { lineCollection.Clear(); } public IEnumerable<CartLine> Lines { get { return lineCollection; } } } publicclass CartLine { public Product Product { get; set; } publicint Quantity { get; set; } } }

购物车类使用CartLine,代表用户选中的一个商品。定义了添加、移除、计算合计、清空的方法。我们也提供了一个属性,返回IEnumerble<CartLine。

3.1.1 测试购物车单元测试

Cart类相对简单,但有一些非常重要的行为,我们必须确保工作正常。一个功能不良的购物车会破坏程序的整体。偶们对这些特性一个一个测试。

第一个要测试的行为,是将添加货物到购物车。如果该商品是第一次被加到购物车,我们需要一个新的CartLine。

[TestMethod] publicvoid Can_Add_New_Lines() { //Arrange - create some test products Product p1 =new Product { ProductID =1, Name ="P1" }; Product p2 =new Product { ProductID =2, Name ="P2" }; //Arrange - create a new cart Cart target =new Cart(); //Act target.AddItem(p1, 1); target.AddItem(p2, 1); CartLine[] results = target.Lines.ToArray(); //Assert Assert.AreEqual(results.Length, 2); Assert.AreEqual(results[0].Product, p1); Assert.AreEqual(results[1].Product, p2); }

如果客户已经添加过该商品,我们需要增加相应CartLine的数量,而不是创建一个新的。

[TestMethod] publicvoid Can_Add_Quantiy_For_Existing_Lines() { //Arrange - create some test products Product p1 =new Product { ProductID =1, Name ="P1" }; Product p2 =new Product { ProductID =2, Name ="P2" }; //Arrange - create a new cart Cart target =new Cart(); //Act target.AddItem(p1, 1); target.AddItem(p2, 1); target.AddItem(p1, 10); CartLine[] results = target.Lines.OrderBy(c => c.Product.ProductID).ToArray(); //Assert Assert.AreEqual(results.Length, 2); Assert.AreEqual(results[0].Quantity, 11); Assert.AreEqual(results[1].Quantity, 1); }

我们也需要检查移除商品的功能。

[TestMethod] publicvoid Can_Remove_Lines() { //Arrange - create some test products Product p1 =new Product { ProductID =1, Name ="P1" }; Product p2 =new Product { ProductID =2, Name ="P2" }; Product p3 =new Product { ProductID =3, Name ="P3" }; //Arrange - create a new cart Cart target =new Cart(); //Arrange - add some products to the cart target.AddItem(p1, 1); target.AddItem(p2, 3); target.AddItem(p3, 5); target.AddItem(p2, 1); //Act target.RemoveLine(p2); //Assert Assert.AreEqual(target.Lines.Where(c=>c.Product==p2).Count(),0); Assert.AreEqual(target.Lines.Count(),2); }

计算总金额的功能:

[TestMethod] publicvoid Calculate_Cart_Total() { //Arrange - create some test products Product p1 =new Product { ProductID =1, Name ="P1" ,Price=100M}; Product p2 =new Product { ProductID =2, Name ="P2" ,Price=50M}; //Arrange - create a new cart Cart target =new Cart(); //Act target.AddItem(p1, 1); target.AddItem(p2, 1); target.AddItem(p1, 3); decimal result = target.ComputeTotalValue(); //Assert Assert.AreEqual(result, 450M); }

最后测试的是清空功能

[TestMethod] publicvoid Can_Clear_Contents() { //Arrange - create some test products Product p1 =new Product { ProductID =1, Name ="P1" ,Price=100M}; Product p2 =new Product { ProductID =2, Name ="P2" ,Price=50M}; //Arrange - create a new cart Cart target =new Cart(); //Act target.AddItem(p1, 1); target.AddItem(p2, 1); target.Clear(); //Assert Assert.AreEqual(target.Lines.Count(), 0); }

3.2 Add to Cart按钮

@model SportsStore.Domain.Entities.Product <div class="item"> <h3>@Model.Name</h3> @Model.Description @using(Html.BeginForm("AddToCart","Cart")){ @Html.HiddenFor(x=>x.ProductID) @Html.Hidden("returnUrl",Request.Url.PathAndQuery) <input type="submit" value="+ Add to cart"/> } <h4>@Model.Price.ToString("c")</h4> </div>

改变ProductSummary.cshtml局部视图。当表单被提交时,会提交到Cart controller中的AddToCart action方法。

默认地,BeginForm helper方法创建一个表单,使用HTTP POST方法。可以变为GET方法。

3.2.1 在同一个地方创建多个HTML FORMS

使用HTML.BeginForm helper在每个商品列表,意味着每个Add to cart按钮会被渲染在相互分隔的自己的HTML from元素中。在ASP.NET Web Forms中,一个页面限制只有一个form。ASP.NET MVC不限制每个页面的form数量,你要多少可以加多少。

不同form返回到相同的controller方法,伴随着不同的参数值,这是一个很好而且简单的方法,来处理button点击。

3.3 实现Cart Controller

我们需要创建一个CartController,来处理Add to cart按钮的点击。

publicclass CartController : Controller { // // GET: /Cart/ private IProductRepository repository; public CartController(IProductRepository repo) { repository = repo; } public RedirectToRouteResult AddToCart(int productId, string returnUrl) { Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId); if (product !=null) { GetCart().AddItem(product, 1); } return RedirectToAction("Index", new { returnUrl }); } public RedirectToRouteResult RemoveFromCart(int productId,string returnUrl) { Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId); if(product!=null){ GetCart().RemoveLine(product); } return RedirectToAction("Index", new { returnUrl }); } private Cart GetCart() { Cart cart = (Cart)Session["Cart"]; if(cart==null){ cart =new Cart(); Session["Cart"] = cart; } return cart; } }

这里有几个点。第一个是ASP.NET session状态特性,存储并检索Cart对象,这是GetCart方法的目的。ASP.NET有很好的session特性,使用cookis或URL重写用户的关联请求,从form一个单一浏览session。相关的的特性是session状态,它允许我们用session关联数据。这是一个适合偶们Cart类的想法。偶们像让每个用户有自己的购物车,我们想让购物车固定在不同的请求。数据关联到session,session过期时会删除。这意味着我们不需要管理Cart类的存储或生命周期。

Session["Cart"]=cart;//在Session对象上设置一个key的value Cart cart=(Cart)Session["Cart"];//检索对象,读取key

Session装填对象,默认存储在Asp.net服务器的内存中。你可以配置一个不同的存储路径,包括使用Sql数据库。

在AddToCart和RemoveFromCart方法中,我们使用参数名匹配HTML form中输入的元素。这允许MVC框架关联POST变量传递来的参数,意味着我们不需要手动处理。

3.4 显示Cart的Content

RedirectToAction方法,它的效果是,发送一个HTTP重定向指令,到客户端浏览器,让浏览器请求一个新的URL。在这个例子中,我们让浏览器请求Cart controller的Index action。

我们会实现Index方法,用它播放Cart的contents。偶们需要传递两个信息碎片给view:Cart对象和如果用户点击继续购物按钮后要显示的URL。为了这个目的,我们会创建一个简单的视图模型类,CartIndexViewModel。

publicclass CartIndexViewModel { public Cart Cart { get; set; } publicstring ReturnUrl { get; set; } }

然后在CartController中添加Index方法

public ViewResult Index(string returnUrl) { return View(new CartIndexViewModel { Cart = GetCart(), ReturnUrl = returnUrl }); }

并使用CartIndexViewModel(SportsStore.WebUI.Models)创建强类型视图。我们想在显示cart的content时,一如既往地与程序的其他部分页面一样,所以没有填layout,它会默认地使用_Layout.cshtml文件。

@model SportsStore.WebUI.Models.CartIndexViewModel @{ ViewBag.Title ="Sport Store : Your Cart"; } <h2>Your cart</h2> <table width="90%" align="center"> <thead> <tr> <th align="center">Quantity</th> <th align="left">Item</th> <th align="right">Price</th> <th align="right">Subtotal</th> </tr></thead> <tbody> @foreach(var line in Model.Cart.Lines){ <tr> <td align="center">@line.Quantity</td> <td align="left">@line.Product.Name</td> <td align="right">@line.Product.Price.ToString("c")</td> <td align="right">@((line.Quantity*line.Product.Price).ToString("c"))</td> </tr> } </tbody> <tfoot> <tr> <td colspan="3" align="right">Total:</td> <td align="right"> @Model.Cart.ComputeTotalValue().ToString("c") </td> </tr> </tfoot> </table> <p align-"center"class="actionButtons"> <a href="@Model.ReturnUrl">Continue shopping</a> </p>

它枚举购物车中的行,将每行添加到HTML表,伴随着每行总额和购物车总额。当我们点击继续购物按钮,会回到来时的页面。

4 使用模型绑定

MVC框架使用一个叫做model binding的系统,从来自HTTP查询,创建C#对象,为了将他们作为参数值传递给action方法。这是MVC如何处理表单。框架查看被触发的action方法的参数,并使用一个model binder,得到表单input元素的值,并使用相同的名字,将他们转换为参数的类型。

Model binders可以从有效查询的任何信息创建C#类型。这是MVC框架的中心特性之一。我们要创建一个自定义的模型绑定,来改进CartController类。

我们喜欢使用session状态特性,来存储和管理Cart对象。但是我们确实不喜欢这种方式。它不适合我们其他部分的程序模型,那些基于action方法参数。我们不能在CartController类使用单元测试,除非我们Mock Session,这意味着mocking真个controller类。

为了解决这个问题,我们要创造一个自定义model binder,获得session data中包含的cart对象。MVC框架然后会创建Cart对象,传递他们作为参数给action方法。模型绑定特性是非常强大和灵活的。

4.1 创建自定义Model Binder

我们创建自定义model binder,以实现IModelBinder接口。在SportsStore.WebUI中新建Binders文件,在它里面创建CartModelBinder类。

publicclass CartModelBinder:IModelBinder { privateconststring sessionKey ="Cart"; publicobject BindModel(ControllerContext controllerContext,ModelBindingContext bindingContext) { //get the Cart from the session Cart cart = (Cart)controllerContext.HttpContext.Session[sessionKey]; //create the Cart if there wasn't one in the session data if(cart==null){ cart =new Cart(); controllerContext.HttpContext.Session[sessionKey] = cart; } //return the cart return cart; } }

IModelBinder接口定义了一个方法:BindModel。两个参数用来创建领域模型对象。ControllerContext提供访问controller拥有的所有信息,包括客户端的查询详情。ModelBindingContext给你关于你将要构建的模型对象的信息。

出于这个目的,ControllerContext类是我们感兴趣的。它由HttpContext属性,它可以给我们sesson属性,并设置session data。偶们通过读取session data的key的value,获得Cart,如果它不存在,就创建它。

偶们需要告诉MVC框架,它可以使用CartModelBinder类,创建Cart的实例。在Global.asax的Application_Start中添加

ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder());

现在我们可以将GetCart从CartController中移除,并使用我们的模型绑定。

public ViewResult Index(Cart cart,string returnUrl) { return View(new CartIndexViewModel { Cart = cart, ReturnUrl = returnUrl }); }

我们移除了GetCart方法,并为每个action方法添加了Cart参数。当MVC框架收到请求,AddToCart方法被调用,它开始查找action方法的参数。它查看可用绑定的列表,尝试着找到一个能创建参数类型的实例。我们自定义的绑定,被要求创建一个Cart对象,它使用session状态特性完成工作。在我们的绑定和默认绑定之间,MVC框架会创建一组调用action方法必须的参数。允许我们重构controller。

使用自定义绑定有一些益处。第一,偶们分离了用来创建Cart的逻辑,从Controller。它允许偶们改变我们存储Cart对象的方法,而不需要改变controller。第二,用到Cart对象的任何Controller类,都能简单地将他们声明为action的参数,并改进自定义模型绑定。第三,是最重要的一点,偶们可以对Cartcontroller进行单元测试了,而不需要mock许多ASP.NET管道。

4..2 使用单元测试cart controller

通过创建Cart对象,并将他们传递给action方法,我们可以测试CarController类。需要测试controller的三个不同的方面:

AddToCart方法应该添加被选择的product到用户的cart

在添加product到cart后,需要重定向到Index View

用户返回到分类的url,必须准确地传递给Index action方法

[TestMethod] publicvoid Can_Add_To_Cart() { //Arrange - create the mock repository Mock<IProductRepository> mock =new Mock<IProductRepository>(); mock.Setup(m => m.Products).Returns( new Product[] { new Product{ProductID=1,Name="P1",Category="Apples"} }.AsQueryable()); //Arrange - create a Cart Cart cart =new Cart(); //Arragne - create the controller CartController target =new CartController(mock.Object); //Act - add a product to the cart target.AddToCart(cart, 1, null); //Assert Assert.AreEqual(cart.Lines.Count(), 1); Assert.AreEqual(cart.Lines.ToArray()[0].Product.ProductID, 1); } [TestMethod] publicvoid Adding_Product_To_Cart_Goes_To_Cart_Screen() { //Arrange - create the mock repository Mock<IProductRepository> mock =new Mock<IProductRepository>(); mock.Setup(m => m.Products).Returns( new Product[] { new Product{ProductID=1,Name="P1",Category="Apples"} }.AsQueryable()); //Arrange - create a Cart Cart cart =new Cart(); //Arragne - create the controller CartController target =new CartController(mock.Object); //Act - add a product to the cart RedirectToRouteResult result = target.AddToCart(cart, 2, "myUrl"); //Assert Assert.AreEqual(result.RouteValues["action"],"Index"); Assert.AreEqual(result.RouteValues["returnUrl"],"myUrl"); } [TestMethod] publicvoid Can_View_Cart_Contents() { //Arrange - create a Cart Cart cart =new Cart(); //Arragne - create the controller CartController target =new CartController(null); //Act - call the Index action method CartIndexViewModel result = (CartIndexViewModel)target.Index(cart, "myUrl").ViewData.Model; //Assert Assert.AreEqual(result.Cart,cart); Assert.AreEqual(result.ReturnUrl,"myUrl"); } }

5 完成购物车

添加两个心的特性,第一个是移除商品,第二个是在页面顶部显示商品总数

5.1 从购物车移除商品

我们已经定义并而是了RemoveFromCart action方法,需要把它放到视图,在购物车汇总的每一行添加Remove按钮。

<td align="right">@((line.Quantity*line.Product.Price).ToString("c"))</td> <td> @using(Html.BeginForm("RemoveFromCart","Cart")){ @Html.Hidden("ProductId",line.Product.ProductID) @Html.HiddenFor(x=>x.ReturnUrl) <input class="actionButtons" type="submit" value="Remove"/> } </td>

我们可以使用强类型Html.HiddenFor helper方法,为模型属性ReturnUrl创建一个隐藏域,但是我们需要使用基于字符串的Html.Hidden helper为ProductID域。如果我们写成

@Html.HiddenFor(x => line.Product.ProductID)

helper会渲染一个

name="line.Product.ProductID" type="hidden" value="2"

的 field。field的name不能匹配CartController.RemoveFromCart action放的的参数名,它会防止默认的模型绑定工作,所以MVC框架不能调用这个方法。

public RedirectToRouteResult RemoveFromCart(Cart cart,int productId,string returnUrl) <input id="ProductID" name="ProductID" type="hidden" value="1"/> <input id="ReturnUrl" name="ReturnUrl" type="hidden" value="/Watersports"/>

name与参数名相匹配。

5.2 添加购物车汇总

我们需要把购物车放在界面上。客户可以屏幕上看到购物车中,商品的数量。他们可以看到一个一个新商品进入购物车。

要做到这点,我们需要添加一个控件,汇总购车的contents,被点击后显示购物车的contents。这和导航控件很相似,做一个注入到Razor layout的action。

在CartController中添加

public ViewResult Summary(Cart cart) { return View(cart); }

它仅需要渲染一个视图,提供当前Cart(从我们自定义的模型绑定中获得)作为视图数据。我们需要创建一个局部视图,它会在Summary方法被调用时,在response中被渲染。创建Summary的局部视图,强类型Cart。

@model SportsStore.Domain.Entities.Cart @{ Layout = null; } <div id="cart"> <span class="caption"> <b>Your cart:</b> @Model.Lines.Sum(x=>x.Quantity) item(s), @Model.ComputeTotalValue().ToString("c") </span> @Html.ActionLink("Checkout", "Index", "Cart", new { returnUrl = Request.Url.PathAndQuery },null) </div>

在_Layout.cshtml文件中?:

<div id="header"> @{Html.RenderAction("Summary", "Cart");} <div class="title">SPORTS STORE</div> </div>

使用RenderAction,结合action方法,渲染输出到页面。这是个不错的技术,打碎了程序的功能,使之成为不同的,可以再度重用的块。

6 提交订单

现在,偶们到达了最后一个客户特性,结账的能力和完成订单。接下来,我们会扩展领域模型,支持从用户捕捉购物明细,并添加处理这些细节的特性。

6.1 扩展领域模型

在Entities中添加ShippingDetails类,这个类代表了用户的购物明细。

publicclass ShippingDetails { [Required(ErrorMessage ="Please enter a name")] publicstring Name { get; set; } [Required(ErrorMessage ="Please enter the first address line")] publicstring Line1 { get; set; } publicstring Line2 { get; set; } publicstring Line3 { get; set; } [Required(ErrorMessage ="Please enter a city name")] publicstring State { get; set; } publicstring Zip { get; set; } [Required(ErrorMessage ="Please enter a country name")] publicstring Country { get; set; } publicbool GiftWrap { get; set; } }

使用了System.ComponentModel.DataAnnotations的验证属性。必须添加引用才能使用。ShippingDetails类中没有任何函数,所以我们没有明显的单元测试。

6.2 添加结账处理

我们的目标是用户可以输入他们的购物详情,并提交订单。我们需要添加Checkout now按钮到Views/Cart/Index.cshtml文件。

<p align-"center"class="actionButtons"> <a href="@Model.ReturnUrl">Continue shopping</a> @Html.ActionLink("Checkout now","Checkout") </p>

这个按钮调用了Cart/Checkout,所以要在CartController类中添加Checkout方法。这个方法返回默认视图,并传递一个新的ShippingDetails对象,作为视图模型。创建强类型视图,视图模型为ShippingDetails。

@model SportsStore.Domain.Entities.ShippingDetails @{ ViewBag.Title ="SportsStroe: Checkout"; } <h2>Check out now</h2> Please enter your details, and we'll ship your goods right away! @using(Html.BeginForm()){ <h3>Ship to</h3> <div>Name: @Html.EditorFor(x=>x.Name)</div> <h3>Address</h3> <div>Line 1: @Html.EditorFor(x=>x.Line1)</div> <div>Line 2: @Html.EditorFor(x=>x.Line2)</div> <div>Line 3: @Html.EditorFor(x=>x.Line3)</div> <div>City: @Html.EditorFor(x=>x.City)</div> <div>State: @Html.EditorFor(x=>x.State)</div> <div>Zip: @Html.EditorFor(x=>x.Zip)</div> <div>Country: @Html.EditorFor(x=>x.Country)</div> <h3>Options</h3> <label> @Html.EditorFor(x=>x.GiftWrap) </label> <p align="center"> <input class="actionButtons" type="submit" value="Complete order"/> </p> }

使用Html.EditorFor helper方法,为每个表单域渲染了input元素。这个方式是一个templated view helper。我们让MVC框架画出input元素类型的必须的视图模型属性,而不是明确地使用Html.TextBoxFor指定它。

我们看到模板视图助手,多么只能地为我们的bool属性,渲染了一个checkbox。为string属性渲染了textbox。

我们将来会使用Html.EditorForModel helper方法,它会为ShippingDetails视图模型类的所有属性生成一个label和一个inputs。然而,我们想将name,address区分开来,并且显示在表单的不同区域,所以简单地直接参照每个属性。

6.3 实现Order Processor

我们需要一个组件,提交订单给处理。为了保持MVC模型的原则,我们为这个功能定义一个接口,并写一个它的实现,关联到DI容器和Ninject。

6.3.1 定义接口

在Abstrack文件夹中创建新接口IOrderProcessor。

publicinterface IPrderProcessor { void ProcessOrder(Cart cart, ShippingDetails shippingDetails); }

6.3.2 接口的实现

IOrderProcessor的实现,用来处理订单,发e-mail给管理员。当然,我们简化了销售过程。大多数电子贸易网站,不会简单地将order发e-mail,但是我们不提供处理信用卡或其他形式的支付的支持。我们只想关注MVC,所以经它发e-mail。

在Concrete文件夹中创建EmailOrderProcessor类。这个类使用了.NET框架内建的SMTP支持,来发送e-mail。

为了让事情变得简单,我们定义了EmailSettings类,EmailOrderProcessor的构造器方法需要这个类的实例,它包含.NET e-mail类需要的所有配置。

不要担心没有SMTP可用,如果设置了EmailSetting.WriteAsFile属性为true,e-mail messages会被直接写入FileLocation指定的文件。这个途径必须存在而且可以写入。文件会以.eml扩展。

6.4 注册实现

现在偶们有了IOrderProcessor接口的实现,意味着可以配置它。我们可以使用Ninject创建它的实例。在NinjectControllerFactory类中添加绑定。

privatevoid AddBindings() { ninjectKernel.Bind<IProductRepository>().To<EFProductRepository>(); EmailSettings emailSettings =new EmailSettings { WriteAsFile =bool.Parse(ConfigurationManager.AppSettings["Email.WriteAsFile"] ??"false") }; ninjectKernel.Bind<IOrderProcessor>(). To<EmailOrderProcessor>().WithConstructorArgument("settings", emailSettings); }

我们创建了一个EmailSettings对象,当IOrderProcessor接口被请求创建一个新的实例时,偶们使用Ninject WithConstructorArgument方法将它注入到EmailOrderProcessor的构造器中。我只指定了一个属性,WriteAsFile。它允许我们访问Web.config文件中的程序设置。

<appSettings> <add key="ClientValidationEnabled" value="true"/> <add key="UnobtrusiveJavaScriptEnabled" value="true"/> <add key="Email.WriteAsFile" value="true"/> </appSettings>

6.5 完成Cart Controller

要完成CartController类,我们需要修改构造函数,让它需要一个IOrderProcessor接口的实例,并添加一个新的action方法,处理当用户点击Complete Order按钮时的HTTP表单POST。

private IProductRepository repository; private IOrderProcessor orderProcessor; public CartController(IProductRepository repo,IOrderProcessor proc) { repository = repo; orderProcessor = proc; } [HttpPost] public ViewResult Checkout(Cart cart, ShippingDetails shippingDetails) { if(cart.Lines.Count()==0){ ModelState.AddModelError("", "Sorry,your cart is empty!"); } if (ModelState.IsValid){ orderProcessor.ProcessOrder(cart, shippingDetails); cart.Clear(); return View("Completed"); }else { return View(shippingDetails); } }

Checkout方法使用HttpPost属性装饰,这意味着它会通过POST查询的方式调用。当用户提交表单。再一次,你依赖模型绑定系统,包括ShippingDetails参数(它通过HTTP表单数组自动被创建)和Cart参数(它使用自定义绑定创建)。

这个改变需要我们变更CartController类的单元测试,传递Null为新的构造器参数。

MVC框架会检查ShippingDetails的date annotation属性的验证约束。任何违反的都会通过ModelState属性传递给action方法。我们可以通过检查ModelState.IsValid属性,看看这里有没有问题。注意,如果购物车为空,我们调用Modelstate.AddModelError方法,注册一个错误消息。

6.5.1 订单处理的单元测试

要使得CartController类的单元测试变得完整,需要测试Checkout的重写方法。

[TestMethod] publicvoid 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); } [TestMethod] publicvoid 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 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); } [TestMethod] publicvoid 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.Never()); //Assert - check that the method is returning the Completed vie Assert.AreEqual("Completed", result.ViewName); //Assert - check that we are passing a valid model to the view Assert.AreEqual(true, result.ViewData.ModelState.IsValid); }

测试确保了不能check out使用空购物车。我们检查这点,通过确保mock IOrderProcessor实现的ProcessOrder永远不会被调用。model state被标记为invalid,传递给view。

6.6 显示验证错误

如果用户输入不能通过验证的信息,这个表单域就会高亮,但不显示错误信息。如果用户使用空的购物车结账,我们不让他完成订单,但是它看不到任何错误信息。为了解决这点,我们需要添加验证汇总到Checkout.cshtml视图。

Please enter your details, and we'll ship your goods right away! @using (Html.BeginForm()) { @Html.ValidationSummary() <h3>Ship to</h3>

6.7 显示总结页面

我们要显示一个确认订单已经处理完毕,感谢他们购买。为Checkout方法添加Completed视图。

@{ ViewBag.Title ="SportsStore: Order Submitted"; } <h2>Thanks!</h2> Thanks for placing your order. We'll ship your goods as soon as possible.

7 总结

偶们有一个可以浏览分类和页面的产品分类。一个优雅的购物车,一个简单的结账过程。完好分离的建筑学,以为着偶们可以简单的改变程序任意一部分的功能,而不用担心产生问题或与其他地方不一致。例如,我们可以使用数据库存储订单,并且对它在购物车中,产品分类中,或程序的任何部分,都没有影响。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐