单元测试2

单元测试的定义:一个单元测试是一段自动化的代码,这段代码调用被测试的工作单元,之后对这个单元的单个最终结果的某些假设进行检验。单元测试几乎都是用单元测试框架编写的。单元测试容易编写,能快速运行。单元测试可靠、可读,并且可维护。只要产品代码不发生变化,单元测试的结果是稳定的。

成功进行TDD的三种核心技能:知道如何编写优秀的测试、在编码前编写测试、以及良好的测试设计。

测试命名和位置的基本规则


测试对象


测试方创建的对象


项目


创建一个名为"项目名+.UnitTests"的测试项目



对应被测试项目中的一个类,创建一个名为[ClassName]Tests的类


工作单元(一个方法,或者几个方法组成的一个逻辑组,或者几个类)


对应被测试项目中的一个类,创建一个如下命名的测试方法:[UnitOfWorkName]_[ScenarioUnderTest]_[ExpectedBGheavior]。如果整个工作单元就是一个方法,工作单元名就可以很简单,就是这个方法名;如果工作单元是一个包含多个方法或类的用例,工作单元名就可能比较抽象,如:UserLogin、RemoveUser或Startup。你可以从方法名开始,之后逐渐过渡到比较抽象的工作单元名。如何使用方法名,要确保这些方法是公共的,否者它们不能真正代表一个工作单元的起点。

测试方法名称的三部分:

  1. UnitOfWorkName 被测试的方法、一组方法或者一组类
  2. Scenario 测试进行的假设条件,例如"登入失败""无效用户"或"密码正确"。你可以用测试场景描述传给公开方法的参数,或者单元测试进行时系统的初始状态,例如:"系统内存不足""无用户存在"或"用户已经存在"。
  3. ExpectedBehavior 在测试场景指定的条件下,你对被测试方法行为的预期。测试方法的行为有三种可能的结果:返回一个值(一个真实值,或者一个异常),改变系统状态(例如在系统中添加了一个用户,导致在下一次登入时系统的行为发生变化),或调用一个第三方系统(例如一个外部的Web服务)。

在我们对IsValidLogFileName方法进行测试中,场景是你给方法传入一个有效的文件名,预期行为是方法返回一个值true。我们可以把这个测试的方法命名为IsValidFileName_BadExtension_ReturnFalse()。

你应该把测试代码放在产品代码项目中吗?还是应该把测试代码单独放在另一个测试相关的项目里呢?我通常选择把测试和产品代码分开,这样可以使测试相关的所有其他任务更容易进行,而且,在产品代码中包含测试代码容易导致复杂的条件编译设置,还会带来其他的问题,降低代码的可读性,因此很多人都不喜欢这种做法。

一个单元测试通常主要包含三个行为:

  1. 准备(Arrange)对象,创建对象,进行必要的设置;
  2. 操作(Act)对象;
  3. 断言(Assert)某件事情是预期的。

下面是一段简单的代码,分别为被测试代码与测试单元,测试单元包含了以上全部三个行为,其中断言部分使用了NUnit框架提供的Assert类。

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]
public void IsValidLogFileName_BadExtension_ReturnsFalse()
{
LogAnalyzer analyzer = new LogAnalyzer();//三部分行为“A-A-A”,都隔一行就是便于区分与阅读

bool result = analyzer.IsValidLogFileName("filewithbadextension.foo");

Assert.False(result);
}

如上,当我们需要使用多个文件名来测试单元有效性时,难道要写多个测试方法吗。肯定不是,可以使用TestCase属性标记,该属性与更多属性的详细说明上一节已经列出。如下代码:

[TestCase("filewithgoodextension.SLF",true)]
[TestCase("filewithgoodextension.slf",true)]
[TestCase("filewithbadextension.foo",false)]
public void IsValidLogFileName_VariousExtensions_ChecksThem(string file, bool expected)
{
    LogAnalyzer analyzer = new LogAnalyzer();

    bool result = analyzer.IsValidLogFileName(file);

    Assert.AreEqual(expected, result);
}

检测预期的异常,一个常见的场景是:保证当异常应该抛出时,被测试的方法能够抛出正确的异常。

假设传入一个空文件名的时候,你的方法应该抛出一个ArgumentException异常,如果代码在这种情况下没有抛出异常,你的测试就应该失败,代码上面已列出。对此有两种测试方法,让我们先来看不应该用的那种,因为这种方法很流行,而且曾经是做这种测试的唯一方法。使用ExpectedException属性标记测试异常。代码如下:

