Aaron的测试生活小说

半两五钱,笃志向前
   :: 首页 :: 新随笔 :: 联系 ::  :: 管理

      这篇文章起源于有位园友在笔者的博客上提问(怎样对于一个Drawing类做单元测试),因为没有像其它的很多类或者方法,我们可以通过几句Asser.AreEqual(expectedValue, actualValue);来设置断言。这一类的单元测试笔者的经验或者说经历是一片空白,所以很遗憾不能及时帮助那位园友解决问题,而只是按照传统的单元测试的想法提供了点点建议。

一般的单元测试

     有返回值的

      一般而言,我们所要应对的单元测试是是来应付类似于public ObjectA SampleFunc(ObjectB param1, ObjectC param2){...},对于这类,应用Xunit系列工具就可以很轻松的摆平了。

      首先我们准备一批测试数据,包括输入和输出两块数据,然后一组输入数据inputs[]和一组期望中的输出数据expectedoutput对应,最后用上面提到的Assert就搞定了:

      Assert.AreEqual(expectedoutput, new SampleFunc(inputs[0],inputs[1]))

然后泡上一壶茶,运行一下XUnit,绿了也就Ok了。

    改变了某些变量的

    这一类的单元测试的对象——改变了某些变量的方法,一般它们没有返回值,但是它们会有一无意的流下了蛛丝马迹,比如改变了某个全局变量的值,或者给某个文件记录插上了一刀子留下了一道记录等等。例如某个方法啥也没干,除了向数据库添加了一条数据,自己还掩耳盗铃返回个void,跟没事的人似的。这类方法,XUnit系列也支持的很好,大不了咱去找受害者查证——查一查数据库看刚才添加的数据,然后来一个Assert一样完事。

 

