编写高质量的代码---单元测试Nunit+NCover

来源:互联网 发布:王宝强 熊乃瑾 知乎 编辑:程序博客网 时间:2024/04/28 18:21

编写高质量的代码---单元测试                                                      Nunit+NCover                                                                                                 

一、           前言

测试,对于软件人员来说应该不会陌生,尤其是测试人员,因为在每个项目或产品在由开发人员作完编码后都会有相关的测试环节。当然小公司也许测试环节比较弱,而大公司则对测试非常重视,微软的开发团队中测试人员和开发人员的比例达到了31,足见测试作为保证软件正确性检查的关键环节是多么的重要。当然,对于这些功能测试,集成测试等都是面向测试人员的,开发人员只需要将编写好的程序交由测试人员,然后自己根据测试结果进行Debug。我们有没有办法在开发人员这一级就实现很好的质量保证呢,这样在我们提交到测试人员的程序就会包含尽量少的BUG,答案就是使用单元测试。当然,现在的单元测试工具也很多,今天我要讲的是一款在.net下比较优秀的开源的测试工具——NUnit 及其辅助工具 NCover。下载地址是:http://www.NUnit.com   http://www.NCover.com

二、           单元测试的必要性

很多编程人员都遇到这样一个问题,就是当自己编写完代码,有经验的朋友也许会凭自己的经验和理解也反复手工测试了一通,然后拍拍胸脯说:“OK,没问题!”,然后交由测试人员。但经过测试人员一折腾,可能马上就揪出了一大堆Bug。然后,开发人员就疑惑了,为什么我还有这么多问题没有考虑到呢?当然,也许经验老到的开发人员出的问题会少一些,但是,对于缺乏经验的开发人员难道就没有办法保证提交高质量的代码吗?其实不是,通过自动化的测试工具完全可以弥补这些不足。

现在有一种说法叫测试驱动(Test-Driven)的开发方式,认真做好单元测试的同时,其实也是在迫使你优化自己的代码结构和逻辑。

当然,单元测试的好处还有很多,他可以让你对自己的代码更加自信,使用它,你会喜欢上它的,让我们现在就开始吧!

三、           单元测试的相关技术

本文章我们主要是讲解如何通过NUnit测试代码逻辑和单元测试伴侣NCover查看代码的运行覆盖率,从而保证完整地进行测试。

四、           灵活的单元测试框架---NUnit

在讲解实际的例子之前我们还是先来学习一下单元测试的必备知识吧。

(一) NUnit的安装