[Test]
[ExpectedException(typeof(ArgumentException),ExpectedMessage = "filename has to be provided")]
public void IsValidLogFileName_EmptyFileName_ThrowsException()
{
    LogAnalyzer la = MakeAnalyzer();
    la.IsValidLogFileName(string.Empty);
}

private LogAnalyzer MakeAnalyzer()
{
    return new LogAnalyzer();
}

在这段代码中没有使用Assert调用,[ExceptedException]属性内部包含断言,为什么说不应该使用这种方法呢?因为这个属性基本上是告诉测试运行器把这整个方法包在一个大的try-catch块里,如果没有东西"捕捉"到,就认为测试失败。这种做法有一个很大的问题,就是你不知道是哪一行代码抛出的这个异常。实际上,如果构造函数有问题,抛出了一个异常,你的测试也会通过,而构造函数是绝对不应该抛出异常的,这样的话,使用这个属性,测试结果有可能是不真实。所以尽量不要用这种方法。

NUnit提供了一个更新的API:Assert。Catch<T>(delegate),以下是使用Assert.Catch编写的代码:

[Test]
public void IsValidLogFileName_EmptyFileName_Throws()
{
    LogAnalyzer la = MakeAnalyzer();

    var ex = Assert.Catch<ArgumentException>(() => la.IsValidLogFileName(""));

    StringAssert.Contains("filename has to be provided", ex.Message);
}

Assert.Catch函数返回Lambda内抛出的异常实例,你可以在之后的代码中对这个异常对象的消息进行断言。

使用StringAssert,它包含能够简化字符串测试的辅助方法,使用这个类可以提高代码可读性。

没有用Assert.AreEqual进行全字符串相等断言,而是使用StringAssert.Contains断言消息包含你寻找的字符串。随着时间的变化,当代码中加入新功能后,字符串经常会发生变化,经常会包含额外的换行符以及你不关心的多余信息,使用StringAssert.Contains可以使测试更容易维护,否则就不得不对这个测试进行修复。

使用这种方法测试结果的可能性就比较小了,因此我推荐使用Assert.Catch而不是[ExpectedException]。

测试系统状态的改变而非返回值

基于状态的测试(也称为状态验证)通过检查被测试系统极其协作方(依赖物)在被测试方法执行后行为的改变,判定被测试方法是否正确工作。

考虑对LogAnalyzer类的基于状态的简单测试,引入一个新的属性WasLastFilenameValid,这个属性记录IsValidLogFileName方法的上次调用成功与否。代码如下:

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",StringComparison.CurrentCultureIgnoreCase))
        {
            return false;
        }
        WasLastFileNameValid = true;
        return true;
    }
}
[TestCase("badfile.foo", false)]
[TestCase("goodfile.slf", true)]
public void IsValidLogFileName_WhenCalled_ChangesWasLastFileNameValid(string file, bool expected)
{
    LogAnalyzer la = MakeAnalyzer();

    la.IsValidLogFileName(file);

    Assert.AreEqual(expected, la.WasLastFileNameValid);
}

如你在以上代码中所见,LogAnlyzer记住了最后一次验证的结果,因为WasLastFileNameValid的值依赖另一个方法先调用,所以无法通过编写一个获得方法返回值的测试来检测它的功能。需要单独的状态属性进行断言。

以上内容根据《单元测试的艺术----第二版》进行整理的(其内容主要讲解编写优秀的测试)。

时间: 2024-11-13 09:12:17

单元测试2的相关文章

单元测试Junit

###<center> 单元测试Junit </center>###- - -1.**单元测试**:> ==单元测试==是软件之中对于最小的功能模块的的测试,其可以对最基本的软件构成单元来测试.> 需要注意的是:> >**测试用例是用来达到测试想要的预期结果,而不能测试出程序的逻辑错误**. 2.**JUnit**:>1.**Junit是基于断言机制的**.是用于编写可复用测试集的简单框架,是xUnit的一个子集.xUnit是一套基于测试驱动开发的测试

MVC与单元测试实践之健身网站(四)-动作管理

网站后台负责进行动作的管理,包括动作名称.介绍.训练要点.配图等内容,以便前台能够使用这些内容.在上一篇< Fit项目图片上传和云存储的调通>中已经准备好了这里涉及到的主要技术难点,现在就开始完成该模块了. 一 列表介绍 健身管理模块包括肌群.肌肉的显示以及动作的管理.这儿也算是开始涉及"业务内容"了,还好我之前有储备了一些关于健身的资料,现在是时候派上另一种用场了. a) 肌群和肌肉因为内容相对固定,所以为了减少业务逻辑以及单元测试的代码量,当然最主要是为了偷懒,就只提供

