浅谈单元测试

  无论是在面试中或者项目实践时,在写代码过程中,一个很好的习惯就是先写好单元测试,再开始写自己的函数。这样给人感觉你的思维比较缜密。

  1.单元测试干了什么,作用是什么?通俗讲单元测试就是检查一个函数执行后它的返回结果或者它对系统数据造成的影响(或者其它方面的影响)是否跟你的期望一致,也就是为了证明代码的行为和我期望的一致!

    ①最直接的是保证了函数的正确性(这个大家都知道)!

      ②还有我们可以根据单元测试来判断此函数是用来干什么的,也就说单元测试类似于一个可执行文档,其它开发人员可以通过看单元测试就会明白你测试的函数是用来干嘛的!

2.单元测试的本质

    请记住一点,不是为工作而编写单元测试,单元测试是方便我们开发人员的,可以使我们的工作变的轻松!

    单元测试可以减少我们花在解决不必要的BUG之上(并不是说没有BUG,而是说减少不必要的BUG),而把大量时间专注于业务需求上!

3.走进单元测试二:测试需要从哪些方面着手

  

笼统的来说测试条件无非就是两个方面:① 正向测试 ,② 反向测试!

  如果单从这两个方面来思考,肯定出现丢三落四的情况,也就是说不全面,所以应该在上面两种情况的基础上再进行具体划分,那么只要我们能够遵循这些条件基本上就能做到全面(如果能做到,大约80%的问题应该都解决了),于是就出现了下面要说的六个方面内容!

  前辈们把这些测试条件总结为:Right – BICEP

 1.Right - 做正确的事,可以说是“正向测试”

    这种测试前期任务是要准备足够的正确数据(前提是要保证数据的正确性,这个很重要),运行代码后返回的值或产生的影响是要跟自己的预期是一致的!

    注意:如果准备的数据太大或容易丢失,建议把它放在数据文件中,然后让单元测试读取这个文件,这种方法会在下一篇会说到!

 

 2.B - 边界条件(Boundary)

    边界条件是测试里面的重中之重,必须要有足够的认识和重视!

    而它又被分为七个方面的子条件,下面就让我们来一一熟悉它!

    ①一致性(Conformance)

      数据是否符合我规定的格式(也可以说是非法字符吧)!

      案例:比如我传入的参数文件名需要的格式是:文件名 + 日期(yy-mm-dd) + 扩展名,那么我就要写一个测试传入的文件名为 :“sa#$#$#$#”这样的格式!

    ②有序性(Ordering)

      这方面主要是对涉及到数组和集合的数据,而且对数据的顺序有严格要求的函数,需要对它们里面数据的顺序进行测试!

      比如:点菜系统菜谱中每道菜的顺序,或者去银行办理业务的排队系统等等!

    ③范围,区间性(Range)

      值是否存在于一个最大值和一个最小值之间,主要是对值类型的数据做的测试!

      这里面还有一个重要的测试点是 → 对数组,集合,以及Table,DataSet中的索引值进行测试,比如索引值不能为负,不能超出索引的范围等等情况!

      比如:一个通过ID来搜索信息的函数,应该对这个ID进行最大值和最小值的测试!

    ④引用,耦合性(Reference)

      这方面主要是:代码是否引用了一些不受本身代码控制的外部因素(比如:调用第三方接口,调用其它模块的接口等等)!

      对于这些情况我们是没有办法控制的,所以在测试的时候只能模拟,而在模拟时我们会用到“Mole”技术,让它来帮助我们创建一个模拟环境(下一篇会介绍)!

      比如:有的项目会调用银行接口,这种情况下只能先创造一个虚拟银行接口,然后再进行测试!

    ⑤存在性(Exist)

      固定的测试,如Null,Empty,非零等等,这些都是必须考虑的!

    ⑥基数性(Cardinality)

      对于这个测试说起来还是蛮难理解,这个测试只有在特定的场合下才会去考虑它!

      它遵循一个原则:“0-1-N”!       

    ⑦时间性(Timer)

      对时间比较有依赖的软件或系统应该在这个方面着重测试!

      主要考虑:事情是否按时间的顺序执行,是否在正确的时间执行,是否出现执行事情延误了!

                  相对时间:网站超时,数据更新超时等等!

                  绝对时间:不同的client间的时间是否同步!

                  并发问题在时间性测试中比较重要!   

  

 3.I - 反向关联(Inversion)      

    在准备数据或者验证数据时的一种反向思维,涉及到个人的思维方式问题了!

    比如:有个函数对数据库进行了操作,但是它没任何返回值也没有任何提示,如果你是对正确的数据进行了测试,那么你要怎么知道测试结果跟你的预期一致呢,这里你就应该去查找数据库,看数据库里面的数据是否有真的改动,这就是一种反向的思维方式!

 

 4.C - 交叉检查(Cross)

    用一种数量检查另一种数量(需要考虑的情况不是很多)!

 

 5.E - 强制产生错误(Error)

    通过代码强制产生软件在运行过程中出现的特殊情况!

    可以参考下面几种测试方面:内存耗光,磁盘用满,断电,正在执行更新数据时出现断网现象,网络负载严重导致瘫痪,系统时间出现导致和国际时间不一致等等一些情况!

 

 6.P - 性能特性(Property)

    性能测试工具的使用,没具体研究过性能测试工具,知道的朋友可以说下你们的经验!

              进行压力测试,一点一点的加大数据量,10000条,100000条,1000000条这样进行压力测试!

 

  总结:本人对反向关联和交叉检查这两个测试条件不是很理解,知道的朋友可以留言给我,我会把它补充到文章中去!

  下一篇:走进单元测试三:创建自己的单元测试

  最后:下面是我对测试条件的小小总结,比较简陋!

 
 
