• 单元测试的艺术-入门篇


    前记:前段时间团队在推行单元测试,对于分配的测试任务也很快的完成,但觉得自己对单元测试的理解也不够透彻,所以就买了《单元测试的艺术》这本书来寻找一些我想要的答案。这本书并不是手把手教你写单元测试代码的,而是教你一些思想,循序渐进,最终达到能够写出可靠的、可维护的、可读的测试。本篇文章是入门篇,主要是讲解单元测试的概念、与集成测试的区别以及如何使用框架进行最基础的单元测试等。

    一、单元测试的基础

    1.1、什么是单元测试

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

      特征:

    • 自动化、可重复执行;
    • 很容易实现;
    • 第二天还有意义;
    • 任何人都应该能一键运行它;
    • 运行速度应该很快;
    • 结果应该是稳定的;
    • 能完全控制被测试的单元;
    • 完全隔离(独立于其他测试的运行);

    1.2、什么是集成测试

      集成测试是对一个工作单元进行的测试,这个测试对被测试的工作单元没有完全的控制,并使用该单元的一个或多个真实依赖物,例如时间,网络、数据库、线程或随机数产生器等。

    1.3、单元测试与集成测试的区别在哪里?

      单元测试与集成测试最大的区别在于:集成测试依赖于一个或多个真实的模块,当运行集成测试时,出现失败的情况后你并不能立即判断是哪里出了问题,因此找到缺陷的根源会比较困难

      

    二、TDD(测试驱动开发)

    2.1、传统的开发流程

           [虚线代表是一个可选的行为]

      

    2.2、TDD的开发流程

      [这是一个螺旋式的过程]

      

    由上面的两个图中可以看出TDD与传统开发模式的区别:先编写一个会失败的测试,然后创建产品代码,并确保这个测试通过,接下来是重构代码或者创建另一个会失败的测试。

    三、开始使用框架进行基础的单元测试

    3.1、单元测试框架的作用

      单元测试框架是帮助开发人员进行单元测试的代码库和模块。

    3.2、NUnit

      NUnit 是一套开源的基于.NET平台的类Xunit白盒测试架构,支持所有的.NET平台。这套架构的特点是开源,使用方便,功能齐全。很适合作为.NET语言开发的产品模块的白盒测试框架。

         起初是从流行的Java单元测试框架JUnit直接移植过来的,之后NUnit在设计和可用性上做了极大地改进,和JUnit有了很大的区别,给日新月异的测试框架生态系统注入了新的活力。

         如何在VS安装并运行呢?用Nuget是最方便的一种形式了,如下图:

     

    3.3、编写第一个单元测试

      (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用属性机制来识别和加载测试。

    3.4、运行过程与结果

      

      

      从上图可以看出,测试方法并没有通过,我们期望(Expected)的结果是False,而实际(Actual)的结果却是True。并且还帮你指出了行号。

    四、更多NUnit属性的介绍

    4.1、参数化测试

      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);
            }

    4.2、[Setup]与[TearDown]

      进行单元测试时,很重要的一点是保证之前测试的遗留数据或者实例得到销毁,新测试的状态是重建的。幸好,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用起来很方便,但是不建议使用,因为这种方式随着代码的增加,后面测试方法很快就变得难以阅读了,最好是采用工厂方法来初始化被测试的实例。

    4.3、检验预期的异常

      我们现在修改一下要测试的代码,在输入为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();
            }

    4.4、忽略测试

      有时候代码有问题,但是你又需要把代码签入到主代码中(这种情况应该是少中极少,因为这是一种错误的方式)。可以采用[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!");
            }

      结果如下:

    4.5、设置测试的类型

      可以把测试按指定的测试类别运行,例如:慢测试和快测试。使用[Category]属性可以实现这个功能。

            [Test]
            [Category("Fast Tests")]
            public void IsValidFileName_ValidFile_ReturnTrue()
            {
                Assert.IsTrue(MakeLogAnalyzer().IsValidLogFileName("xxx.SLF"));
            }

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

      上面所有测试示例,都是有根据被测试方法的返回值来进行测试,但一个工程里面不可能每个方法都是有返回值的,有的是需要判断系统状态的改变的,称为基于状态的测试。

      定义:通过检查被测试系统及其协作方(依赖物)在被测试方法执行后行为的改变,判定被测试方法是否正确工作。

        //被测试代码
    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();
            }

    五、总结

    • 创建测试类、项目和方法的管理;
    • 测试命名要有规范;
    • 使用工厂方法重用测试中的代码,例如用来创建和初始化所有测试都要用到的对象代码;
    • 尽量不要使用[SetUp]和[TearDown],因为它们使测试变得难以理解。
  • 相关阅读:
    SQL Server控制语句
    MATLAB中取整函数(fix, floor, ceil, round)的使用
    MATLAB程序设计
    Thinking In Java<<Java编程思想>>
    Boost::bimap
    MySQL学习随笔1
    Boost 1_42_0在windows下的编译及其设置
    MySQL执行mysql脚本及其脚本编写
    Pygame介绍
    Erlang
  • 原文地址:https://www.cnblogs.com/Helius/p/6867144.html
Copyright © 2020-2023  润新知