今天要说的单元测试

       几天要说的单元测试其实在开篇就已经泄露了天机——对Drawing相关的方法进行单元测试,也有人称之为对于GUI单元测试。笔者觉得这个名字怪怪的,因为之前经历的GUI测试大多是在功能自动化测试中才会遇到,但是要让我适应GUI单元测试这种说法,还是有点困难。为了更贴切的来说明这个问题,我取了个名字“Draw a line”单元测试。我们会写一个简单的方法,什么坏事都不干,就在墙壁上涂涂鸦——在WinForm窗口上画一条线,然后我们要对这个(类)方法进行单元测试。听起来很有意思,好像也不难,我开始也是这么想的,但是直到现在也没能找出一个让自己百分百满意的解决方案,不禁开始严重怀疑自己的水准~~

      对于这类单元测试怎么做呢?我当时的第一直觉就是——还真不晓得……于是,只好请教Google老师了,或许是自己搜商确实不高,只找到了几篇相关联的,然后其中几篇看不大明白,剩下几篇自认为看明白了的我整理了一下,然后加上一些自己傻瓜式的天真想法。

     检查LOG法

     把这种方法首先列出来并不是因为这种方法好,相反是因为我习惯先苦后甜,这么不靠谱的方法拿出来先让大家扔砖头考验自己的砖头承受力。这种方法在于把很难检查的画图过程文本化,然后检查文本来验证我们是怎样画的这个图。

    例如:我们有这样一段代码:

     protected void DrawALine(PaintEventArgs e)
        {
            Graphics g = e.Graphics;
            myPen.Width = 5;
            g.DrawLine(myPen, 16, 27, 38, 49);
        }

      这个方法做的事情是画了一条宽度为5的线,起点是(16,27)而终点是(38,49),对于这个方法,我们可以在末尾加上一段代码:

     protected void DrawALine(PaintEventArgs e)
        {
            Graphics g = e.Graphics;
            myPen.Width = 5;
            g.DrawLine(myPen, 16, 27, 38, 49);

             ActionLog("Line(5)(16,27,38,49)");//在log中记录下来我们画了一条线,宽度是5,坐标值是(16,27,38,49)
        }

      后面的事情就简单了,在测试方法中运行相应的方法,然后再读取Log看一下Log中是不是做了我们期望做的事。

      额,看到这里一定有人在开始骂“不知者无畏”了,其实也是,这种方法不仅傻,而且没有一点点实际用处,也说明不了任何问题。我绞尽脑汁凑了这么个应用场景:

      //画一个三角形

      protected void DrawATri(PaintEventArgs e)
        {
            Graphics g = e.Graphics;
            myPen.Width = 5;
            g.DrawLine(myPen, 16, 27, 38, 49);

             ActionLog("Line(5)(16,27,38,49)");//在log中记录下来我们画了第1条边,宽度是5,坐标值是(16,27,38,49)

            g.DrawLine(myPen, 38, 49, 54, 49);

             ActionLog("Line(5)(38, 49, 54, 49)");//在log中记录下来我们画了第2条边,宽度是5,坐标值是(16,27,38,49)

            g.DrawLine(myPen, 54, 49, 16, 27);

             ActionLog("Line(5)(54, 49, 16, 27)");//在log中记录下来我们画了第3条边,宽度是5,坐标值是(54, 49, 16, 27)
        }

     这样我们在测试代码中先读出log中的内容,然后逐步检查我们干了什么—画三条边的先后顺序,甚至我们还可以检查这三条边是不是一样宽,这三条边是不是连接到了一起(检查任一边的两端点与另两边中的一个端点相同)。总之,我们是将我们在代码中做的不可见测试的代码用文本的形式表现了出来,然后我们就可以利用XUnit工具来Assert了,绿了,然后完事了。

      可是……不得不面对的是,这种方法确实局限性太大,而且也太明显——这种方法有N大缺点:

     1) 太不保险  我们甚至不能保证我们的代码是不是真的按照Log中写的那样做了,很可能并没有画出线却在Log中一本正经报告给我们已经画了第2条边,宽度是5,坐标值是(16,27,38,49)之类的东西,这种虚假情报很可能会害死人的。

     2) 也太复杂  Assert的时候实在复杂,光是检查三条边是不是一样粗就需要两个Assert,检查三条边是不是连到了一起又需要两个Assert。。。

     3)代码依赖性  乍一看无非是加上一点点log,可是我们善后的时候可能还要加上条件编译,以防止太多的垃圾信息占用了资源,这些都要修改源代码,相信没有多少人愿意去做这件事情;另外当源代码的数量增多的时候,复杂度也就相应地增加了。

     刚才已经绞尽了的脑汁再拿来绞一次,想2条这种方法的好处:

     1) 告诉我们我们的代码是怎样做的,而不是做了什么(也就是How而不是What)

     2) 给我们一点点自欺欺人的自我安慰(我们这些代码是做了测试的。。。你看测试覆盖率摆在那里的)

    改进版Log法

    这种方法之所以称之为改进版主要是因为我们采用了让代码自己记录行为的方法——我们需要写一个新的Graphics的子类,就叫做CustomGraphics吧,然后重写其中的一些方法,比如在DrawLine方法,我们需要可能需要这样干:

     public override void DrawLIne(int x1, int y1, int x2, int y2)

     {

           base.DrawLine(int x1, int y1, int x2, int y2);

           ActionLog("DrawLIne(int " + x1

                                       +", int " + y1

                                       +", int " + x2

                                       +", int " + y2

                                        +")");

     }

     或许这个样子会更好:

     public string ActionSaveAsText = "Do nothing";

     public override void DrawLIne(int x1, int y1, int x2, int y2)

     {

           base.DrawLine(int x1, int y1, int x2, int y2);

           ActionSaveAsText ="DrawLIne(int " + x1

                                       +", int " + y1

                                       +", int " + x2

                                       +", int " + y2

                                        +")";

     }

     之后其他的事情跟Log法中一样,这样做显然会极大程度上减少我们在源代码中的ActionLog的数量,但是我们同样需要修改代码:我们要使用CustomGraphics而不是以前的Graphics类,我们还要保证我们使用过的方法中都增加了相应的 ActionSaveAsText  = "...";方法,这样看来,我们同样轻松不了多少。

     Notes:额,可能前面我给大家传递了错误的信息吧,其实我的测试对象是自己写的一个函数DrawALine,那个函数可以接收各种参数,然后画出一条线,而不是说我去测试.net中已有的DrawLine方法,这个方法当然是不需要我们去测试的。或许我应该改成DrawTwoLines来测试,这样就不会引起误会了~~

 

     截图法

     好吧,上面的话已经讲完了,想扔砖头的使劲扔过来吧,我正好没钱买房子,说不定可以凑点砖头自己垒起来一层~~接下来介绍的,看起来是另外一个麻烦的东西。从名字上看,你可能会想说:是不是就是画完图之后把图片接下来然后比较图片啊。如果你这么想的话,那么恭喜你,我确实是这样想的——刚才脑汁已经绞尽了两次了,所以也想不出什么更好的办法了,只能拿这个方法来凑一凑数。

     大体思路是这样的:

    步骤1:运行待测方法DrawALine,在Winform窗口上画条线

    步骤2:检查画的线是否正确

    步骤3:保存刚才画的线为图片形式

    步骤4:在测试代码中重复上述操作,并将测试代码中得到的图片跟手工运行(调试运行)得到的图片进行比较

    上述步骤其实跟我们常见的单元测试过程很接近(当然这里不包括TDD中的测试,而是传统的流程中先代码后测试的顺序)。难道我们找到银弹了,难道事情就这样被解决了?显然不会这么简单。。。不过为了安慰一下我和大家受伤的心灵,我们先来展望这种方法好处:

     1) 准确性提高   如果是两个一模一样的图片,自然我们画的图就是对的了,而且我们还可以拿截下来的图好好瞅瞅,随机抽查一下我们的DrawALine上不上镜,是不是照的艺术照给照得面目全非了——毕竟我们需要的是Draw出来的是A 真是的Line。

     2) 源代码的修改少  这是另外一个很大的优势。试想我们只是运行一下DrawALine方法,DrawALine方法帮我们把线画出来,然后我们拍个照带走跟之前的标准像比较就成,不需要在源代码中作额外的事情。

     看到上面的展望,心情有点激动,似乎真的是银弹,不费吹灰之力就摆平了,很好很强大。。。Ok,在YY过后我们应该清醒过来了,我们还要事先预估一下可能遇到的问题以及这种方法的缺陷:

     1)  截出想要的图   我们能不能在运行了DrawALine方法之后顺利的截出我们想要的图,而且要跟标准像大小位置一致,这个需要再想一想。

     2) 图片比较   要比较两张图是不是完全一样

     3) 图片保存   标准像怎么保存,不同的方法不同的用例的标准像要怎么保存?而测试运行过程中的图片是否要保存下来以供抽查,这个又怎么保存?

     4) 运行速度   对于单元测试来讲,我们希望很快运行完毕,而我们的这种办法是不是存在效率地下的毛病?

    Mock法

     对于很多人来讲,Mock也许是解决此类问题的不二法宝了吧:)就连很多介绍mock的文章中提到mcok的最佳应用环境时候,都不忘提到UI相关的单元测试是它的强项。关于Mock法,自然也有一些它的劣势,比如过于死板的一条道儿走到黑——对原代码的实现方式依赖性较大,而且Moss也不能满足某些人的需要,让他们亲眼看到这个程序真实的运行效果。

     总的来讲,正如楼下兄弟说的那样,Mock应该是最佳解决方案了,至于上面的截图法可能略显得臃肿,也显得更像是功能测试而非轻便的单元测试了。