MVC与单元测试实践之健身网站(二)-管理员模块

开始动手做这个项目时,发现无法做到完全的先设计.再编码,于是决定分模块进行,从管理员模块开始设计.编码,而且接口就已经改了好几次了. 管理员模块涉及的功能有登录和后台对管理员的维护,其中也涉及前端的开发.UI模板使用Inspinia,感觉这套模板功能丰富.界面美观,而且基于HTML5和BootStrap,对这两方面的知识也可以多些了解. 在上一篇<如何在单元测试时隔离ORM>中,解决了对Service层进行测试怎样构建伪对象的问题,随后管理员模块的Service层和单元测试在齐头并进中完成了:

单元测试(一)-NUnit基础

单元测试作为提高代码和软件质量的有效途径,其重要性和益处自不必多说,虽然我没有实践过TDD之类,但坚信单元测试的积极作用.作为一种开发方法,单元测试早在上世纪70年代就已经在Smalltalk语言被运用了,这么多年来,单元测试一次又一次证明了自身的价值,在各种开发方式此起彼伏的浪潮中,经受住了时间的考验. 现在,俺也开始学习了,并在以后好好实践.这个系列的学习素材为Roy Osherove所著The Art of Unit Testing with examples in C#, 2nd Edi

OA项目CRUD和单元测试(一)

使用ModeFirst方法生成数据库,EntityFramework5.0. 一:Model层的模型:(根据模型生成数据库) 二:Dal层的UserInfo代码: namespace SunOA.EFDAL { public class UserInfoDal { //crud DataModelContainer db = new DataModelContainer(); public UserInfo GetUserInfoById(int id) { return db.UserInfo

词频统计-单元测试

我自己的单元测试没有弄出来,我用c编的,在visual studio中貌似实现不了单元测试,而李俞寰同学是用c#编写的词频统计,在vs2015中实现单元测试无比的方便,所以我请教了他并借鉴了一下. [TestMethod()] public void DictionarySortTest() { Dictionary<string,int>input=new Dictionary<string,int>() { {"you,1}, {"are",1},

使用Xunit来进行单元测试

不管你爱与不爱,单元测试对于一个软件的长治久安还是必不可少的一环.在Visual Studio 2012后,VS中的测试浏览器也能与第三方的集成了,用起来还是非常方便的.目前在.Net框架下的测试工具主要有Nunit.内置的MSTest以及Xunit这三个工具,本文就简单的介绍一下如何在VS中使用XUnit这个测试框架的后起之秀. 安装Xunit: Xunit的安装现在不需要插件支持了,直接使用NuGet安装如下两个库即可: PM> Install-Package xunit PM> Inst

作业八——单元测试练习(个人练习)

必做一: 针对附录1给出的三角形判断Java 代码,应用等价类划分法设计测试用例,用表格形式列出设计的测试用例: 测试用例如下:(红色字体为错误预言) 序号 测试输入:三条边 测试预言:[Oracle:Illegal(非三角形),Scalene(一般三角形), Isoceles(等腰三角形),Regular(等边三角形)] 1 (5,5,5) Regular 2 (-5,-5,-5) Regular 3 (1,4,5) Illegal 4 (2,3,5) Illegal 5 (3,4,5) Sc

作业8:单元测试练习(个人练习)

要求 [必做题1] 针对附录1给出的三角形判断Java 代码,应用等价类划分法设计测试用例,用表格形式列出设计的测试用例,写到博客中.(10分) [必做题2] 模仿附录2给出的三角形判断Junit测试代码,设计单元测试脚本,测试 [必做题1]设计得到的测试用例.注意测试脚本中测试用例出现顺序与[必做题1]表格所列顺序一致.运行所得的测试脚本,截运行结果图,写到博客中,同时将源代码push到你自己的github.(20分) [必做题3] 心得体会.写下本次练习你收获的知识点(PS:测试用例设计方法

实验二 单元测试

1. 学习单元测试和代码覆盖率工具的使用 (1)写一个程序,用于分析一个字符串中各个单词出现的频率,并将单词和它出现的频率输出显示.(单词之间用空格隔开,如“Hello World My First Unit Test”): (2)编写单元测试进行测试: (3)用ElcEmma查看代码覆盖率,要求覆盖率达到100%. import java.util.HashMap; import java.util.Map; public class Test { private static String