安装NUnit后,我们会在安装目录下发现一个Doc目录(存放NUuit的帮助文件),一个Src目录(存放当前版本NUnit的源文件,有兴趣的朋友可以作为学习参考),还有一个是bin目录,bin目录中存放了NUnit的控制台程序和GUI程序,我们可以通过两种方式来使用NUnitNUnit的控制台程序主要用于自动化的测试,协同自动化的编译工具一同使用,如NAnt。而NUnitGUI程序则通过可视化的界面提供给用户测试。还可以安装集成到VS.NetNUnit测试插件(下载地址:http://mutsntdesign.co.uk/nunit-addin/ ),方便在开发环境中进行测试工作。本文主要介绍前两种方式。笔者当前的Nunit版本2.2

(二) NUnit的语法

进行NUnit单元测试时需要编写相应的测试代码,代码也是使用标准的C#语法,并可以使用NUnit框架提供的一系列基础的功能。在编写测试代码前请保证你正确的安装了NUnit,并在项目中引用了NUnit.Framework,并在代码文件中引用了NUnit.Framework命名空间。

下面是开始前的几条建议:

l         建议对每个重要的、复杂的、易出错的、包含重要业务逻辑的方法进行单元测试,测试代码需要有权访问到产品代码(被测代码);

l         测试代码可以和产品在同一个项目中也可以在不同的项目中,视具体情况而定;

正常的测试流程:

l         准备测试所需要的各种条件(创建所需要的对象,分配必要的资源等)

l         调用要测试的方法

l         验证被测试方法的行为和期望是否一致

l         完成后清理各种资源

 

 

 

 

先来看一个简单的产品代码和测试代码

产品代码:

using System;

 

 

 

 

namespace NUnitDemo

{

  /// <summary>

  /// Class1 的摘要说明。

  /// </summary>

  public class Class1

  {

       public Class1()

       {            

       }

 

 

 

 

       //演示处理结果的正确性和异常的处理

       public  int GetMaxItem(int[] ints)

       {

            int Max = ints[0];

            foreach(int intItem in ints)

            {

                if(intItem > Max)

                {

                     Max = intItem;

                }

            }

            return Max;

       }

  }

}

测试代码:

using System;

 

 

 

 

using NUnit.Framework;

 

 

 

 

namespace NUnitDemo

{

     class TargetClass

     {}

     /// <summary>

     /// Class1_Test 的摘要说明。

     /// </summary>

     [TestFixture]

     public class Class1_Test

     {

         public Class1_Test()

         {

             

         }

 

 

 

 

         [Category("simple")]

         [Test]

         public void TestGetMax()

         {

             Class1 Cls1 = new TargetClass();

              int[] Ints = new int[10]{1,2,3,4,5,6,7,8,9,10};

              Assert.AreEqual(10,Cls1.GetMaxItem(Ints));

 

 

 

 

              Ints = new int[10]{1,2,3,4,5,6,15,8,9,10};

              Assert.AreEqual(15,Cls1.GetMaxItem(Ints));

         }

     }

}

代码中,上面是一个实现了获取整数树组中最大值的完整的产品代码程序,下面是该产品代码的测试程序,我们可以看到测试代码也是标准的C#代码,不过使用了一些NUnit    框架特有的方法和属性。

测试中断言测试的主要组成,其中大多数都是包含在Assert类中的静态方法。下面我们来一一介绍。

l          AreEquals

语法Assert.AreEqual(expected,actual[,string Message])

描述:这种断言应该是使用最多的一种,expected是期望得到的值,actual是被测代码产生的值,Message是可选的,如果提供该参数,则在错误发生时,会报告该消息。

l          IsNull

语法Assert.IsNull(object [,string message])

描述:验证一个对象是否为null,对象不是null则验证失败, message可选。

l          AreSame

语法Assert.AreSame(excepted actual[,string message])

描述:验证exceptedactual是否引用的是同一个,message可选。

l          IsTrue

语法Assert.IsTrue(bool condition [,string message])

描述:验证一个二元条件是否为真,message可选。

l          IsFail

语法Assert.IsFail([string message])

描述:该断言回使测试立即失败,message可选,常用于验证某个不应运行到达的地方。

l          自定义断言

虽然NUnit提供的大部分断言都已经能够满足日常测试需要,但如果遇到了特殊的测试要求就需要自己自定义测试断言。自定义断言由自己实现一个公共的测试方法的逻辑,然后在测试时调用。

测试中也常通过属性的方式来标记代码块的作用,下面介绍常用属性。

l          [TestFixture]

用于标记一个测试类,该测试类可以包含多个测试方法,

l          [Suit]

用于将多个TestFixture组织在一起,任何一个测试类都可以包含一个用[Suit]标记的静态方法,该方法返回一个TestSuit,该TestSuitTestFixture的集合(注意:使用TestSuit类时请添加NUnit.Core;的引用)

l          [Test]

标记一个测试方法

l          [Category("name")]

l          用于把不同的测试方法进行分类管理,使用相同[Category("name")]标记的Test可以一起运行,弥补了[Suit]只能对TestFixture分类的不足

l          [SetUp]

标记用于在每个测试方法开始前需要执行的方法,如申请某些资源

l          [TearDown]

标记用于在每个测试方法结束后需要执行的方法,如释放某些资源

l          [TestFixtureSetUp]

标记用于在每个测试类开始前需要执行的方法,如申请某些资源

l          [TestFixtureTearDown]

标记用于在每个测试类开始前需要执行的方法,如申请某些资源

五、       使用NCover协同工作

介绍了那么多NUnit方面的知识,现在来了解一下如何使用NCover,看看它如何来为NUnit增色。

我们都知道在测试一个程序的时候,最苦恼的莫过于自己不能想到所有的可能情况,往往会漏掉很多的重要的Case,致使前期无法发现的错误遗留到后面,为后面的编程和测试工作带来很大的负面效应。当然这些情况我们是可以尽量避免的,利用自动化的代码覆盖工具更能在这方面游刃有余。

安装完NCover后可以在安装目录下发现

NCover是一个命令行工具,用法为:

Usage: NCover /c <command line> [/a <assembly list>]

 

/c 运行需要检测的应用的命令行.
/a 需要检测的程序集的列表.  "MyAssembly1;MyAssembly2"
/v 允许详细的日志信息,该命令行非常适合于测试,不过会使得coverage变的非常大

NCover会在检测完代码运行后生成3个文件

  • Coverage.log – 该文件主要是记录覆盖检测期间产生的事件和消息日志,还有错误日 志,当然,如果使用verbose logging,则会包括更详细的中间语言相关的信息

  • Coverage.xml – NCover的分析输出文件. 例子如下

  • Coverage.xsl - Coverage.xml的转换文件,便于将Coverage.xml转换成友好的表     格形式显示

转换后的Coverage文件

Visit Count

Line

Column

End Line

End Column

Document

1

48

13

48

58

C:/Dev/Utilities/ncover/NCoverTest/NCoverTest.cs

1

49

13

49

22

C:/Dev/Utilities/ncover/NCoverTest/NCoverTest.cs

1

50

17

50

24

C:/Dev/Utilities/ncover/NCoverTest/NCoverTest.cs

0

51

13

51

48

C:/Dev/Utilities/ncover/NCoverTest/NCoverTest.cs

0

52

9

52

10

C:/Dev/Utilities/ncover/NCoverTest/NCoverTest.cs

如上所示,我们可以很方便的得知哪些文件,哪些行,那些列的代码被访问了多少次,这样就能很有针对性的对当前产品代码编写测试代码,测试是每个代码路径都能测试到位。

(题外话:笔者认为如果能和VSNetIDE集成的话,从而能在代码中很直观的看出哪些代码执行了多少次,哪些没有执行,并以不同的颜色来区分,则会更友好。呵呵,不过这些大家不用担心了,盖茨和他的那帮兄弟们都早就为我们想到了,在VS2005里面就集成了单元测试和代码覆盖工具,非常的强大。不过今天是讨论NCover,还是多讲点NCover的优点吧!)

如我们要测试D:/DemoAppFolder/NUnitDemo/TestPrj/bin/Debug下的TestPrj.exe

则可以使用如下命令(请确认在环境变量中添加了NCover的安装目录)

此时

表示成功,并讲结果存在了Coverage.xml中,可以通过IE等工具进行查看,在Coverage.txt中保存了更详细的日志信息。可以将这些信息作为编写测试方法的依据。

      由于NUnit是通过反射的方式对测试方法进行调用,所以说对于测试方法是由NUnit框架驱动运行的,所以在使用NCover中进行代码覆盖测试的时候应该输入NUnit命令行,但是NCover/c中不支持该命令行,如:NUnit-console testprj.exe,所以替代的解决方案是把复杂的命令行写在批处理文件中,这样将该.bat文件做为NCover/c中的参数就OK

六、       单元测试高级主题

(一) 如何组织测试代码

如何有效地组织单元测试代码,这个一直是开发人员比较关心的问题。

在小型项目中,也许你并不在意测试代码放在哪,也许放在产品代码的工程中会更省事,只要能区分就行;在大型项目中就不能这样了,应该很好地组织自己的代码,最好建议建立在单独的工程中,作统一管理。但无论是使用哪种方式,有一点是必须的,就是你的测试代码必需能访问到被测的产品代码。如果在同一个工程中的测试代码可以测试protectedinternal修饰的方法;但如果是外部程序集则需要要么编写一个继承产品类的新类作为测试目标,要么就干脆将要测试的代码Open开来,申明为public,测试完后在改过来,但这种方式也许会在改的同时引入错误;再者,可以专门写个公用方法来调用需要测试私有方法,达到测试目的,等等,不管怎样,需要尽量保持测试的结构清晰,有条理。

同时,就个人的感受吧,还是给点建议。

l         不能盲目的频繁测试,这样会影响正常的编码,同时也削弱了测试的意义;

l         最好在重新编译程序的时候,自动运行编译成功后的测试脚本,既省事,又能及时发现不足;

l         当然能把单元测试和一些自动化构建工具,如NAnt协同工作能达到更好的效果,在下面主题中,我将介绍这方面的内容

(二) 真实环境的替身----MOCK

MOCK---仿冒品,仿制品,顾名思义就是替身,在NUnit里面也是需要替身的。

有没有遇到过这样的情况,当你在编写完一段代码的时候,你需要对这段代码进行测试,但糟糕的是要运行这段代码需要很多的外部支持,如需要日志组件的支持,需要数据库连接的支持等,我真的需要具有所有这些资源才能正常测试吗?答案是否定的,这个时候我们就可以使用替身来模拟这些外部的测试环境,当然这些MOCK我们可以自己写,也可以使用现在网上比较多的免费的MOCK框架,提供了非常多的MOCK对象供使用。其中一款比较优秀的是DotNetMock,可以从http://www.mockobjects.org进行下载。框架中包含了许多日常开发中单元测试会用到的MOCK,同时还提供了动态MOCK(下面会进行讲解)。

那么,在什么情况下需要使用MOCK对象呢?

l         真实对象的行为不可确定(也就是说真实对象没有一个可以预见的行为)。

l         真实对象很难被创建(当我门在进行远程访问方面的开发的时候,不一定时时都有创建远程对象的条件)

l         真实对象很难触发(如网络错误)

l         真实对象令程序的运行速度很慢(如果某个对象的执行会耗用很多的硬件资源的话,将会这一点)

l         真实对象是一个UI或着包含一个UI

l         测试需要询问真实对象他是如何被调用的(例如:测试可能需要验证某个回调函数是否被调用了)

真实对象并不存在(在协作开发中会出现这种情况,如果你需要测试的一个方法依赖的真实对象别人还没有完成,这时就会使用接口实现一个MOCK来测试)

//日志接口

using System;

public interface ILogger {

  void SetName(String name);

  void Log(String msg);

//实现了日志接口的MOCK

using System;

using DotNetMock;

 

 

 

 

public class MockLogger2 : MockObject, ILogger {

 

 

 

 

  private ExpectationValue _name =

    new ExpectationValue("name");

 

 

 

 

  // Mock 对象的接口

 

 

 

 

  public String ExpectedName {

    set { _name.Expected = value; }

  }

 

 

 

 

  // 实现ILog对象的接口

  public void SetName(String name) {

     _name.Actual = name;

  }

 

 

 

 

  public void Log(String msg) {

  }

//使用日志接口的产品代码

using System;

using System.Data;

using System.Data.SqlClient;

 

 

 

 

public class AccessController1 {

  private ILogger       logger;

  private String        resource;

  private IDbConnection conn;

 

  public static readonly String CHECK_SQL =

    "select count(*) from access where " +

    "user=@user and password=@password " +

    "and resource=@resource";

 

 

 

 

  public AccessController1(String resource,

                           ILogger logger,

                           IDbConnection conn) {

    this.logger   = logger;

    this.resource = resource;

    this.conn     = conn;

    logger.SetName("AccessControl");

  }

 

 

 

 

  public bool CanAccess(String user, String password) {

    logger.Log("Checking access for " + user +

      " to " + resource);

 

 

 

 

    if (password == null || password.Length == 0) {

      logger.Log("Missing password. Access denied");

      return false;

    }

 

 

 

 

    IDbCommand cmd = conn.CreateCommand();

    cmd.CommandText = CHECK_SQL;

    cmd.Parameters.Add(

      new SqlParameter("@user",     user));

    cmd.Parameters.Add(

      new SqlParameter("@password", password));

    cmd.Parameters.Add(

      new SqlParameter("@resource", resource));

    IDataReader rdr = cmd.ExecuteReader();

   

    int rows = 0;

 

 

 

 

    if (rdr.Read())

      rows = rdr.GetInt32(0);

 

 

 

 

    cmd.Dispose();

 

 

 

 

    if (rows == 1) {

      logger.Log("Access granted");

      return true;

    }

    else {

      logger.Log("Access denied");

      return false;

    }

  }

}

//测试代码

using System;

using NUnit.Framework;

 

 

 

 

[TestFixture]

public class TestAccessController {

 

 

 

 

  [Test]

  public void MissingPassword1() {

    MockLogger1 logger = new MockLogger1();

    AccessController access =

      new AccessController("secrets", logger);

    Assert.IsFalse(access.CanAccess("dave", null));

    Assert.IsFalse(access.CanAccess("dave", ""));

  }

 

 

 

 

 

 

 

 

  [Test]

  public void MissingPassword2() {

    MockLogger2 logger = new MockLogger2();

    logger.ExpectedName = "AccessControl";

    AccessController access =

      new AccessController("secrets", logger);

    Assert.IsFalse(access.CanAccess("dave", null));

    logger.Verify();

  }

 

 

 

 

 

 

 

 

  [Test]

  public void MissingPassword3() {

    MockLogger3 logger = new MockLogger3();

    logger.ExpectedName = "AccessControl";

    logger.AddExpectedMsg(

      "Checking access for dave to secrets");

    logger.AddExpectedMsg(

      "Missing password. Access denied");

    AccessController access =

      new AccessController("secrets", logger);

    Assert.IsFalse(access.CanAccess("dave", null));

    logger.Verify();

  }

 

 

 

 

 

 

 

 

}

当然对于MOCK的使用也是一个很大的主题,在后续随着笔者的学习,也希望能弄些相关文章和大家共同学习吧。

(三) 测试相关规则

很多开发人员在做单元测试的时候总是依赖自己的经验,凭感觉编写测试用例,想得了多少就写多少,自己没有一套完整的测试思路。所以,结果就是很多的需要测试的地方都被遗漏了。

如何有效地进行测试呢?当然笔者自己也总结了一些测试心得,不过,我觉得我看过的一本书叫《单元测试之道》中总结的几点比较恰当,现在就拿出来大家分享一下吧。

其中主要总结了6个值得重点注意的地方:简称Right-BICEP

Right – 结果是否正确,当然这是最基本的

B是否所有边界条件都是正确的

I 能查一下反向关联吗

C能用其它手段交叉检查吗

E你是否可以强制错误条件发生

P能否满足性能要求

(四) 测试驱动的开发

我们在进行单元测试的时候,往往为了测试,需要将代码不断的进行改动,其实这是在对代码进行优化,使代码的结构更合理,更利于测试代码的调用。这种以测试来驱动代码的开发,也就称之为测试驱动(Test-Driven)的开发吧。

利用测试驱动的开发方式一般都会在进行产品代码的编写前,编写测试代码,这样,自己就成了产品代码的真正用户,从而知道如何去优化接口,怎么去实现接口会更合理。

举个例子吧,如果你要实现一个小函数,用于格式化某些特殊的打印页,此时实现如下:

AddCropMarks(PSStream str, double paper_width,

double paper_height,

double body_width,

double body_height)

            下面编写测试代码:

                     Public Process()

{

AddCropMarks(str,8.5,11.0,6.0,8.5);

×××××××××××××××××××××××

×××××××××××××××××××××××

AddCropMarks(str,8.5,11.0,6.0,8.5);

×××××××××××××××××××××××

×××××××××××××××××××××××

AddCropMarks(str,5.0,7.0,4.0,5.5);

×××××××××××××××××××××××

×××××××××××××××××××××××

AddCropMarks(str,5.0,7.0,4.0,5.5);

 

 

 

 

}

我们会发现,其实纸的类型是固定的几种,这时我们会对其进行优化,

Paper paper1 = new Paper(8.5,11.0,6.0,8.5)

Paper paper2 = new Paper(5.0,7.0,4.0,5.5)

则,当前打印方法可以得到优化,如

AddCropMarks(PSStream str, Paper paper)

这样就可以产生如下测试方法:

AddCropMarks(str, paper1);

×××××××××××××××××××××××

×××××××××××××××××××××××

AddCropMarks(str, paper2);

当然这只是一个很简单的例子,在日常开发中你可能会遇到更复杂的情况,但是如果使用测试驱动的开发思想,相信能给你的代码编写带来很多的好处的。

(五) NUnitNAnt协作进行自动化单元测试

a)     NAnt入门  

要使用NAnt,第一步当然是从NAnt网站下载这个工具。正在用NAnt作为整合和构造工具的开发者注意一下,最新的NAnt集成了优秀的单元测试工具NUnit 2.0NUnit 2.01.0的基础上作了重大的改进,如果你使用NUnit 2,应该使用最新的NAnt以充分发挥它的优势。
  下面我们通过一个最简单的例子看看NAnt的使用过程——构造一个由单个执行文件组成的C#控制台程序。应用程序的代码如下:

public class HelloWorld {

    static void Main() {

      System.Console.WriteLine("Hello world.");

    }

  }


   当然,对于这样一个简单的项目,用C#命令行编译器也可以很方便地编译,只要执行一个“csc *.cs”命令就可以了。编译得到的结果是一个二进制可执行文件HelloWorld.exe。要用NAnt完成同样的任务,首先要创建一个扩展名为.buildXML构造文件,下面是一个NAnt构造文件的例子default.build,它完成的任务与执行一个简单的csc编译命令一样:

  【Listing 1:创建单个执行文件的简单NAnt构造脚本】



<?xml version="1.0"?>

  <project name="Hello World" default="build" basedir=".">

    <target name="build">

      <csc target="exe" output="HelloWorld.exe">

        <sources>

          <includes name="HelloWorld.cs"/>

        </sources>

      </csc>

    </target>

  </project>

容构成。一个task就是要求NAnt执行的一个任务,举例来说,NAnt支持的任务包括运行编译器、复制/删除文件、发送email,甚至还能够压缩一组文件(关于NAnt支持的完整任务清单,请参见这里)。

  目标描述了一组要求NAnt执行的任务,它是一种将任务分成逻辑组的手段。例如,假设我们要求NAnt删除bin目录的内容、编译5个执行文件、把编译得到的二进制文件复制到某个位置,可以把这些动作组织成一个target


  相关性可以看作是两个target之间的关系。不过Listing 1只有一个target,它的名称是build,它的任务是运行编译器编译指定的源文件。把标记的default属性设置为buildNAnt就会处理名称为buildtarget


  在csc任务内有一个子节点,它指定了要编译的源文件。

b)     定义相关性

现在我们加入第二个target——编译好HelloWorld.exe文件后立即执行。修改后的构造文件如Listing 2所示。

  【Listing 2:包含两个相关target的构造脚本】

 

 

 

 

<?xml version="1.0"?>

  <project name="Hello World" default="run" basedir=".">

    <target name="build">

      <csc target="exe" output="HelloWorld.exe">

        <sources>

          <includes name="HelloWorld.cs"/>

        </sources>

      </csc>

    </target>

 

 

 

 

    <target name="run" depends="build">

      <exec program="HelloWorld.exe"/>

    </target>

  </project>

      新添加的target名叫run,只包含一个用来执行程序的动作exec,此外它还有一个对build目标的相关性。这个相关性表示,在执行run这个target之前,必须先实现build这个target且必须执行成功。注意在节点中,我们把default属性由原来的build改成了run。由于run依赖于build,因此确保了在运行应用之前先编译好应用。

  如果由于某种原因build目标没有达到(通常是由于编译器发现了代码存在的错误),run目标也不会执行。你可以试验一下:先在HelloWorld的代码中故意加入一个语法错误,然后再次运行NAntNAnt将把编译器的错误信息显示到控制台,可以方便地看出哪里出现了错误。

c)               从头开始构造