总结

     断断续续整了好几天,终于整了一点点眉目,对于第三种截图法,笔者也已经有了一些初步的代码,相信在下一篇文章中可以拿出来跟大家一起分享。不过稍显遗憾的是,笔者没能找出其他的更方便的方法,比如是不是存在这样一种方法,把DrawALine的结果也可以描述成对象,然后直接比较对象的属性;亦或者其他的笔者想都没有想到的地方和方法?对于上述介绍的方法,欢迎大家扔砖~~

后记

      由于本本上面的开发环境崩溃的原因,本文中的代码并未在IDE中调试,代码中可能有一些因疏忽导致的纰漏甚至弱弱的语法错误,而代码的颜色也不如VS中那么容易辨认,给大家阅读造成的困难敬请原谅~~

Feedback

#1楼  回复 引用 查看   

2009-05-04 00:35 by 温景良(Jason)      
收藏

#2楼[楼主]  回复 引用 查看   

2009-05-04 00:48 by Aaron Wu      
@温景良(Jason)
期望大家不吝指教~

#3楼  回复 引用 查看   

2009-05-04 02:45 by DiryBoy      
能不能在测试时注入CustomGraphics对象,这个CustomGraphics将所有绘制操作输出到一个Bitmap中,完了再跟标准的Bitmap对比?