4.实例,

在软件开发过程中,我们可能会有很多的模块,而每个模块有可能又由许多函数组成。当我们的系统发生错误时,我们必须定位发生错误的模块,然后精确到模块中某个具体的函数中,而这工作往往又是非常浪费时间和生产效率的,如果系统越复杂,那么定位错误的成本将越高。所以在每个函数集成进模块时,必须通过严格的单元测试来验证。

在VS2010中我们可以为我们的函数自动生成单元测试,无论它是否是public或者的private的。所有用于单元测试的类和函数都被定义在Microsoft.VisualStudio.TestTools.UnitTesting这个命名空间中。

 

创建Unit Test

我们先创建一个被测试的类库工程,名字叫TestLibrary,然后添加如下代码:

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TestLibrary
{
public class Class1
{
public double CalculateTotalPrice(double quantity)
{
double totalPrice;
double unitPrice;

unitPrice = 16.0;

totalPrice = unitPrice * quantity;
return totalPrice;
}

public void GetTotalPrice()
{
int qty = 5;
double totalPrice = CalculateTotalPrice(qty);
Console.WriteLine("总价: " + totalPrice);
}

}
}
复制代码

 

然后我们在需要单元测试的函数上鼠标右键,如图会有个Create Unit Tests选项。

Untitled

 

点击该选项后,就会弹出如下窗口,该窗口会显示出该工程和类中所有的函数,这里我们可以选择我们要进行单元测试的函数。

Capture

 

我们选择CalculateTotalPrice和GetTotalPrice两个函数,点击OK,然后输入测试工程的名字点Create。(我们可以在Output project选项中选择一个以创建的工程或者创建一个新的测试工程)我们的单元测试代码就自动创建好了,当然,这个自动创建的测试代码并没有完成的,而是为我们的单元测试搭好了框架而已。自动生成的代码如下:

复制代码
using TestLibrary;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;