如果有一个编译好的二进制文件比源文件还新,NAnt不会再执行编译操作——换句话说,NAnt不会编译任何无需编译的文件。此外,如果构造文件定义了多重相关性(即,二个或二个以上的组件依赖于另一个组件),NAnt聪明,它只构造被依赖的组件一次,不会重复构造同一个组件。这种处理方式大大提高了构造大型项目所需的时间,但有的时候,人们需要能够说不管我有什么,你都编译不误的权利,也就是说,要能够清除所有已经编译好的二进制文件,从头开始构造。

为此,许多构造文件会包含一个clean目标,开发者可以利用它来清除上一次编译留下的所有文件。下面是一个包含clean目标的构造文件例子:

 

 

 

 

  【Listing 3:包含clean目标的构造脚本】

 

 

 

 

<?xml version="1.0"?>

<project name="Hello World" default="run" basedir=".">

  <target name="build">

    <mkdir dir="bin" />

   

    <csc target="exe" output="bin/HelloWorld.exe">

      <sources>

        <includes name="HelloWorld.cs"/>

      </sources>

    </csc>

  </target>

 

 

 

 

  <target name="clean">

    <delete dir="bin" failonerror="false"/>

  </target>     

 

 

 

 

  <target name="run" depends="build">

    <exec program="bin/HelloWorld.exe"/>

  </target>