#4楼  回复 引用   

2009-05-04 08:18 by shangfc[未注册用户]
是不是可以使用关键点检查的方式。比如检查线的起点和终点,和中间位置的颜色。类似于痕迹检查的形式?

#5楼  回复 引用   

2009-05-04 09:10 by LOGOLS[未注册用户]
google mock-framework
please

#6楼[楼主]  回复 引用 查看   

2009-05-04 10:07 by Aaron Wu      
@DiryBoy
其实我在想能不能尽量在不更改源代码的前提下达到目的。

#7楼[楼主]  回复 引用 查看   

2009-05-04 10:09 by Aaron Wu      
@shangfc
这个方法可行性还好,但是如果碰到了画圆,就又要做相应的变通,如果是画曲线,双曲线,抛物线,画长方形等等,这样看来扩展性就糟糕了。

#8楼[楼主]  回复 引用 查看   

2009-05-04 10:10 by Aaron Wu      
@LOGOLS
我马上去看,谢谢推介。

#9楼  回复 引用   

2009-05-04 10:21 by Jacky Zhang[未注册用户]
这样做UnitTest 真的要死人的.... Unit Test是test你自己的代码, 不是要test别人的代码, 尤其是系统的代码, 所以你要test的是你确定调用了Api的某个函数, 然后传入了某个正确的参数, 就可以了, 至于API是不是真的画了, 不是你要Unit Test的. 所以用Moch Framework吧.

#10楼[楼主]  回复 引用 查看   

2009-05-04 10:26 by Aaron Wu      
@LOGOLS
@Jacky Zhang
一语惊醒梦中人,我用NMock搞一下就行。~~

#11楼[楼主]  回复 引用 查看   

2009-05-04 10:28 by Aaron Wu      
--引用--------------------------------------------------
Jacky Zhang: 这样做UnitTest 真的要死人的.... Unit Test是test你自己的代码, 不是要test别人的代码, 尤其是系统的代码, 所以你要test的是你确定调用了Api的某个函数, 然后传入了某个正确的参数, 就可以了, 至于API是不是真的画了, 不是你要Unit Test的. 所以用Moch Framework吧.
--------------------------------------------------------
额,可能是我在文章中给大家传递了错误的信息吧,其实我的测试对象可能是自己写的一个函数,那个函数可以接收各种参数,然后画出一条线,而不是说我去测试.net中已经写好的DrawLine方法

#12楼  回复 引用 查看   

2009-05-04 10:42 by HCOONa      
我觉得应该参考老赵的那篇尽量解除对HttpContent的依赖文章
如果要测试你的那个函数,首先得解除对Graphics的依赖,然后用Mock做单元测试就可以了

#13楼  回复 引用 查看   

2009-05-04 10:56 by DiryBoy      
--引用--------------------------------------------------
Aaron Wu: @DiryBoy
其实我在想能不能尽量在不更改源代码的前提下达到目的。
--
其实我的测试对象可能是自己写的一个函数,那个函数可以接收各种参数,然后画出一条线
--------------------------------------------------------
我的意思是说
class YourDrawingClass
{
private Graphics g;
public YourDrawingClass( Graphics g )
{
this.g = g;
}
public void DrawALine(...)
{
// Draw using g
}
}
实际用就传入上下文的 Graphics 对象,测试就用 CustomGraphics,这个CustomGraphics 不调用 base 的成员。跟 Mock 的道理差不多吧?

