在之前的文章《单元测试培训系列:(一)单元测试概念以及必要性》中最后一段有提到,单元测试其实是完全为了测试先行,测试驱动准备的,并简单阐述了一下实施的流程,很多朋友对此很感兴趣,希望能更深入了解具体是如何实施的。

      隔离,是单元测试中最重要的概念。一个被单元测试的方法,需要与所有依赖项进行隔离。而依赖项包括了环境的依赖项(I/O,网络,数据库,系统时间等)以及外部类和方法的依赖。因此,隔离性保障了单元测试是最小粒度的测试。

      但隔离也导致了单元测试的局限性,主要是以下两个方面:

      1. 通过单元测试是不能检测到一个方法修改后对系统的影响范围的。

         单元测试因为隔离了对其他方法的依赖,因此当一个方法因为重构或者修改BUG等原因进行了改变时,运行已有的单元测试只能检测到这个被修改的方法本身是否依然符合以前预期的目标;而对修改这个方法对整个系统有任何影响,是完全无法通过运行单元测试得知的!!

         很多朋友一直错把集成测试和单元测试混为一谈,认为单元测试能够检测到一个方法改变后对整个系统哪些部分造成了影响,这种想法很显然是错误的:因为单元测试的每个测试方法都把被测试的方法和其他方法、外部环境隔离开来,每一个被测试的方法都不依赖其他方法的具体实现,因此,即便其他类或方法的实现发生了改变,只要接口依然保持原样,对当前的单元测试是都不会产生任何影响的!!

      2. 单元测试对于需求变更基本没有太大作用。

         需求发生变更,首先要改变的就是单元测试!因为单元测试的关注点是每个方法的进出项(输入值和输出值)是否满足期望值。当发生需求变更时,意味着相关方法的预期值发生改变,此前相关的单元测试不再具有价值,需要重新编写。

      因为以上两个原因,对一个已有系统的代码追加单元测试的价值也变得非常鸡肋了。对于一个已有系统追加单元测试之后,单元测试唯一能在某个方法的内部实现进行重构的时候起到作用(例如修改BUG和算法优化,并且是在不修改当前调用关系以及相关接口的前提下)

 

      说了这么多局限性,估计很打击大家积极性,难道单元测试就那么一无是处么?非也,而是单元测试的使用场景没对。

      测试先行还是测试驱动也好,都是目标导向方法论的具体实践,其目的都是在编写代码之前先行确定好要编写代码的目标以及校验方式。而再来看单元测试,单元测试只关注一个单独方法本身的功能以及这个方法的进出项是否满足期望值。这根本上就是和测试先行的目标是完全吻合的。可以说单元测试是测试先行的具体实施办法。

      有了这个基调,我们再来看如何把单元测试按照测试先行的指导来进行实施,因为该篇文章的关注点还是单元测试,就不过多探讨如何实施测试先行或者测试驱动。

      我们直接从拿到一个具体的业务功能模块开始。

一、设计简要的类图以及类之间关系

      我们首先简要的为该模块设计出基本的类图和类之间的关系(虽然有些敏捷方法对测试先行的要求是不做设计,只做测试用例,然后再编写测试代码和实现代码,但在这里个人还是按照先设计功能的类结构方式)

      在这个阶段,基本只需要定义好几个类,而类里面的成员则没有必要在一开始就全部设计出来。(推荐使用Visual Studio里的项目选项View Class Diagram进行代码和类设计的同步进行)
      来看下面这个例子:一个订单系统,订单明细里面的每条订单的产品分为服装和数码产品类,而服装的价格来源是Vancl,数码产品的价格来源是Newegg. 首先这个订单系统需要一个订单统计总价格的功能。

结合测试驱动TDD实施单元测试UnitTest

      如上图所示,定义了主要的类以及彼此之间的关系, 除了一些数据属性外,还没有定义这几个类的方法。
      ProductOrder类, 即定单类,需要实现方法Count,包含一个IList<BaseOrderDetail>类型的属性OrderDetails。

      BaseOrderDetail类,抽象类,包含ProductID和Amount属性以及一个IPriceProvide类型的属性PriceProvider,以及一个Count方法,即每条明细合计自己的总价。

      ClothingOrderDetail类,BaseOrderDetail的子类,即服装类的订单明细,该类的PriceProvider属性应该为VanclProvider类的实例。

      DigialOrderDetail类,BaseOrderDetail的子类,即数码类的订单明细,该类的PriceProvider属性应该为NeweggProvider类的实例。

      IPriceProvide接口,该接口定义提供了从第三方获取产品价格的查询方法QueryPriceByProductID。

      VanclProvider类,实现了IPriceProvide接口,提供Vancl的产品价格查询。

      NeweggProvider类,实现了IPriceProvide接口,提供Newegg的产品价格查询。

二、为某一个类上的某一个公开方法编写单元测试

      当类以及类之间关系设计好之后,就开始根据业务功能的需要,逐步设计类的成员以及方法以实现这个类的功能。而在设计一个类的方法时,则是根据业务需求的要求来设计的,因此在编写一个类中的公开方法时,对这个类需要达成什么样的效果,应该是非常明确的。在明确了目标之后,其实编写单元测试就已经可以实现了,尽管现在实现代码还根本不存在。根据上面的例子,我们首先来编写ProductOrder类的Count方法的单元测试,此时,Count方法是没有真正实现的,甚至Count方法都还不存在(在单元测试代码中使用dynamitic关键字调用不存在的Count方法)。

      ProductOrder类的Count方法,其实是统计所有单据中包含的所有货物的价格,因此可以分析得知Count方法依赖于BaseOrderDetail类的Count方法,并且属性OrderDetails会包含多个BaseOrderDetail类,这意味着在编写单元测试时,我们需要把这些BaseOrderDetail类都使用Mock对象代替。

/// <summary> /// A test for the method Count ///</summary> [TestMethod()] public void TestCount() { // Arrange dynamic target = new ProductOrder(); decimal expected = 8748.55m; // Mock a jacket order detail and set return value of the method Count(), no matter real price and amount. Mock<BaseOrderDetail> mockJacketOrderDetail = new Mock<BaseOrderDetail>(); mockJacketOrderDetail.Setup(e => e.Count()).Returns(350.55m); // Mock a iPAD order detail and set return value of the method Count(), no matter real price and amount. Mock<BaseOrderDetail> mockiPadOrderDetail = new Mock<BaseOrderDetail>(); mockiPadOrderDetail.Setup(e => e.Count()).Returns(3499.00m); // Mock a iPAD order detail and set return value of the method Count(), no matter real price and amount. Mock<BaseOrderDetail> mockNotebookDetail = new Mock<BaseOrderDetail>(); mockNotebookDetail.Setup(e => e.Count()).Returns(4899.00m); target.OrderDetails = new List<BaseOrderDetail>(); target.OrderDetails.Add(mockJacketOrderDetail.Object); target.OrderDetails.Add(mockiPadOrderDetail.Object); target.OrderDetails.Add(mockNotebookDetail.Object); // Acction decimal actual = target.Count(); // Assert Assert.AreEqual(actual, expected); }

相关文章: