• 单元测试与Nunit的基本使用


     一、单元测试是什么

    单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,C#里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。

    单元测试(模块测试)是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为。

    二、为什么需要单元测试

    在我们现在的编程思维中一直都是编码=>编译=>调试,一直循环,直到要处理的功能完成,每一个功能完成都是如此,且有的功能是严重依赖于上一个功能。在如此处理中存在几个问题。

    1. 编译通过后,运行程序出现的bug难以定位。
    2. 修改一个bug,容易引进其他bug。
    3. Bug越到后期发现,越难以修改。
    4. 后期系统的复杂性,导致代码难以修改和重构,使得系统难以维护。
    5. 开发人员常认为编译功过,进行了几次手工测试就等于测试通过(认为详细的测试是测试人员的工作,非开发人员的工作)。
    6. 在完全依赖外部系统的情况下,难以进行有效的测试。
    7. 手工测试效率低下,针对性不强,测试不能重用。

    有了单元测试在开发过程中起到的作用。

    1. 大大节约了测试和修改的时间,有效且便于测试各种情况。
    2. 能快速定位bug(每一个测试用例都是具有针对性)。
    3. 能使开发人员重新审视需求和功能的设计(难以单元测试的代码,就需要重新设计)。
    4. 强迫开发者以调用者而不是实现者的角度来设计代码,利于代码之间的解耦。
    5. 自动化的单元测试能保证回归测试的有效执行。
    6. 使代码可以放心修改和重构。
    7. 测试用例,可作为开发文档使用(测试即文档)。
    8. 测试用例永久保存,支持随时测试。

    既然单元测试有这些好处,为什么我们不去用呢。可以归纳为以下几个理由。

    1. 对单元测试存在的误解,如:单元测试属于测试工作,应该由测试人员来完成,所以单元测试不属于开发人员的职责范围。答:虽然单元测试虽然叫做"测试",但实际属于开发范畴,应该由开发人员来做,而开发人员也能从中受益。
    2. 没有真正意识到单元测试的收益,认为写单元测试太费时,不值得。

      答:在开发时越早发现bug,就能节省更多的时间,降低更多的风险。单元测试先期要编写测试用例,是需要多耗费些时间,但是后面的调试、自测,都可以通过单元测试处理,不用手工一遍又一遍处理。实际上总时间被减少了。

    3. 项目经理或技术主管没有要求写单元测试,所以不用写。

      答:写单元测试应该成为开发人员的一种本能,开发本身就应该包含单元测试。

    4. 不知道有单元测试这回事,不知道如何用。经过这篇文档的说明,就基本知道如何处理单元测试。

    结论:

    只进行手工测试,只是临时性的单元测试,代码测试覆盖率要超过70%都很困难,未覆盖的代码可能遗留大量的细小的错误,这些错误还会互相影响,当bug暴露出来的时候难于调试,大幅度提高后期测试和维护成本。可以说,进行充分的单元测试,是提高软件质量,降低开发成本的必由之路。

    要进行充分的单元测试,应专门编写测试代码,并与产品代码隔离。比较简单的办法是为产品工程建立对应的测试工程,为每个类建立对应的测试类,为每个函数(很简单的除外)建立测试函数。

    单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。

    对于程序员来说,如果养成了对自己写的代码进行单元测试的习惯,不但可以写出高质量的代码,而且还能提高编程水平。

    三、单元测试工具。

    在.Net平台有三种单元测试工具,分别为MS Test、NUnit、Xunit.Net。

    1.MS Test为微软产品,集成在Visual Studio 2008+工具中。

    2.NUnit为.Net开源测试框架(采用C#开发),广泛用于.Net平台的单元测试和回归测试中,官方网址(www.nunit.org)。

    3.XUnit.Net为NUnit的改进版。

    (以下主要讲解NUnit的使用,会了NUnit其他2个测试工具也能快速熟悉)。

    任何xUnit工具都使用断言进行条件的判断,NUnit自然也不例外,与其它的xUnit(如JUnit、phpUnit、pythonUnit)相比,由于大量使用了Generic、Attribute等语言特征,NUnit提供了更为方面、灵活的测试方法,下面先介绍一下断言。

    NUnit一共有五个断言类,分别是Assert、StringAssert、FileAssert、DirectoryAssert、CollectionAssert,它们都在NUnit.Framework命名空间,其中Assert是常用的,而另外四个断言类,顾名思义,分别对应于字符串的断言、文件的断言、目录的断言、集合的断言。理论上,仅Assert类就可以完成所有条件的判断,然而,如果合理的运用后面的四个断言,将使代码更加简洁、美观,也更加便于理解和维护。

    四、NUnit的使用。

    本处演示所使用的NUnit版本为2.6.4,若要使用最新版可以去官网下载。

    首先创建一个类库项目(也可以是其他项目),然后创建一个Test+类库名称的项目(也可以是项目名称+Test),用于代表是测试工程。如下图:

    Demonstration项目中含有一个计算功能类,对应的测试项目含有一个测试计算类,一个计算功能类中方法可能需要多个测试用例来完成检测。如下展示出了2个类的代码:

        /// <summary>
        /// 用于演示的一个简单计算功能
        /// </summary>
        public class Calculate
        {
            /// <summary>
            /// 加法
            /// </summary>
            public int Add(int a, int b)
            {
                return a + b;
            }
            /// <summary>
            /// 减法
            /// </summary>
            public int Subtract(int a, int b)
            {
                return a - b;
            }
            /// <summary>
            /// 乘法
            /// </summary>
            public int Multiply(short a, short b)
            {
                return a * b;
            }
            /// <summary>
            /// 除法
            /// </summary>
            public int Quotient(int a, int b)
            {
                return a / b;
            }
            /// <summary>
            /// 开平方根
            /// </summary>
            public double SquareRoot(int num)
            {
                return Math.Sqrt(num);
            }
            /// <summary>
            /// 四舍五入,取整
            /// </summary>
            public int Round_Off(double num)
            {
                return (int)Math.Round(num);
            }
            /// <summary>
            /// 向上取整
            /// </summary>
            public int UpwardTrunc(double num)
            {
                return (int)Math.Ceiling(num);
            }
            /// <summary>
            /// 平方
            /// </summary>
            public int Square(short num)
            {
                throw new NotImplementedException();
            }
        }
        [TestFixture(Description = "测试示例")]
        public class TestCalculate
        {
            private Calculate calculate;
            private StreamReader reader;
            private string[] sourceData = new string[] { @"......Resourcescore_1.csv" };
            private short a, b;
            [TestFixtureSetUp]
            public void Initialize()
            {
                Console.WriteLine("初始化信息");
                calculate = new Calculate();
            }
            [TestFixtureTearDown]
            public void Dispose()
            {
                Console.WriteLine("释放资源");
                if (reader != null)
                {
                    reader.Close();
                }
            }
            [SetUp]
            public void SetUp()
            {
                a = 3;
                b = 2;
            }
            [TearDown]
            public void TearDown()
            {
                Console.WriteLine("我是清理者");
            }
            [Test(Description = "加法")]
            [Category("优先级 1")]
            public void TestAdd()
            {
                Assert.AreEqual(5, calculate.Add(a, b));
            }
            [Category("优先级 1")]
            [TestCase(1, 2), TestCase(2, 3)]
            public void TestSubtract(int a, int b)
            {
                Assert.AreEqual(a - b, calculate.Subtract(a, b));
            }
            [Category("优先级 2")]
            [TestCase(1, 2, Result = 2), TestCase(2, 3, Result = 6)]
            public int TestMultiply(short a, short b)
            {
                return calculate.Multiply(a, b);
            }
            [Test]
            [Category("优先级 2")]
            [ExpectedException(typeof(DivideByZeroException))]
            public void TestQuotient()
            {
                calculate.Quotient(a, 0);
            }
            [Test]
            [Category("优先级 3")]
            public void TestSquareRoot()
            {
                Assert.Less(1, calculate.SquareRoot(a));
            }
            [Test]
            [Category("优先级 3")]
            [Sequential]
            public void TestRound_Off([Values(3.4, 4.5, 4.6, 5.5)] double num, [Values(3, 5, 5, 6)] int result)
            {
                Assert.AreEqual(result, calculate.Round_Off(num));
            }
            [Test]
            [Category("优先级 3")]
            public void TestUpwardTrunc([ValueSource("sourceData")] object fileName)
            {
                reader = new StreamReader((string)fileName);
                string content;
                while ((content = reader.ReadLine()) != null)
                {
                    var nums = content.Split(',').Select(c => double.Parse(c)).ToArray();
                    Array.ForEach(nums, (num) =>
                    {
                        int result = calculate.UpwardTrunc(num);
                        Console.Write(result + "
    ");
                    });
                }
            }
            [Test]
            public void TestSquare()
            {
                Assert.Throws<NotImplementedException>(() => calculate.Square(b));
            }
            [Test, Explicit]
            [Ignore]
            public void TestFactorial()
            {
                Assert.Fail("未能实现阶乘功能");
            }
        }
    View Code

    在粗略看了代码后,下面就详细说明相应的测试标记(属性)的用法。

    1. [TestFixture(arguments)]属性标记类为测试类,若没有填写参数,则测试类必须含有无参构造函数,否则需要相应的有参构造函数。也可以多个测试[TestFixture(1), TestFixture("a")]
    2. [Test]属性标记方法为测试方法,中添加Description参数可以给我们测试的功能添加描述信息。
    3. [TestCase(arguments)]属性标记有参数无返值方法为测试方法(泛型方法一样标记),想要多次测试可用逗号隔开([TestCase(1,2), TestCase(2,3)])。
    4. [TestCase(arguments,Result = value)属性标记带参数与返回值的方法为测试方法,执行的时候把预期的返回值也告诉NUnit,如果返回值不对,测试同样无法通过。
    5. [Suite](测试套件,仅对属性与索引器标记有效):可以将多个测试类组合到一起,同时执行多个测试。本版本的开发人员的一个信念就是减少这个的需要,可以使用[Category]来替代它。
    6. [Explicit]属性标记测试方法需要在UI界面显式执行,如果不想对某个方法进行单元测试,只是在它被选中时才进行测试的话,可以调用该特性。
    7. [Ignore]属性标记一个测试方法或一个测试类被忽略,如果测试类被忽略,其内中的测试方法也会被忽略。
    8. [ExpectedException(Type)]属性标记测试方法在运行时抛出一个期望的异常,如果是则测试通过,否则不通过。
    9. [Category("")]属性标记用于将测试分类(便于只测试需要的类别),可在方法与类上进行标记,在NUnit-GUI界面的Categories选项卡中对要测试种类进行添加,Run时仅测试该类别的测试。
    10. [TestFixtureSetUp]属性标记方法为类级别设置(初始化)方法,在整个测试类中执行一次初始化,所有的测试方法共享初始化数据。
    11. [TestFixtureTearDown]属性标记方法为类级别拆卸方法,在整个测试类中执行一次拆卸.当测试类中的所有测试方法执行完成,就会执行拆卸方法,用于清除数据、释放资源。
    12. [TearDown]属性标记方法为函数级别的拆卸方法,在执行完每个测试方法后,执行该拆卸方法。一个测试类可以仅有一个TearDown/Setup/TestFixtureSetUp/TestFixtureTearDown方法。如果有多个定义,测试类也会编译成功,但是测试时不会运行这些标记过的方法。
    13. [SetUp]属性标记方法为函数级别的设置方法,在执行每个测试方法前,执行该设置方法。
    14. 每执行一次Run,就是new一个新的实例在测试。
    15. [Maxtime]/[Timeout]属性标记测试用例的最大执行时间,前者超时时不取消测试,而后者会强行中断,用法如:[Test, Maxtime(2000)],[Test, Timeout(2000)]。
    16. [Repeat]属性标记测试方法重复执行多少次,如:[Test, Repeat(100)]。
    17. [RequiresMTA]/[RequiresSTA]/[RequiresThread]属性标记测试用例必须的在多线程、单线程、独立的线程状态下运行。
    18. [Values]属性标记测试用例的参数,以参数的形式传入一组值,NUnit会把这组值分解成相应数量的子测试。当测试用例的2个参数都使用[Values]进行标记,NUnit默认生成2组数量乘积的用例,需要使用[Sequential]标记测试用例才能按顺序生成一一对应的n(n=2组中最大数组长度)个子测试用例。
    19. [ValueSource]属性标记测试用例的参数,指定参数的数据源来自哪里,在使用[ValueSource]指定数据源时,该数据源必须实现了IEnumerable接口,数据源可以是属性、无参方法、实例或静态成员。

    更多属性标记与详细说明,可以查阅NUnit官网提供的说明文档。一个方法的测试可能要写很多个测试用例,这都是正常的,如果一个测试用例包含多个断言,那些紧跟失败断言的断言都不会执行,因为通常每个测试方法最好只有一个断言。

    在运行单元测试时有3种方式分别为:

    1. 把测试工程的属性=>调试=>启动外部程序,设置为NUnit运行程序。在启用调试时会启动NUnit界面程序,但NUnit界面没有测试用例的信息,需要自己添加在File=>Open Project->文件资源管理器,找你的测试工程类库或程序添加即可。点击Run运行,根据选中的节点运行该节点下所有的子测试用例(该测试可进行调试)。如下图:

    以上的图片展示了运行错误界面和运行输出界面。在测试用例的节点中绿色'√'代表通过,黄色'√'代表忽略,红色'×'代表失败。

    1. 直接启动NUnit界面程序,在File=>Open Project->文件资源管理器,添加测试工程类库或程序,点击相应的节点进行Run测试,NUnit会根据类库或程序生成更新,自动更新界面中测试用例节点,但运行的测试用例不能进行调试。效果图与①中的效果一样。
    2. 在Visual Studio 2010+的IDE中以插件的方式集成NUnit测试工具,直接在测试工程中点击鼠标右键,运行测试即可。或者在VS菜单栏的测试中运行NUnit测试。集成与运行效果图在"第五节"中展示。

    五、Nunit常用类和方法

    1、Assert(断言):如果断言失败,方法将没有返回,并且报告一个错误。

    1)、测试二个参数是否相等

    Assert.AreEqual;

    Assert.AreEqual;

    2)、测试二个参数是否引用同一个对象

    Assert.AreSame;

    Assert.AreNotSame;

    3)、测试一个对象是否被一个数组或列表所包含

    Assert.Contains;

    4)、测试一个对象是否大于另一个对象

    Assert.Greater;

    5)、测试一个对象是否小于另一个对象

    Assert.Less;

    6)、类型断言:

    Assert.IsInstanceOfType;

    Assert.IsAssignableFrom;

    7)、条件测试:

    Assert.IsTrue;

    Assert.IsFalse;

    Assert.IsNull;

    Assert.IsNotNull;

    Assert.IsNaN;用来判断指定的值是否为数字。

    Assert.IsEmpty;

    Assert.IsNotEmpty;

    Assert.IsEmpty;

    Assert.IsNotEmpty;

    8)、其他断言:

    Assert.Fail;方法为你提供了创建一个失败测试的能力,这个失败是基于其他方法没有封装的测试。对于开发你自己的特定项目的断言,它也很有用。

    Assert.Pass;强行让测试通过

    2、字符串断言(StringAssert):提供了许多检验字符串值的有用的方法

    StringAssert.Contains;

    StringAssert.StartsWith;

    StringAssert.EndsWith;

    StringAssert.AreEqualIgnoringCase;

    3、CollectionAssert类

    CollectionAssert.AllItemsAreInstancesOfType;集合中的各项是否是某某类型的实例

    CollectionAssert.AllItemsAreNotNull:集合中的各项均不为空

    CollectionAssert.AllItemsAreUnique;集合中的各项唯一

    CollectionAssert.AreEqual;两个集合相等

    CollectionAssert.AreEquivalent;两个集合相当

    CollectionAssert.AreNotEqual;两个集合不相等

    CollectionAssert.AreNotEquivalent;两个集合不相当

    CollectionAssert.Contains;

    CollectionAssert.DoesNotContain;集合中不包含某对象

    CollectionAssert.IsSubsetOf:一个集合是另外一个集合的子集

    CollectionAssert.IsNotSubsetOf:一个集合不是另外一个集合的子集

    CollectionAssert.IsEmpty;集合为空

    CollectionAssert.IsNotEmpty;集合不为空

    CollectionAssert.IsOrdered;集合的各项已经排序

    4、FileAssert

    FileAssert.AreEqual;

    FileAssert.AreNotEqual;

    5、DirectoryAssert

    DirectoryAssert.AreEqual;

    DirectoryAssert.AreNotEqual;

    DirectoryAssert.IsEmpty;

    DirectoryAssert.IsNotEmpty;

    DirectoryAssert.IsWithin;

    DirectoryAssert.IsNotWithin;

    六、NUnit集成到VS中的使用。

    在使用NUnit-GUI处理运行测试用例,是不是感觉比较麻烦,还要使用外部的NUnit应用程序,有没有简单点的最好能够跟VS开发工具紧密结合的方式来进行NUnit单元测试呢?答案是肯定的,有2种方式。

    1.我们在VS中选择工具菜单栏下的扩展和更新,选择联机并在搜索框中输入NUnit。出现如下图的信息,有2个版本的Nunit适配器,分别为NUnit 3.x(最新版为3.4.1)和NUnit 2.x(最新版为2.6.4),都支持Visual Studio 2012+。若想在VS2010中集成,需要安装NUnit 2.6.4安装包(可在官网下载)与VS2010 NUnit整合插件(下载地址:

    http://visualstudiogallery.msdn.microsoft.com/c8164c71-0836-4471-80ce-633383031099),下载安装完毕就能在 VS2010 的视图=>其他窗口中看到 Visual Nunit(或使用快捷键Ctrl + F7),打开该视图,将之拖到合适的位置。

    下载安装NUnit Test Adapter后关闭VS,重启一下就好了,我们打开类库项目中的TestCalculate类,在右键弹出的菜单中点击运行测试。运行结束后,会在左侧的测试资源管理器当中显示本次操作的结果。

    2.通过ReSharper工具处理NUnit的单元测试,在VS2010+中安装了ReSharper开发插件,ReSharper内中自带支持NUnit与MS Test这2个单元测试工具,只要你的测试工程中引用了相应的单元测试类库(如nunit.Framework.dll)、以及含有测试用例。通过鼠标右键或快捷键(Ctrl + T,R),就可以运行单元测试,也可以进行单元测试调试,ReSharper选项图与运行效果如下图。

    七、后续

    上面列出只能单元测试的基本使用,未能说明对Mock等其他功能的使用,也没有解释对难以单元测试的代码进行重新设计的说明,需要后期深入了解才能列出相应的文档说明。能够更好的使用单元测试才能更好的使用TDD(测试驱动开发)来开展项目,TDD测试驱动开发是测试先行(此测试是单元测试)、是极限编程的一个重要特点,它以不断的测试推动代码的开发,既简化了代码,同时也保证了软件指令,另一方面说编写的测试用例将成为重要文档(可以作为SDK提供给开发者,测试即文档)。

    -----------------以上内容是根据博客园其他博客的说明与Nunit官方文档,以及自己测试使用,进行了整理说明。----------------------------

  • 相关阅读:
    转:windows通过VNC访问远程ubuntu14.04 【 server】服务器
    【转】研究了代码质量后,开发速度提高了2倍,bug减少了15倍
    【转】PuTTY的ppk密钥与OpenSSH密钥之间的相互转换
    【转】outlook配置腾讯企业邮箱(腾讯企业邮箱imap服务器地址)
    【转】YApi结合swag管理和生成go项目restful API文档
    [转] 超高效!SwaggerYapi的秘密
    【转】SSH 远程登录很慢的解决方法
    [转]go语言io reader_如何从io.Reader 中读数据
    eslint 支持多个三目表达式
    33
  • 原文地址:https://www.cnblogs.com/zwt-blog/p/5788222.html
Copyright © 2020-2023  润新知