首先要说一下哪部分code一定要有unit test.
1. 依赖第三方接口的模块。因为第三方提供的结果不一定完全可信,数据可能会变化。如果有针对第三方接口的测试,当程序出现问题时,我们跑一下unit test, 就可以先判断出是第三方接口数据有变化还是自己的代码有问题。这样可以缩短debug的时间。
2. 模块之间的接口。和上面的原因一下,模块之间的接口是连接几个模块的唯一通道。通过对接口测试,也可以先把问题缩写在某个模块。
下面说一下可能在单元测试中需要注意的地方。
1. 子单元测试
想一想你有没有遇到过以下的问题。你的单元测试失败了,但是你要花费一段时间才能定位到错误的原因。为什么会这样,是因为unit test负责的功能太大了。这个时候需要把这一个unit test分成几个小的test进行测试。
我也会遇到这个问题。一个类它提供了public的接口。我会写unit test测试这个接口。但是这个接口可能是有三个函数实现的,比如A,B,C。所以当我发现unit test失败时,我不知道是A,B,C哪个错误导致的。那现在如果我们分别针对A,B,C也加了unit test进行测试,这样我们就会更容易地定位出错的地方。那如果在这里A可能是private的话,那A需要unit test为这个函数测试吗?
针对private(或protected)的函数是否需要测试的问题,有人说不应该对private 函数测试,因为private 函数之所以设置成private就是不想让外界访问,而且private 的函数可能更容易变化,这样导致unit test也要跟着变。也有人说如果需要可以对private 函数测试,就是因为如果不针对子函数进行测试的话,可能导致错误不容易定位的问题。对我来说,在某些情况下,还是会有对private函数进行测试的时候。
针对private,可以通过反射机制访问。针对protected的函数,我一般会让unit test的class继承被测试的类,这样在unit test的class中就可以访问到要测试的函数了。
2. Mock Object
我想写过unit test的人,几乎都写过Mock Object. Mock是模仿的意思,证明不是真正的对象。比如你要测试依赖数据库的逻辑是否正确,但是你真正去操作一个数据库,可能会对数据库的记录造成影响。或者你现在对一个文件进行操作,对文件进行操作你需要处理IO异常的情况。但是如果你读取的是一个真正的文件,你可能永远不知道处理IO异常的代码是否正确。这个时候你就可以用一个Mock的对象去模拟IO出现异常,看看你的程序是否能正常处理这个情况。
比如你定义一个class,它继承自StreamWriter(通过streamwriter访问文件).
class MockStreamWriter : StreamWriter
{
public override void Write(string value)
{
throw new IOException("");
}
}
在unit test测试时,将需要StreamWriter的时候,传入MockStreamWriter的对象,这样当调用Write方法时,就调用了MockStreamWriter的Write方法,这个时候你就可以测试你的代码能否成功handle IOException.
3. Self Shunt
Self Shunt就是说我们可以把unit test自己作为Mock Object, 而不需要额外再创建一个class. 比如现在有一个class Person, 这个class有很多Listener, 只要Person中的Notify执行,就会把object的状态通知Listener.
class Person
{
//other methods and fields....
public IList<Listener> ListenerList;
public void Notify()
{
for (int i=0; i<ListernList.count; i++)
{
ListenerList[i].Accept(this);
}
}
}
正常测试时,我们需要Mock Listener, 它继承自Listener.
class MockListener : Listener
{
public int Count = 0;
public override void Accept(Person p)
{
Count++;
}
}
Unit Test:
public void Test()
{
Person p = new Person();
MockListener listener = new MockListener();
p.ListenerList.Add(listener).
p.Notify();
Assert.AreEqual(1, listener.Count);
}
现在我们让Unit Test的class再增加一个功能,即作为MockListener.
class MockListener_UnitTest : Listener
{
public int Count = 0;
public override void Accept(Person p)
{
Count++;
}
public void Test()
{
Person p = new Person();
p.ListenerList.Add(this).
p.Notify();
Assert.AreEqual(1, this.Count);
}
}
这样做的好处是你可以不用再写一个额外的class了。但是有可能你需要实现Listener所有的接口(可以返回一个合理的值或者抛出异常)。把Unit Test本身作为Mock object, 代码读起来更方便。