什么是单元测试

  

  单元测试是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为。例如,你可能把一个很大的值放入一个有序list  中去,然后确认该值出现在list  的尾部。或者,你可能会从字符串中删除匹配某种模式的字符,然后确认字符串确实不再包含这些字符了。

 

  执行单元测试,是为了证明某段代码的行为确实和开发者所期望的一致。

  对于客户或最终使用者而言,这种测试必要吗,它与验收测试有关吗?这个问题仍然很难回答。事实上,我们在此并不关心整个产品的确认、验证和正确性等等;甚至此时,我们都不去关心性能方面的问题。我们所要做的一切就是要证明代码的行为和我们的期望一致。因此,我们所要测试的是规模很小的、非常独立的功能片断。通过对所有单独部分的行为建立起信心,确信它们都和我们的期望一致;然后,我们才能开始组装和测试整个系统。

 

  毕竟,要是我们对手上正在写的代码的行为是否和我们的期望一致都没把握,那么其他形式的测试也都只能是浪费时间而已。在单元测试之后,你还需要其他形式的测试,有可能是更正规的测试,那一切就都要看环境的需要来决定了。总之,做测试如同做善事,总是要从家(代码最基本的正确性)开始。

 

为什么要使用单元测试 

 

  单元测试不但会使你的工作完成得更轻松,而且会令你的设计变得更好,甚至大大减少你花在调试上面的时间。

  通常,我们会因为假设底层的代码是正确无误的而卷入麻烦之中,先是高层代码中使用了底层代码;然后这些高层代码又被更高层的代码所使用,如此往复。在对这些代码的行为没有任何信心的前提下,我们等于是在假设上面用竖立卡片堆砌了一间房子——只要将下面卡片轻轻移动,整间房子就会轰然倒塌。

 

  当基本的底层代码不再可靠时,那么必需的改动就无法只局限在底层。虽然你可以修正底层的问题,但是这些对底层代码的修改必然会影响到高层代码,于是高层代码也连带地需要修改;以此递推,就很可能会动到更高层的代码。于是,一个对底层代码的修正,可能会导致对几乎所有代码的一连串改动,从而使修改越来越多,也越来越复杂。于是,整间由卡片堆成的房子就由此倒塌,从而使整个项目也以失败告终。

 

  Pat 总是说:“这怎么可能呢?”或者“我实在想不明白为什么会这样”。如果你发现自己有时候也会有这种想法,那么这通常是你对自己的代码还缺乏足够信心的表现——你并不能确认哪些是工作正常的而哪些不是。

 

  为了获得Dale 所具有的那种对代码的信心,你需要“询问”代码究竟做了什么,并检查所产生的结果是否确实和你所期望的一致。

这个简单的想法描述了单元测试的核心内涵:这个简单有效的技术就是为了令代码变得更加完美。

 

 我需要做什么 

 

引入单元测试是很简单的,因为它本身就充满了乐趣。然而在项目交付的时候,我们给客户和最终用户的仍然是产品代码,而不包含单元测试的代码;因此,我们必须对单元测试的目的有个充分的认识。首先也是最重要的,使用单元测试是为了使你的工作——以及你队友的工作——完成得更加轻松。

  ■ 它的行为和我的期望一致吗

  最根本的,你需要回答下面这个我饿提:"这段代码达到我的目的了吗?"也许就需求而言,代码所做的是错误的事情,但那是另外一个问题了。你要的是代码想你证明它所做的就是你期望的。

  ■ 它的行为一直和我的期望一致吗

  许多开发者说它们至编写一个测试。 也就是让所有代码从头到尾跑一次, 只测试代码 的一条正确执行路径, 只要这样走易变下来没有问题, 测试也就算是完成了。 

  你不能这样来测试一座桥: 在风和日丽的某天, 仅让一辆车顺利的开过这座桥。 显然,这种测试对于桥梁测试来说是远远不够的。 类似的 ,在测试某段代码的行为是否和你的期望一致时, 你需要确认: 在任何情况下,这段代码是否都和你的期望是一致的; 比如在风很大, 参数很可疑, 硬盘没有剩余空间, 网络掉线的时候。 

  ■ 单元测试说明我的意图了吗

  对于单元测试而言,一个最让人高兴的意外收货就是它能够帮助你充分理解代码的用法。从效果上而言, 单元测试就像是能够执行的文档,说明了在你用各种条件调用代码时候,你所能期望这段代码完成的功能。

如何进行单元测试

  单元测试本来就是一项简单易学的技术;但是如果能够遵循一些指导性原则和基本步骤,那么学习将会变得更加容易和有效。

首先要考虑 的是在编写这些测试方法之前,如何测试那些可以的方法。有了这样一个大概的想法之后,你将可以在编写实现代码的时候,或者之前编写测试代码本身。

  下一步,你需要运行测试本身,或者同时运行系统模块的所有其他测试,甚至运行整个系统的测试,前提是这些测试运行起来相对比较快。在此,我们要确保所有的测试都能够通过,而不只是新写的测试嗯哪挂钩通过;这一点是非常重要的。也就是说,在保证不引入直接bug的同事,你也要保证不会给其他的测试带来破坏。

首个单元测试  

我们从一个简单的demo开始:

  1. 我们要先把工具下载过来,用于单元测试的开源软件nunit,它提供一套测试框架和一个测试运行程序。默认安装到C:\Program Files\目录下,安装完成后可以在开始菜单中找到。

  2. 创建一个类库项目,暂且叫UnitTest,接着创建一个Largest.cs的文件和一个TestLargest.cs文件,代码如下:

 

namespace UnitTest

{
    public class Cmp

    {
        public static int Largest(int[] list)
        {
            int index, max = 0;
            if (list.Length == 0)
            {
                 throw new Exception("Em");
            }
            for (index = 0; index < list.Length ; index++)
            {
                if (list[index] > max)
                {
                    max = list[index];
                }
            }
            return max;
        }
    }
}

namespace UnitTest
{
    [TestFixture]
    public class TestLargest
    {
        [Test]
        public void LargestOf3()
        { 
            Assert.AreEqual(9,Cmp.Largest(new int[]{7,8,9}));
        }
    }
}


  3. 在开始菜单中找到刚才安装的Nunit打开。在界面的tool--》setting--》visual studio 打勾,然后open一个project,即一个编译好的dll文件,最后run一下就出来结果了

 

   4. 当你运行一个选定的测试时,GUI将会显示一个大的、有颜色的状态栏。如果所有的测试都通过,状态将会显示开心的绿色;如果任何测试都失败,状态栏将会变成生气的红色。如果状态栏呈现境界黄色的话,那意味着一些测试被跳过了(具体将在后面说明)。

 

  5. 我们这里看到的是理想的状态,如果我们将前面的测试的数据改成Assert.AreEqual(-7, Cmp.Largest(new int[] { -9, -8, -7 })); 那么结果是怎样的,大家可以试试。

  测试后的结果是一条红色,外加说明:

  

UnitTest.TestLargest.LargestOf3:
Expected: -7
But was:  0

它告诉你,本来应该-7,结果确实0。相比大家已经知道为什么了,我们应该在初始化的时候,把max初始化为int的最小值minvalue。

 

这样我们一个简单的unit demo 就结束了。