</project>



   clean目标并不是每次构造时都要运行,只是偶尔需要运行一下。要运行clean目标,只需执行nant clean命令即可。nant clean命令要求NAnt只执行clean目标(也就是说,不会执行构造项目的操作,只是清除一下bin目录的内容)。另外还可以看到,这个修改之后的构造脚本包含了一个mkdir动作,用来创建bin子目录以存放编译好的二进制文件。如果既要清除bin目录,又要构造项目,执行命令:nant clean build

d)        执行单元测试

如果要将构造过程和其他操作结合,例如email提醒和自动化的单元测试,NAnt也能够很好地完成。Listing 4就是这样一个构造文件的例子,它构造一个应用,并把执行NUnit也作为构造过程中很自然的一部分。

  【Listing 4:集成了单元测试的构造文件】

 

 

 

 

<?xml version="1.0"?>

<project name="NUnit Integration" default="test">

  <property name="build.dir" value="/dev/src/myproject/" />

 

 

 

 

  <target name="build">

    <csc target="library" output="account.dll">

      <sources>

        <includes name="account.cs" />

      </sources>

    </csc>

  </target>

 

 

 

 

  <target name="test" depends="build">

    <csc target="library" output="account-test.dll">

      <sources>

        <includes name="account-test.cs" />

      </sources>

      <references>

        <includes name="nunit.framework.dll" />

        <includes name="account.dll" />

      </references>

    </csc>

 

