------------------
前言 Preface
------------------
本文是第二代测试框架系列文章,同时也是软件工程革命三部曲中的技术文献。
本人对现有的测试技术统称为第一代测试框架;本人总结他们的优缺点后,提出第二代测试框架理论并实现。
阅读本文的门槛比较高,需要您掌握一定的测试技术和理论,例如单元测试、回归测试、代码覆盖率等;同时需要掌握.net的IL,c#词法分析等。
本文所提出的原理都会附上源代码。
------------------
开篇 Introduction
------------------
第一篇文章先定位是篇口水文,让大家读起来不费力。
测试驱动历史不算悠久,所以国内很多兄弟并没有、也根本不想系统的学习。我个人觉得几乎90%的人写代码都是一边写一边debug。通过掉断点查看问题所在。
在testdriven.net出来之前,开发是个比较痛苦的事情,即使有了vs200x强大的调试引擎,可是每次调试都要F5运行整个项目,实在费力。于是我当时的做法就是开一个demo项目,把需要调试的代码copy过去,调好了在copy回来。后来Testdriven.net出来之后,写好代码之后,直接通过鼠标右键点击运行查看断点,基本上调试速度快了n倍。
似乎到达这里,已经能够解决了大部分人的需求了。所以看了所谓的测试先行,心里就想:简直弱智,代码还没有写好,就写什么测试代码,简直自己找抽!
不过今天,我已经读了大部分测试框架的源码和理论,我的观点依然是: 简直弱智,代码还没有写好,就写什么测试代码,简直自己找抽!测试是需要的, 但是测试先行不是所有开发都要遵循。准确的说,测试先行只有在极少数的情况下才有效果。
测试驱动是国外大牛提出来的理论,他们提出的历史背景是已有一大堆的代码库基础上,开发额外的功能。换句话说,他们现在手头已经有了很多的业务逻辑代码,添加的新功能占了整个项目估计不超过10%。
这个时候,他们对代码的运行结果、影响范围等有了充分的估计,所以使用测试先行,让设计师写好接口和测试代码,再让手下的民工完成业务逻辑。
虽然大部分的测试驱动书籍里面开篇就说的TD好像神乎其神,用了就腿不疼了、腰不酸了。但是他们却刻意的回避了他们的历史背景,因此我们和书的作者不是在一条起跑线上。
我们,即使是小公司(就算是大公司,例如爱立信),在开发中,经常都会修改接口、修改public 方法,甚至删除类。因为根本没有人能保证咱们一下手就是对的。 所以搞什么测试先行,结果连测试代码都要修改多次,麻烦!
所以,我的结论就是,新项目开发,测试放在中间和最后。一旦新项目开发完成进入稳定运行期后,完善测试用例;日后的迭代使用测试驱动。
这个才是可行的测试驱动。
------------------------------------------------------
第二代测试框架概述 TestDriven 2.0 Introduction
------------------------------------------------------
第二代测试框架,就是为了考虑到80%的需求提出来的。除了是nunit+mock+ncover,我还引入了代码分析、关联分析、测试代码自动更新等技术。
先展望一下美好的未来:
1. 我们开辟了一个新项目后,鼠标点击生成测试代码;就得到一个.testdriven文件夹,里面的测试代码自动根据您的代码之间关联排序,建立起基础到复杂的关联测试。
2. 当我们完成第一个testcase的时候,第二个已经自动准备好了,而且完成了50%。慢慢到最后最复杂的测试代码,基本上已经自动完成了80%了。
3. 当我们发现项目开发出来之后并不能符合实际需求,大改之后,鼠标再轻轻右键点击更新测试代码,系统又自动分析了代码的接口和上一次测试代码,自动合并更新出本次的测试代码;当然历史测试代码已经被打包归档了。
4. 当我们的系统稳定运行了n天之后,客户提出了新的需求,我们发现原有代码不能支持,需要修改接口。我们轻轻在需要修改的方法点击查看影响范围,立刻获得受到这个方法影响的dll。当修改完毕之后,系统自动根据影响范围自动对所有相关的测试用例进行回归测试;
这个就是我看得到的未来,也是到了目前,微软还没有提出的未来!!网络有web 2.0,体现了人人之间的沟通,那么测试驱动也有2.0,体现了不同类之间的沟通。单元测试不再是独立的,而是相互关联的。
------------------------------------
Assert 2.0 技术
------------------------------------
有人认为我在吹,我可没有功夫花几个小时打一篇水文出来。接下来我就介绍自己如何实现这个TestDriven2.0。首先最基础的,当然是Assert技术,即所谓的断言。
传统我会使用Console.Writeline()进行测试。因为直观,直接从控制台看到结果。正如一名测试工程师说的,写一堆代码让系统判断,还不如直接打出来,人看。所以我介绍的Assert技术结合了nunit的优点和console的优点。
先看看效果,测试代码:
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
{
class Class1
{
public void test001()
{
new Assertion().IsEqual(123, new Class2().GetValue(null, 3));
}
class Class2
{
public Class2()
{
}
public double GetValue(Class2 value1, int value2)
{
return 123;
}
}
}
}
控制台的输出是:
一目了然,控制台清楚的告诉了我被测试的方法是什么,返回值是什么,期望值是什么,测试结果是什么。
这个就是assert 2.0 技术。
还没有缓过神来的兄弟再仔细对比一下和nunit的Assert.AreEqual有什么不同。
1. 名字不同,我压根就没搞明白什么用AreEqual,几乎99%的方法都是Is开头,非让我用个Are。
2. 我们不再需要写以下的测试代码:
Assert.AreEqual(123, xxxxx)
因为需要被测试的方法已经自动被打印出来了。
就这么简单的一步,估计让我们再次节省了50%的时间。
为了实现这个功能,让我忙了2天。具体的思路如下:
1. 在assert.isEqual里面,使用了 StackTrace,获取调用堆栈StackFrame,指向调用堆栈public void test001()和方法assert.isequal的IL偏移量。
2. 别高兴的太早,掌握IL的兄弟都知道, new Class2().GetValue(null, 3) 这段代码在调用堆栈是看不到了。
因为IL里面已经运行出了结果,把结果压入了计算栈.
所以我们只能获得 Assert.IsEqual和上一个栈是 public void test001().
3. 这里开始,就需要打开IL,查看assert.isequal的参数入栈情况。首先我们从“1”,得知了test001()这个方法,和assert.isequal的il偏移量,然后通过GetMethodBody().GetILAsByteArray();,获取test001的IL代码片段。
4. 翻译出assert.isequal对应的IL代码,获取入口参数个数(当然 = 2了,这不废话!)
5. 接下来在IL代码片段中,从assert.isEqual的il偏移量开始,反向搜索出压入栈的参数,例如: OpCodes.Ldcxxx / OpCodes.Ldarg / OpCodes.Newarr之类的。这里非常需要时间,由于我实在没有功夫把整本IL看完,只能自己写出一系列的测试代码去测可能的情况,到目前,还有部分情况无法覆盖到。
6. 只要发现压入的参数符合要求,就返回,例如是OpCodes.Call,我就知道这个参数是某个方法的返回值。
------------------
后续
------------------
大功告成!assert 2.0的核心原理就介绍到这里了。要实现TestDriven 2.0,是需要一点技术和时间的,希望有兴趣的兄弟多留言,给点思路。下一篇,我将解剖 NUnit的核心机制,然后扩展他的代码,成为UnitTest 2.0.
下篇再见。