• 单元测试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的值依赖另一个方法先调用,所以无法通过编写一个获得方法返回值的测试来检测它的功能。需要单独的状态属性进行断言。

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

  • 相关阅读:
    How To Use Google Logging Library (glog)
    段错误调试
    getline 使用
    remove_if筛选数组元素
    getline C++ Reference
    c++ Why remove_copy_if returns an empty vector? Stack Overflow
    About iClick
    哈工大社会计算与信息检索研究中心
    python的16进制和10进制间的转换
    毕业生 哈尔滨工业大学社会计算与信息检索研究中心 理解语言,认知社会
  • 原文地址:https://www.cnblogs.com/zwt-blog/p/5881415.html
Copyright © 2020-2023  润新知