第四话 Asp.Net MVC 3.0【MVC基本工具及单元测试】
前面也有说"控制反转"所谓的依赖注入(Dependency Injection)简称DI。针对它有一款不错的容器,那就是"Ninject",这东西是一个简单易用的东西。话不多说,直接开始吧!
使用Ninject
先用控制台程序玩玩首先定义一个类,接口以及它的实现,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Ninject_Tools { public class Product : Object { public int ProductID { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal Price { get; set; } public string Category { get; set; } } }
下面创建一个接口,具体代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Ninject_Tools { public interface IValueCalculator { decimal ValueProducts(params Product[] products); } }
注:上面的IValueCalculator里面多了一个ValueProducts的方法,用于返回商品的累计值。
接着我们写一个类来实现接口IValueCalculator,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Ninject_Tools { public class LinqValueCalculator : IValueCalculator { public decimal ValueProducts(params Product[] products) { return products.Sum(h => h.Price); } } }
注:这里用LINQ的扩张方法来计算Prducts商品的总价,当然还有其他的办法也可以实现。
接下来我们需要创建一个类来实现依赖注入,具体的代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Ninject_Tools { public class ShoppingCart : Object { private IValueCalculator calcuator; public ShoppingCart(IValueCalculator calcParam) { this.calcuator = calcParam; } public decimal CalculateStockValue() { //计算产品集总和 Product[] products = { new Product(){Name="Huitai",Price=1M}, new Product(){Name="ShuaiShuai",Price=10M}, new Product(){Name="Jack",Price=100M}, new Product(){Name="Cena",Price=200M} }; //计算产品总和 decimal totaValue = this.calcuator.ValueProducts(products); //返回结果 return totaValue; } } }
注:ShoppingCart构造器接受接口IValueCalculator的实现的实例作为参数来构造注入来实现ShoppingCart与LinqValueCalculator(IValueCalculator接口实现)的解耦,这也就是依赖注入里的"构造注入(Constructor Injection)".它们之间的关系如下图1.
图1.
由上面的图可以看出ShoppingCart类和LinqValueCalculator类都依赖于IValueCalcutor,ShoppingCart和LinqValueCalculator没有直接关系,甚至都不知道他的存在。
下面我们就需要添加Ninject到我们的项目里来玩玩,在我们的应用程序里使用管理程序"NuGet"包来引入Ninject的DLL,如下图2.给我们的应用程序来加载进来Ninject.dll.
图2.
加载进来Ninject.dll,然后我们就需要开始使用它了。
然后我们在控制台程序的Mian方法里使用它,具体的代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Ninject; namespace Ninject_Tools { class Program { static void Main(string[] args) { //创建Ninject实例,方便我们下面使用 IKernel ninjectKerenl = new StandardKernel(); //绑定相关的类型和创建的接口 ninjectKerenl.Bind<IValueCalculator>().To<LinqValueCalculator>(); //得到接口的实现 IValueCalculator calcImpl = ninjectKerenl.Get<IValueCalculator>(); //创建实例ShoppingCart实例依赖注入 ShoppingCart cart = new ShoppingCart(calcImpl); //输出 Console.WriteLine("Total:{0:C}", cart.CalculateStockValue()); } } }
好了,现在跑一下我们的程序,可以看到如下图3.的结果。
图3.
小结一下吧!我们开始解耦,然后将IValueCalcutor的实例对象作为参数传递给ShoppingCart的构造器的方式,然后我们通过"依赖注入"容器来处理这个参数,在上面我们用Ninject来实现的,到这里相信大家对NinJect有一点初步的认识。
创建依赖链(Creating Chains of Dependency)
当向Ninject创建一个类型,它检查该类型与其它类型之间的耦合关系,类型和其他类型。如果有额外的依赖,Ninject解决它们并创建新的我们需要的类的实例。
给我们的控制台程序继续添加一个新的接口IDiscountHelper进来,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Ninject_Tools { public interface IDiscountHelper { decimal ApplyDiscount(decimal totalParam); } }
然后在添加一个实现新接口IDiscountHelper的类DefaultDiscountHelper进来,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Ninject_Tools { public class DefaultDiscountHelper : IDiscountHelper { public decimal ApplyDiscount(decimal totalParam) { return (totalParam - (10m / 100m * totalParam)); } } }
然后在我们上面的LinqValueCalculator类里添加依赖,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Ninject_Tools { public class LinqValueCalculator : IValueCalculator { private IDiscountHelper discounter; //LinqValueCalculator构造器接受IDiscountHelper实例对象 public LinqValueCalculator(IDiscountHelper discountParam) { this.discounter = discountParam; } public decimal ValueProducts(params Product[] products) { return this.discounter.ApplyDiscount(products.Sum(h => h.Price)); } } }
然后就是绑定实现接口,具体代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Ninject; namespace Ninject_Tools { class Program { static void Main(string[] args) { //创建Ninject实例,方便我们下面使用 IKernel ninjectKerenl = new StandardKernel(); //绑定相关的类型和创建的接口 ninjectKerenl.Bind<IValueCalculator>().To<LinqValueCalculator>(); ninjectKerenl.Bind<IDiscountHelper>().To<DefaultDiscountHelper>(); //得到接口的实现 IValueCalculator calcImpl = ninjectKerenl.Get<IValueCalculator>(); //创建实例ShoppingCart实例依赖注入 ShoppingCart cart = new ShoppingCart(calcImpl); //输出 Console.WriteLine("Total:{0:C}", cart.CalculateStockValue()); } } }
然后在跑下我们修改后的程序,结果如下图4.
图4.
小结一下吧:代码中Ninject将两个接口分别绑定到对应的实现类中,我们没有改变实现IValueCalculator类的代码。当我们请求IValueCalculator的类型时Ninject知道就实例化LinqValueCalculator对象来返回。但是它会去检查这个类,发现LinqValueCalculator还同时依赖另一个接口,并且这个接口是它能解析的,Ninject创建一个DefaultDiscountHelper的实例注入LinqValueCalculator类的构造器,并返回IValueCalculator类型的对象。Ninject会只用这种方式检查每一个要实例化的类的依赖,不管依赖链有多的复杂。
指定属性和参数值(Specifying Property and Parameter Values)
我们可以配置类提供一些属性或者具体的值传给Ninject,当我们绑定和接口的实现。然后我们对上面的DefaultDiscountHelper类写死的折扣经行修改。
然后我们给DefaultDiscountHelper类添加一条属性,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Ninject_Tools { public class DefaultDiscountHelper : IDiscountHelper { public decimal DiscountSize { get; set; } public decimal ApplyDiscount(decimal totalParam) { return (totalParam - (this.DiscountSize / 100m * totalParam)); } } }
然后在我们绑定实现接口的时候,就可以动态的使用WithPropertyValue来动态的设置我们的折扣力度。代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Ninject; namespace Ninject_Tools { class Program { static void Main(string[] args) { //创建Ninject实例,方便我们下面使用 IKernel ninjectKerenl = new StandardKernel(); //绑定相关的类型和创建的接口 ninjectKerenl.Bind<IValueCalculator>().To<LinqValueCalculator>(); ninjectKerenl.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize",50M); //得到接口的实现 IValueCalculator calcImpl = ninjectKerenl.Get<IValueCalculator>(); //创建实例ShoppingCart实例依赖注入 ShoppingCart cart = new ShoppingCart(calcImpl); //输出 Console.WriteLine("Total:{0:C}", cart.CalculateStockValue()); } } }
添加了属性的项目跑起来看是不是我们输入5折的折扣,运行起来如下图5.
图5.
使用自动绑定(Self-Binding)
一个有用的功能整合到你的代码中完全是Ninject自我绑定,也就是来自Ninject内核被请求的类的地方。这似乎是一个奇怪的事情,但是这意味着我们不用手动的执行DI初始化,空值太程序可以修改如下代码所示:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Ninject; namespace Ninject_Tools { class Program { static void Main(string[] args) { //创建Ninject实例,方便我们下面使用 IKernel ninjectKerenl = new StandardKernel(); //绑定相关的类型和创建的接口 ninjectKerenl.Bind<IValueCalculator>().To<LinqValueCalculator>(); ninjectKerenl.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize", 50M); ////得到接口的实现 //IValueCalculator calcImpl = ninjectKerenl.Get<IValueCalculator>(); ////创建实例ShoppingCart实例依赖注入 //ShoppingCart cart = new ShoppingCart(calcImpl); ////输出 //这里是调用ShoppingCart本身,所以注释掉这行代码程序也可以执行,但是调用具体的类经行自我绑定,代码如下: //ninjectKernel.Bind<ShoppingCart>().ToSelf().WithParameter("<parameterName>", <paramvalue>); ninjectKerenl.Bind<ShoppingCart>().ToSelf(); ShoppingCart cart = ninjectKerenl.Get<ShoppingCart>(); Console.WriteLine("Total:{0:C}", cart.CalculateStockValue()); } } }
运行程序可以看到如下图6.所示的结果
图6.
绑定派生类型(Binding to a Derived Type)
前面一直都在接口绑定的方面(因为接口在MVC里相关性更强),我们也可以使用Ninject绑定具体类。Self-Binding就是这样搞的。那样我们可以使用Ninject绑定一个具体的类到它的派生类,因为前面我们绑定接口到实现它的具体类--实现类继承了该接口,这里就是类继承类罢了。那我们修改一下ShoppingCart类,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Ninject_Tools { public class ShoppingCart : Object { protected IValueCalculator calcuator; protected Product[] products; public ShoppingCart(IValueCalculator calcParam) { this.calcuator = calcParam; //计算产品集总和 Product[] products = { new Product(){Name="Huitai",Price=1M}, new Product(){Name="ShuaiShuai",Price=10M}, new Product(){Name="Jack",Price=100M}, new Product(){Name="Cena",Price=200M} }; } public virtual decimal CalculateStockValue() { //计算产品总和 decimal totaValue = this.calcuator.ValueProducts(products); //返回结果 return totaValue; } } }
写一个LimitShoppingCart类派生自ShoppingCart类:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Ninject_Tools { public class LimitShoppingCart : ShoppingCart { public LimitShoppingCart(IValueCalculator calcParam) : base(calcParam) { } public override decimal CalculateStockValue() { //过滤掉价格超过我们设定价格的商品然后在求和 var filteredProducts = products.Where(h => h.Price < ItemLimit); return this.calcuator.ValueProducts(filteredProducts.ToArray()); } public decimal ItemLimit { get; set; } } }
然后绑定ShoppingCart到它的派生类LimitShoppingCart类,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Ninject; namespace Ninject_Tools { class Program { static void Main(string[] args) { //创建Ninject实例,方便我们下面使用 IKernel ninjectKerenl = new StandardKernel(); //绑定相关的类型和创建的接口 ninjectKerenl.Bind<IValueCalculator>().To<LinqValueCalculator>(); ninjectKerenl.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize", 50M); ////得到接口的实现 //IValueCalculator calcImpl = ninjectKerenl.Get<IValueCalculator>(); ////创建实例ShoppingCart实例依赖注入 //ShoppingCart cart = new ShoppingCart(calcImpl); ////输出 //这里是调用ShoppingCart本身,所以注释掉这行代码程序也可以执行,但是调用具体的类经行自我绑定,代码如下: //ninjectKernel.Bind<ShoppingCart>().ToSelf().WithParameter("<parameterName>", <paramvalue>); //ninjectKerenl.Bind<ShoppingCart>().ToSelf(); ninjectKerenl.Bind<ShoppingCart>().To<LimitShoppingCart>().WithPropertyValue("ItemLimit", 100M); ShoppingCart cart = ninjectKerenl.Get<ShoppingCart>(); Console.WriteLine("Total:{0:C}", cart.CalculateStockValue()); } } }
使用条件绑定(Using Conditional Binding)
我们可对同一个接口会有多重实现或者是对同一个类有多个派生,这时Ninject可以指定不同的条件来说明哪一个应该被使用。比如下面,我们创建一个新的类IterativeValueCalculator实现IValueCalculator,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Ninject_Tools { public class IterativeValueCalculator : IValueCalculator { public decimal ValueProducts(params Product[] products) { decimal totalValue = 0; foreach (Product p in products) { totalValue += p.Price; } return totalValue; } } }
下面是对IValueCalculator的两个不同实现了,可以通过Ninject设置条件来指定哪个显示被应用。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Ninject; namespace Ninject_Tools { class Program { static void Main(string[] args) { //创建Ninject实例,方便我们下面使用 IKernel ninjectKerenl = new StandardKernel(); //绑定相关的类型和创建的接口 ninjectKerenl.Bind<IValueCalculator>().To<LinqValueCalculator>(); //在这里绑定可以使用条件绑定 ninjectKerenl.Bind<IValueCalculator>().To<IterativeValueCalculator>().WhenInjectedInto<ShoppingCart>(); //ninjectKerenl.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize", 50M); ////得到接口的实现 //IValueCalculator calcImpl = ninjectKerenl.Get<IValueCalculator>(); ////创建实例ShoppingCart实例依赖注入 //ShoppingCart cart = new ShoppingCart(calcImpl); ////输出 //这里是调用ShoppingCart本身,所以注释掉这行代码程序也可以执行,但是调用具体的类经行自我绑定,代码如下: //ninjectKernel.Bind<ShoppingCart>().ToSelf().WithParameter("<parameterName>", <paramvalue>); //ninjectKerenl.Bind<ShoppingCart>().ToSelf(); ninjectKerenl.Bind<ShoppingCart>().To<LimitShoppingCart>().WithPropertyValue("ItemLimit", 1000M); ShoppingCart cart = ninjectKerenl.Get<ShoppingCart>(); Console.WriteLine("Total:{0:C}", cart.CalculateStockValue()); } } }
从上面的代码可以看出,绑定指定IterativeValueCalculator类应该被实例化在绑定IValueCalculatorinterface时被当对象的依赖注入。如果我们的条件没有一个合适也没有关系,Ninject会寻找一个对该类或接口的默认的绑定,以至于Ninject会有一个返回的结果。
在Asp.Net MVC 中使用Ninject及运用单元测试
前面是都是控制台程序里面玩Ninject,在MVC玩Ninject可就没那么简单了,比起来就稍微有点复杂了。开始是我们需要创建一个类,它的派生自System.Web.Mvc.DefaultControllerFactory。该类MVC依赖默认情况下创建控制器类的实例。.Net有好多单元测试包,其中又由好多包开放源代码,但愿测试工具最受欢迎那可能就是NUnit。我们就玩玩怎么创建但愿测试和填充测试。
首先先看看在Asp.Net MVC 中如何使用Ninject,开始需要创建一个类,让他继承DefaultControllerFactory ,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using Ninject; using System.Web.Routing; namespace MVC_Tools.Models { public class NinjectControllerFactory : DefaultControllerFactory { private IKernel ninjectKernel; public NinjectControllerFactory() { this.ninjectKernel = new StandardKernel(); AddBindings(); } protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return controllerType == null ? null : (IController)ninjectKernel.Get(controllerType); } private void AddBindings() { //把额外的东西绑定到这里 this.ninjectKernel.Bind<IProductRepository>().To<FakeProductRepository>(); } } }
这个类创建一个Ninject内核,通过GetControllerInstance方法为控制器类的请求服务,这个方法在需要一个控制器对象的时候被MVC框架调用。我们不需要显式地使用Ninject绑定控制器类。我们可以依靠默认的self-binding(自我绑定)特性,因为controllersare具体类派生自 System.Web.Mvc.Controller。AddBindings()方法允许我们绑定Repositories和其他需要保持松耦合的组件。我们也可以使用这个方法绑定需要额外的构造器参数或属性参数的controller classes。
一旦我们创建这个类,我们必须注册它与MVC框架,我们需要在Global.asax文件测试它,具体代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; using MVC_Tools.Models; namespace MVC_Tools { // 注意: 有关启用 IIS6 或 IIS7 经典模式的说明, // 请访问 http://go.microsoft.com/?LinkId=9394801 public class MvcApplication : System.Web.HttpApplication { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); } public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Default", // 路由名称 "{controller}/{action}/{id}", // 带有参数的 URL new { controller = "Home", action = "Index", id = UrlParameter.Optional } // 参数默认值 ); } protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory()); } } }
现在MVC框架将使用我们的NinjectControllerFactoryto获得实例的控制器类,Ninject会处理迪向控制器对象自动。有关这东西就先了解到这里吧!
下面看我们的单元测试,弄一个控制台程序来搞吧!
创建相关的Product类,IPriceReducer,IProductRepository接口如下面:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Text { public class Product : Object { public int ProductID { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal Price { get; set; } public string Category { set; get; } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Text { public interface IPriceReducer { void ReducePrices(decimal priceReduction); } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Text { public interface IProductRepository { IEnumerable<Product> GetProducts(); void UpdateProduct(Product product); } }
IProductRepository定义了一个存储库,我们通过将获取和更新Product对象。IPriceReducer定义了一个具体的降价方法,针对所有的Products。我们的目标就是实现这些接口并需要遵循下面的情况:
- 所有的Product都应该降价
- 总共的价格必须等于所有的Product数量乘以降价数额的乘积
- 降价后的Products必须大于1美元
- Repository的UpdateProduct方法应该被每一个Product商品调用到
然后我们添加一个实现接口IProductRepository的类FakeRepository,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Text { public class FakeRepository : IProductRepository { private Product[] products = { new Product() { Name = "Kayak", Price = 275M}, new Product() { Name = "Lifejacket", Price = 48.95M}, new Product() { Name = "Soccer ball", Price = 19.50M}, new Product() { Name = "Stadium", Price = 79500M} }; public IEnumerable<Product> GetProducts() { return products; } public void UpdateProduct(Product productParam) { foreach (Product p in products .Where(e => e.Name == productParam.Name) .Select(e => e)) { p.Price = productParam.Price; } UpdateProductCallCount++; } public int UpdateProductCallCount { get; set; } public decimal GetTotalValue() { return products.Sum(e => e.Price); } } }
还需要添加一个类MyPriceReducer去实现IPriceReducer接口,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Text { public class MyPriceReducer : IPriceReducer { private IProductRepository repository; public MyPriceReducer(IProductRepository repo) { repository = repo; } public void ReducePrices(decimal priceReduction) { throw new NotImplementedException(); } } }
开始我们的单元测试吧!我们要按照TDD模式和编写单元测试代码之前编写应用程序,点击MyPriceReducer类里的ReducePrices方法,然后选择"创建单元测试",如下图6.
图6.当选择"创建单元测试"会出现如下图7.所示的弹出框,
图7.由于单元测试是一个单独的项目,所以我们给他取名"Test_Project",,创建完成后会新建,一个单元测试的项目加载进来,并生成了一个测试类MyPriceReducerTest,它里面包含一些属性和方法让供我们使用,具体生成代码如下:
using Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; namespace Test_Project { /// <summary> ///这是 MyPriceReducerTest 的测试类,旨在 ///包含所有 MyPriceReducerTest 单元测试 ///</summary> [TestClass()] public class MyPriceReducerTest { private TestContext testContextInstance; /// <summary> ///获取或设置测试上下文,上下文提供 ///有关当前测试运行及其功能的信息。 ///</summary> public TestContext TestContext { get { return testContextInstance; } set { testContextInstance = value; } } #region 附加测试特性 // //编写测试时,还可使用以下特性: // //使用 ClassInitialize 在运行类中的第一个测试前先运行代码 //[ClassInitialize()] //public static void MyClassInitialize(TestContext testContext) //{ //} // //使用 ClassCleanup 在运行完类中的所有测试后再运行代码 //[ClassCleanup()] //public static void MyClassCleanup() //{ //} // //使用 TestInitialize 在运行每个测试前先运行代码 //[TestInitialize()] //public void MyTestInitialize() //{ //} // //使用 TestCleanup 在运行完每个测试后运行代码 //[TestCleanup()] //public void MyTestCleanup() //{ //} // #endregion /// <summary> ///ReducePrices 的测试 ///</summary> [TestMethod()] public void ReducePricesTest() { IProductRepository repo = null; // TODO: 初始化为适当的值 MyPriceReducer target = new MyPriceReducer(repo); // TODO: 初始化为适当的值 Decimal priceReduction = new Decimal(); // TODO: 初始化为适当的值 target.ReducePrices(priceReduction); Assert.Inconclusive("无法验证不返回值的方法。"); } } }
但是对我们来说最重要莫过于项目了,所以我们将代码修改成下面的形式:
using Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Linq; using System.Collections; using System.Collections.Generic; namespace Test_Project { [TestClass] public class MyPriceReducerTest { [TestMethod] public void All_Prices_Are_Changed() { FakeRepository repo = new FakeRepository(); decimal reductionAmount = 10; IEnumerable<decimal> prices = repo.GetProducts().Select(h => h.Price); decimal[] initialPrices = prices.ToArray(); MyPriceReducer target = new MyPriceReducer(repo); target.ReducePrices(reductionAmount); //逐一比较开始和降价后的价格,如果相等说明降价失败 prices.Zip(initialPrices, (p1, p2) => { if (p1 == p2) { Assert.Fail(); } return p1; }); } } }
运行单元测试的的代码,结果如下图8.所示
图8.因为我们没有去实现ReducePrices,单元测试不能通过。有很多不同的方式来构建单元测试年代。常见的是有一个方法是测试所需的所有功能的条件。但是我们更喜欢创建很多小的单元测试,每个单元测试只关注应用程序的一个方面。当让给单元测试方法名字的命名规则根据自己的编程风格自己定义,只要符合命名规范即可。把测试的全部功能分散到一个一个的小的测试方法里经行,通过这种方式不断的完善我们的驱动测试开发(TDD).如下面的代码:
using Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Linq; using System.Collections; using System.Collections.Generic; namespace Test_Project { [TestClass] public class MyPriceReducerTest { [TestMethod] public void All_Prices_Are_Changed() { FakeRepository repo = new FakeRepository(); decimal reductionAmount = 10; IEnumerable<decimal> prices = repo.GetProducts().Select(h => h.Price); decimal[] initialPrices = prices.ToArray(); MyPriceReducer target = new MyPriceReducer(repo); target.ReducePrices(reductionAmount); //逐一比较开始和降价后的价格,如果相等说明降价失败 prices.Zip(initialPrices, (p1, p2) => { if (p1 == p2) { Assert.Fail(); } return p1; }); } [TestMethod] public void Correct_Total_Reduction_Amount() { FakeRepository repo = new FakeRepository(); decimal reductionAmount = 10; decimal initialToatal = repo.GetTotalValue(); MyPriceReducer tatget = new MyPriceReducer(repo); tatget.ReducePrices(reductionAmount); Assert.AreEqual(repo.GetTotalValue(), (initialToatal - (repo.GetProducts().Count() * reductionAmount))); } public void No_Price_Less_Than_One_Dollar() { FakeRepository repo = new FakeRepository(); decimal reductionAmount = decimal.MaxValue; MyPriceReducer target = new MyPriceReducer(repo); target.ReducePrices(reductionAmount); foreach (Product prod in repo.GetProducts()) { Assert.IsTrue(prod.Price >= 1); } } } }
然后运行我们的单元测试代码,结果如图9.
图9.其实上面的单元的测试代码里也使用了简单依赖注入(构造器注入),上面的每一个测试方法就是针对一个功能来测试,貌似测试没有通过,那是因为我们的ReducePrices方法没有实现,现在我们就是去搞下他吧!代码具体如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Text { public class MyPriceReducer:IPriceReducer { private IProductRepository repository; public MyPriceReducer(IProductRepository repo) { repository = repo; } public void ReducePrices(decimal priceReduction) { //throw new NotImplementedException(); //实现功能 foreach (var item in repository.GetProducts()) { item.Price = Math.Max(item.Price - priceReduction, 1); repository.UpdateProduct(item); } } } }
搞完之后,在来跑下我们的单元测试的模块,运行结果如下图10.
图10.呵呵!其实这些基本都是VS里面的东西,差不多随便看看点点,写写就会明白的吧!好了就先分享这么一点东西吧!大家共同学习进步,文章里那里要是有错误还是那里描述错误的,请路过的朋友们,前辈们多多指导,批评。这样我们才能更好的学习。