前记:前段时间团队在推行单元测试,对于分配的测试任务也很快的完成,但以为本身对单元测试的理解也不够透彻,因此就买了《单元测试的艺术》这本书来寻找一些我想要的答案。这本书并非手把手教你写单元测试代码的,而是教你一些思想,按部就班,最终达到可以写出可靠的、可维护的、可读的测试。本篇文章是入门篇,主要是讲解单元测试的概念、与集成测试的区别以及如何使用框架进行最基础的单元测试等。数据库
单元测试是一段自动化的代码,这段代码调用被测试的工做单元,以后对这个单元的单个最终结果的某些假设进行检验。单元测试几乎都是用单元测试框架编写的。单元测试容易编写,能快速运行。单元测试可靠、可读、而且可维护。只要产品代码不发生变化,单元测试的结果是稳定的。网络
特征:架构
集成测试是对一个工做单元进行的测试,这个测试对被测试的工做单元没有彻底的控制,并使用该单元的一个或多个真实依赖物,例如时间,网络、数据库、线程或随机数产生器等。框架
单元测试与集成测试最大的区别在于:集成测试依赖于一个或多个真实的模块,当运行集成测试时,出现失败的状况后你并不能当即判断是哪里出了问题,所以找到缺陷的根源会比较困难。ide
[虚线表明是一个可选的行为]函数
[这是一个螺旋式的过程]单元测试
由上面的两个图中能够看出TDD与传统开发模式的区别:先编写一个会失败的测试,而后建立产品代码,并确保这个测试经过,接下来是重构代码或者建立另外一个会失败的测试。测试
单元测试框架是帮助开发人员进行单元测试的代码库和模块。编码
NUnit 是一套开源的基于.NET平台的类Xunit白盒测试架构,支持全部的.NET平台。这套架构的特色是开源,使用方便,功能齐全。很适合做为.NET语言开发的产品模块的白盒测试框架。spa
起初是从流行的Java单元测试框架JUnit直接移植过来的,以后NUnit在设计和可用性上作了极大地改进,和JUnit有了很大的区别,给突飞猛进的测试框架生态系统注入了新的活力。
如何在VS安装并运行呢?用Nuget是最方便的一种形式了,以下图:
(1)假定咱们要测试下面这段代码:
public class LogAnalyzer { public bool IsValidLogFileName(string fileName) { if (fileName.EndsWith(".SLF")) { return false; } return true; } }
这个方法是用来检查文件扩展名的,以此判断是不是一个有效的文件。在上面的程序中,故意在if条件语句中少了一个‘!’号,这样,咱们能够看到测试失败时在测试运行期间会显示什么内容。
(2)新建一个类库项目,名称最好为[ProjectUnderTest].UnitTests;并添加一个类,类型为[ClassName]Tests的类;在类中就能够写测试方法,通常测试方法是这样子来命名的:[UnitOfWorkName]_[ScenarioUnderTest]_[ExceptedBehavior]。
(3)咱们须要明确的是如何编写测试代码,通常来讲,一个单元测试包含三个行为:
① 准备(Arrange)对象,建立对象,进行必要的设置;
② 操做(Act)对象;
③ 断言(Assert)某件事情是预期的;
(4)根据以上步骤,编写第一个单元测试方法
[TestFixture] public class LogAnalyzerTests { [Test] public void IsValidFileName_BadExtension_ReturnsFalse() { LogAnalyzer analyzer = new LogAnalyzer(); bool result = analyzer.IsValidLogFileName("filewithbadextension.foo"); Assert.AreEqual(false, result); } }
其中,属性[TestFixture]:标识这个类是一个包含自动化NUnit测试的类和[Test]:标识这个方法是一个须要调用的自动化测试是NUnit的特有属性,NUnit用属性机制来识别和加载测试。
从上图能够看出,测试方法并无经过,咱们指望(Expected)的结果是False,而实际(Actual)的结果倒是True。而且还帮你指出了行号。
NUnit有个很酷的功能,叫作参数化测试。能够从现有的测试中任意选择一个,进行一下修改:
(1)把属性[Test]替换成属性[TestCase]
(2)把测试中用到的硬编码的值替换成这个测试方法的参数
(3)把替换掉的值放在属性的括号中[TestCase(param1,param2,...)]
[TestCase("filewithbadextension.SLF")] [TestCase("filewithbadextension.slf")] public void IsValidLogFileName_ValidExtensions_ReturnsTrue(string file) { LogAnalyzer analyzer=new LogAnalyzer(); bool result = analyzer.IsValidLogFileName(file); Assert.True(result); }
须要注意的是:这个时候你须要用一个比较通用的名字从新命令这个测试方法。
固然,[TestCase("")]不只仅只能够写一个参数,也能够写N个参数。
[TestCase("filewithbadextension.SLF",true)] [TestCase("filewithbadextension.slf",true)] public void IsValidLogFileName_ValidExtensions_ReturnsTrue(string file,bool excepted) { LogAnalyzer analyzer = new LogAnalyzer(); bool result = analyzer.IsValidLogFileName(file); Assert.AreEqual(excepted,result); }
进行单元测试时,很重要的一点是保证以前测试的遗留数据或者实例获得销毁,新测试的状态是重建的。幸亏,NUnit有一些特别的属性,能够很方便地控制测试先后的设置和清理状态工做,就是[SetUp]和[TearDown]动做属性。
[SetUp] NUnit每次在运行测试类里的任何一个测试时都会先运行这个方法
[TearDown] 这个属性标识一个方法应该在测试类里的每一个测试运行以后执行。
private LogAnalyzer _logAnalyzer = null; [SetUp] public void Setup() { _logAnalyzer=new LogAnalyzer(); } [Test] public void IsValidFileName_validFileLowerCased_ReturnsTrue() { bool result = _logAnalyzer.IsValidLogFileName("hello.slf"); Assert.IsTrue(result,"filename should be valid!"); } [Test] public void IsValidFileName_validFileUpperCased_ReturnsTrue() { bool result = _logAnalyzer.IsValidLogFileName("hello.SLF"); Assert.IsTrue(result, "filename should be valid!"); } [TearDown] public void TearDown() { _logAnalyzer = null; }
虽然SetUp与TearDown用起来很方便,可是不建议使用,由于这种方式随着代码的增长,后面测试方法很快就变得难以阅读了,最好是采用工厂方法来初始化被测试的实例。
咱们如今修改一下要测试的代码,在输入为Null或者Empty的时候,就跑出一个异常。
public class LogAnalyzer { public bool IsValidLogFileName(string fileName) {
if(string.IsNullOrEmpty(fileName))
{
throw new ArgumentException("filename has to be provided");
}
if (fileName.EndsWith(".SLF")) { return false; } return true; }
测试代码以下:
[Test] [ExpectedException(typeof (ArgumentException), ExceptedMessage = "fileName has to be provided")] public void IsValidFileName_EmptyFileName_ThrowsException() { MakeLogAnalyzer().IsValidLogFileName(string.Empty); } private LogAnalyzer MakeLogAnalyzer() { return new LogAnalyzer(); }
注意:以上的代码虽然是正确的,可是在NUint3.0中已经弃用了,缘由是采用这种方法,你可能不知道哪一行代码抛出的这个异常,若是你的构造函数有问题,也抛出这个异常,那你所写的测试也会经过,但事实上是错误的。NUint提供了一个新的API,Assert.Catch<T>(delegate)。如下是使用Assert.Catch编写的测试代码:
[Test] public void IsValidFileName_EmptyFileName_ThrowsException() { var ex = Assert.Catch<ArgumentException>(() => { MakeLogAnalyzer().IsValidLogFileName(""); }); StringAssert.Contains("fileName has to be provided",ex.Message); } private LogAnalyzer MakeLogAnalyzer() { return new LogAnalyzer(); }
有时候代码有问题,可是你又须要把代码签入到主代码中(这种状况应该是少中极少,由于这是一种错误的方式)。能够采用[Ignore]属性。示例以下:
[Test] [Ignore("it has some problems")] public void IsValidFileName_validFileUpperCased_ReturnsTrue() { bool result = MakeLogAnalyzer().IsValidLogFileName("hello.SLF"); Assert.IsTrue(result, "filename should be valid!"); }
结果以下:
能够把测试按指定的测试类别运行,例如:慢测试和快测试。使用[Category]属性能够实现这个功能。
[Test] [Category("Fast Tests")] public void IsValidFileName_ValidFile_ReturnTrue() { Assert.IsTrue(MakeLogAnalyzer().IsValidLogFileName("xxx.SLF")); }
上面全部测试示例,都是有根据被测试方法的返回值来进行测试,但一个工程里面不可能每一个方法都是有返回值的,有的是须要判断系统状态的改变的,称为基于状态的测试。
定义:经过检查被测试系统及其协做方(依赖物)在被测试方法执行后行为的改变,断定被测试方法是否正确工做。
//被测试代码
public class LogAnalyzer { public bool WasLastFileNameValid { get; set; } public bool IsValidLogFileName(string fileName) { WasLastFileNameValid = false; if (string.IsNullOrEmpty(fileName)) { throw new ArgumentException("fileName has to be provided"); } if (!fileName.EndsWith(".SLF")) { return false; } WasLastFileNameValid = true; return true; } }
测试代码:
[TestCase("filewithbadextension.SLF", true)] [TestCase("filewithbadextension.slf", true)] public void IsValidLogFileName_ValidExtensions_ReturnsTrue(string file, bool excepted) { LogAnalyzer analyzer = MakeLogAnalyzer(); analyzer.IsValidLogFileName(file); Assert.AreEqual(excepted, analyzer.WasLastFileNameValid); } private LogAnalyzer MakeLogAnalyzer() { return new LogAnalyzer(); }