<nunit2>

<formatter type="Plain" usefile="true" extension=".txt" outputdir="${build.dir}/results"/>

      <test assemblyname="${build.dir}account-test.dll" />

    </nunit2>

  </target>

</project>



  造文件的开头声明,但如有必要,也可以改为通过命令行参数提供。在这个例子中,以属性的形式指定项目文件带来不少方便,因为在后面的构造过程中我们要把这些信息传递给NUnit

  接下来,构造文件依次构造出account.dll组件和测试工具account-test.dll。这两个构造过程都包含target="library"选项,这是告诉编译器我们要构造的是一个组件程序集,而不是一个.exe文件。另外,从Listing 4还可以看出,测试工具还通过references节点引用了两个它依赖的程序集——被测试的业务逻辑组件account.dllNUnit框架。当我们构造的项目依赖于外部库时,就要用到这个节点。


  构造好测试工具和业务逻辑组件后,构造脚本调用NUnit,并指定了包含测试组件的程序集的名称,要求生成一个XML格式的文件记录测试结果。


  关于NUnit集成,有一点必须注意:如果你正在用NUnit 2.0,必须使用最新的NAnt版本,这是因为NUnit最近作了重大的修改,某些稳定NAnt根本不能与NUnit 2.0一起运行,但最新的NAntNUnit 2.0的支持相当稳定。

   关于NAnt目前还没有能使用NCover的支持。

 

原创粉丝点击