#14楼  回复 引用 查看   

2009-05-04 11:03 by Nick Wang      
你要测得逻辑实际上是程序是否正确调用了GUI的API来画线。Jacky Zhang说的很对,你需要测得是你自己的代码。在这里画一条线似乎太简单,没什么可测得,你可以设想画一个比较复杂的图形,比如五角星。那么逻辑上可能就要根据五角星的五个顶点的坐标画一组直线。
这样你的类或方法应该有一个对API函数的依赖,将这个依赖用mock来做,就可以从Mock里检查画线是否正确(数量,坐标)。
但是其实这个方法不是太好,因为对实现细节有所依赖,如果有一天你不用画直线的方式而是直接用画多边形的API来画五角星(重构),测试就要跟着变,而且会变得和代码差不多,这时候测试就没啥意义了。
至于更好的办法,还没想到。。。。

另外,很多人都用比较简单的代码来学习写测试,但是实际上,越是复杂的逻辑才越能体现测试的作用,简单的反而不太需要测试,从而也看不出测试的好处了。

#15楼[楼主]  回复 引用 查看   

2009-05-04 11:06 by Aaron Wu      
@HCOONa
使用Mock的话,对于我们上面的代码Public void DrawALine(),即使我们能Mock到调用这个方法,可是由于它没有返回值,没有改变其他的东西,我们很难捕捉到这个函数是否被正确执行了。也就是说我们在Assert的时候,很难找到两个用来比较的对象。

#16楼  回复 引用 查看   

2009-05-04 11:10 by Nick Wang      
--引用--------------------------------------------------
Aaron Wu: @HCOONa
使用Mock的话,对于我们上面的代码Public void DrawALine(),即使我们能Mock到调用这个方法,可是由于它没有返回值,没有改变其他的东西,我们很难捕捉到这个函数是否被正确执行了。也就是说我们在Assert的时候,很难找到两个用来比较的对象。
--------------------------------------------------------
mock有verify来检查方法是否被调用过,以及是否参数正确

#17楼[楼主]  回复 引用 查看   

2009-05-04 11:12 by Aaron Wu      
@Nick Wang
Mock的应用就是太过于一条筋了~

对于使用简单的代码或复杂的代码学习测试,我一般找现成的代码而不谁专门写一个public int Add(int num1, int num2){return num1 + num2;}这种方法来测试,但是要把东西写出来,为了大家理解上的方便,就只能说是越简单越好……

#18楼[楼主]  回复 引用 查看   

2009-05-04 11:15 by Aaron Wu      
@Nick Wang
但是有人就是需要看到我们的函数是不是取得了相应的效果,然后要看得到才放心……
尽管个人认为这已经偏离了单元测试的范畴而更像是GUI测试或者说功能测试了,尽管我也是相信.NET里面的那些函数是绝对不会错的,但是就是有完美主义者希望看到~~

总的看来,Mock是一个我能接受的好的解决方案了。

#19楼  回复 引用   

2009-05-04 12:47 by tubo70[未注册用户]
应该用Mock的

#20楼  回复 引用 查看   

2009-05-04 13:45 by Nick Wang      
--引用--------------------------------------------------
Aaron Wu: @Nick Wang
但是有人就是需要看到我们的函数是不是取得了相应的效果,然后要看得到才放心……
尽管个人认为这已经偏离了单元测试的范畴而更像是GUI测试或者说功能测试了,尽管我也是相信.NET里面的那些函数是绝对不会错的,但是就是有完美主义者希望看到~~

总的看来,Mock是一个我能接受的好的解决方案了。
--------------------------------------------------------
那就不是单元测试了,可以作为功能测试,自动截图,人工比较