namespace TestProject1
{


///<summary>
///This is a test class for Class1Test and is intended
///to contain all Class1Test Unit Tests
///</summary>
[TestClass()]
public class Class1Test
{


private TestContext testContextInstance;

///<summary>
///Gets or sets the test context which provides
///information about and functionality for the current test run.
///</summary>
public TestContext TestContext
{
get
{
return testContextInstance;
}
set
{
testContextInstance = value;
}
}

#region Additional test attributes
//
//You can use the following additional attributes as you write your tests:
//
//Use ClassInitialize to run code before running the first test in the class
//[ClassInitialize()]
//public static void MyClassInitialize(TestContext testContext)
//{
//}
//
//Use ClassCleanup to run code after all tests in a class have run
//[ClassCleanup()]
//public static void MyClassCleanup()
//{
//}
//
//Use TestInitialize to run code before running each test
//[TestInitialize()]
//public void MyTestInitialize()
//{
//}
//
//Use TestCleanup to run code after each test has run
//[TestCleanup()]
//public void MyTestCleanup()
//{
//}
//
#endregion

///<summary>
///A test for CalculateTotalPrice
///</summary>
[TestMethod()]
public void CalculateTotalPriceTest()
{
Class1 target = new Class1(); // TODO: Initialize to an appropriate value
double quantity = 0F; // TODO: Initialize to an appropriate value
double expected = 0F; // TODO: Initialize to an appropriate value
double actual;
actual = target.CalculateTotalPrice(quantity);
Assert.AreEqual(expected, actual);
Assert.Inconclusive("Verify the correctness of this test method.");
}

///<summary>
///A test for GetTotalPrice
///</summary>
[TestMethod()]
public void GetTotalPriceTest()
{
Class1 target = new Class1(); // TODO: Initialize to an appropriate value
target.GetTotalPrice();
Assert.Inconclusive("A method that does not return a value cannot be verified.");
}
}
}
复制代码

 

其实,我们可以在创建单元测试时适当控制自动生成的测试代码,如图我们点击Setting按钮。

Capture2

 

这时会弹出如下图的窗口

Capture3

 

在该对话框中,我们可以对生成的测试文件、测试类以及测试方法自定义名称。

 

  • Mark all test results Inconclusive by default:选中该复选框可为每个测试方法提供 Assert.Inconclusive() 语句作为占位符 Assert。清除该复选框可消除占位符 Assert。
  • Enable generation warnings:在测试函数创建中如果遇到任何的错误,代码生成器会将这些错误信息以注释的形式写在生成的代码中。
  • Globally qualify all types:这个选项是用来解决多个类可能有相同名字的函数问题,单元测试文件可能包含有多个类的测试函数,所以有可能会有同名的冲突,所以为了区分开同名的函数,会在测试命名中添加namespaces。
  • Enable documentation comments:选中此复选框可为每个测试方法提供占位符注释。清除该复选框可消除占位符注释。
  • Honor InternalsVisibleTo Attribute:选中该复选框可使标为 Friend 或 Internal 的方法被视为公共方法(推荐)。清除该复选框则需要使用专用访问器测试这些方法。

 

此外还可以注意到自动生成的代码中有一些被注释的方法,这些方法我们可以选择使用:

 

  1. [ClassInitialize()]标记的方法可在运行类中的第一个测试前运行代码。
  2. [ClassCleanUp()]标记的方法可在运行完类中的所有测试后运行代码。
  3. [TestInitialize()]标记的方法可在运行每个测试前运行代码。
  4. [TestCleanUp()]标记的方法可在运行完每个测试后运行代码。

 

Assert语句

Assert语句用来比较从方法返回来的值和期望值,然后返回pass或者fail的结果。如果在一个测试方法中有多个Assert的话,那么这个测试方法要通过测试必须让所有的Assert方法通过,不然,其中有一个fail,那么这个Case就会fail。如果我们的单元测试中没有任何的Assert语句,那么它的结果始终是pass的。

Assert类有许多进行比较的方法,此外还有StringAssertsCollectionAssert类也可用于单元测试中,具体的我们可以参加MSDN上的介绍了。

 

下面我们修改一下CalculateTotalPriceTest()这段测试代码,并将最后的Assert.Inconclusive注释:

复制代码
        [TestMethod()]
public void CalculateTotalPriceTest()
{
Class1 target = new Class1();
double quantity = 10F;
double expected = 160F;
double actual;
actual = target.CalculateTotalPrice(quantity);
Assert.AreEqual(expected, actual);
//Assert.Inconclusive("Verify the correctness of this test method.");
}
复制代码

 

最后我们可以在该函数中,右键鼠标然后点击Run Tests运行测试一下我们的代码。我们就可以在Test Results窗口中看见我们运行的结果。

posted @ 2013-10-02 14:32  红宝石  阅读(302)  评论(0编辑  收藏  举报