对于Draw A Line函数的单元测试的几点想法和实践
上一篇 / 下一篇 2009-05-05 09:52:21 / 个人分类:软件开发相关
这篇文章起源于有位园友在笔者的博客上提问(怎样对于一个Drawing类做单元测试),因为没有像其它的很多类或者方法,我们可以通过几句Asser.AreEqual(expectedValue, actualValue);来设置断言。这一类的单元测试笔者的经验或者说经历是一片空白,所以很遗憾不能及时帮助那位园友解决问题,而只是按照传统的单元测试的想法提供了点点建议。
(t].q-I)H`-e/o8sP,t Y0N8^!s;|_B EBG3C Y0一般的单元测试51Testing软件测试网3j%WNsy+ky
?,G'T:J(U${-\:x0 有返回值的51Testing软件测试网%~5m#G A9B#gJV
!~R{n;a5X @0 一般而言,我们所要应对的单元测试是是来应付类似于public ObjectA SampleFunc(ObjectB param1, ObjectC param2){...},对于这类,应用Xunit系列工具就可以很轻松的摆平了。
a'_&[_ z051Testing软件测试网i)I"G-x"S c9P4z%?首先我们准备一批测试数据,包括输入和输出两块数据,然后一组输入数据inputs[]和一组期望中的输出数据expectedoutput对应,最后用上面提到的Assert就搞定了:
f4r:J,o#q3ve0L(^[(V B'y6H0 Assert.AreEqual(expectedoutput, new SampleFunc(inputs[0],inputs[1]))51Testing软件测试网DWu(s0_6an
51Testing软件测试网? J+^3rx然后泡上一壶茶,运行一下XUnit,绿了也就Ok了。
%Q"M_[3o!v l@051Testing软件测试网|ulV~M改变了某些变量的
5U0VL)\Y051Testing软件测试网w~5{9lr uzmK这一类的单元测试的对象——改变了某些变量的方法,一般它们没有返回值,但是它们会有一无意的流下了蛛丝马迹,比如改变了某个全局变量的值,或者给某个文件记录插上了一刀子留下了一道记录等等。例如某个方法啥也没干,除了向数据库添加了一条数据,自己还掩耳盗铃返回个void,跟没事的人似的。这类方法,XUnit系列也支持的很好,大不了咱去找受害者查证——查一查数据库看刚才添加的数据,然后来一个Assert一样完事。
mO6Fe7i'p6w051Testing软件测试网#eQ0[^3Og51Testing软件测试网5TYT7~3e'@.y
|)g&kJ1W bn ~hAl0今天要说的单元测试51Testing软件测试网8k'X!s*Q Sx
h_)uK3Y_0 几天要说的单元测试其实在开篇就已经泄露了天机——对Drawing相关的方法进行单元测试,也有人称之为对于GUI单元测试。笔者觉得这个名字怪怪的,因为之前经历的GUI测试大多是在功能自动化测试中才会遇到,但是要让我适应GUI单元测试这种说法,还是有点困难。为了更贴切的来说明这个问题,我取了个名字“Draw a line”单元测试。我们会写一个简单的方法,什么坏事都不干,就在墙壁上涂涂鸦——在WinForm窗口上画一条线,然后我们要对这个(类)方法进行单元测试。听起来很有意思,好像也不难,我开始也是这么想的,但是直到现在也没能找出一个让自己百分百满意的解决方案,不禁开始严重怀疑自己的水准~~51Testing软件测试网;E.z1XeK
51Testing软件测试网 k/xCP/Y)O+LEg对于这类单元测试怎么做呢?我当时的第一直觉就是——还真不晓得……于是,只好请教Google老师了,或许是自己搜商确实不高,只找到了几篇相关联的,然后其中几篇看不大明白,剩下几篇自认为看明白了的我整理了一下,然后加上一些自己傻瓜式的天真想法。51Testing软件测试网(V-K4e1s6~[wpK0b F:YK
;f2ylR'a%eJ~0 检查LOG法
7TfT/KwJ;sf0,G r3{Y1JDV-B0 把这种方法首先列出来并不是因为这种方法好,相反是因为我习惯先苦后甜,这么不靠谱的方法拿出来先让大家扔砖头考验自己的砖头承受力。这种方法在于把很难检查的画图过程文本化,然后检查文本来验证我们是怎样画的这个图。51Testing软件测试网#_s,h!X wP TT#m9v9V8w
51Testing软件测试网l r$g.`%[1@_例如:我们有这样一段代码:
}~qY#x051Testing软件测试网1Q:oF/b4J,~C protected void DrawALine(PaintEventArgs e)
(YX1m0z&`I2D
uy0 {
8}}G-]z2g0 Graphics g = e.Graphics;51Testing软件测试网,E({2K8B,{ N
myPen.Width = 5;51Testing软件测试网mr@7J+H%seb&V
g.DrawLine(myPen, 16, 27, 38, 49);
-e$d)`My/`rw,k#Ep0 }
这个方法做的事情是画了一条宽度为5的线,起点是(16,27)而终点是(38,49),对于这个方法,我们可以在末尾加上一段代码:51Testing软件测试网*IJ1@c {v
#Tb1G}?/w0 protected void DrawALine(PaintEventArgs e)51Testing软件测试网]!oC#w!F2X{O
{
JE k:~'D;_X;]0 Graphics g = e.Graphics;
8g5w#S0P4NjE/h:h0 myPen.Width = 5;51Testing软件测试网ka5@G,Rf
g.DrawLine(myPen, 16, 27, 38, 49);51Testing软件测试网4c6OV4A%F:Ufz
VU}
0UT:e4hH9s-X3Z0L0 ActionLog("Line(5)(16,27,38,49)");//在log中记录下来我们画了一条线,宽度是5,坐标值是(16,27,38,49)
+N4W5GAu0 }51Testing软件测试网FUXKLA0G
后面的事情就简单了,在测试方法中运行相应的方法,然后再读取Log看一下Log中是不是做了我们期望做的事。
MRl s7fHr5ay#[0-B'V3? ?*fm9B}0 额,看到这里一定有人在开始骂“不知者无畏”了,其实也是,这种方法不仅傻,而且没有一点点实际用处,也说明不了任何问题。我绞尽脑汁凑了这么个应用场景:
MYF,Su }}G0/Xk*]L.bPN!bU$H7P0 //画一个三角形51Testing软件测试网HW tS:hTy9[
wxOm
d M\:A~g0 protected void DrawATri(PaintEventArgs e)51Testing软件测试网M T.hySnS
{51Testing软件测试网7nC%n6LVR3O
Graphics g = e.Graphics;51Testing软件测试网G!rp6Jp~
myPen.Width = 5;
7r6`0fTt4p
T0 g.DrawLine(myPen, 16, 27, 38, 49);
2|6J_:~ V/C0 ActionLog("Line(5)(16,27,38,49)");//在log中记录下来我们画了第1条边,宽度是5,坐标值是(16,27,38,49)
-z cbPsKb051Testing软件测试网#iFP7X#Xvmg.DrawLine(myPen, 38, 49, 54, 49);51Testing软件测试网^]Q{h6vT
5Af dGd$E#[5`1s|u0 ActionLog("Line(5)(38, 49, 54, 49)");//在log中记录下来我们画了第2条边,宽度是5,坐标值是(16,27,38,49)51Testing软件测试网!m^Fm4Ge
,~0xi0sS!H&s0 g.DrawLine(myPen, 54, 49, 16, 27);51Testing软件测试网e,W4NBS.J8K3R
51Testing软件测试网.B?!uI(OXy+B` ActionLog("Line(5)(54, 49, 16, 27)");//在log中记录下来我们画了第3条边,宽度是5,坐标值是(54, 49, 16, 27)51Testing软件测试网l,]5k8hHptv D
}51Testing软件测试网 G'S)bR5j
c}^
S!n1U*@2X&Qoi2m#@0 这样我们在测试代码中先读出log中的内容,然后逐步检查我们干了什么—画三条边的先后顺序,甚至我们还可以检查这三条边是不是一样宽,这三条边是不是连接到了一起(检查任一边的两端点与另两边中的一个端点相同)。总之,我们是将我们在代码中做的不可见测试的代码用文本的形式表现了出来,然后我们就可以利用XUnit工具来Assert了,绿了,然后完事了。51Testing软件测试网OS"f-J\xs7Ou6OIru
51Testing软件测试网9kF ^ _*D可是……不得不面对的是,这种方法确实局限性太大,而且也太明显——这种方法有N大缺点:
\P _+FLw\0M051Testing软件测试网 q(V-a3{5eC4r&p1) 太不保险 我们甚至不能保证我们的代码是不是真的按照Log中写的那样做了,很可能并没有画出线却在Log中一本正经报告给我们已经画了第2条边,宽度是5,坐标值是(16,27,38,49)之类的东西,这种虚假情报很可能会害死人的。51Testing软件测试网2vRnN*XA3D
ge4Pf6W:v ~ g1u8|0 2) 也太复杂 Assert的时候实在复杂,光是检查三条边是不是一样粗就需要两个Assert,检查三条边是不是连到了一起又需要两个Assert。。。51Testing软件测试网mFf{ LLCu
U1X'd\ D7Wa0 3)代码依赖性 乍一看无非是加上一点点log,可是我们善后的时候可能还要加上条件编译,以防止太多的垃圾信息占用了资源,这些都要修改源代码,相信没有多少人愿意去做这件事情;另外当源代码的数量增多的时候,复杂度也就相应地增加了。
\5E^Gg2V0l9P,`@RuM G0 刚才已经绞尽了的脑汁再拿来绞一次,想2条这种方法的好处:
:Y9]R.i6JzS{0TO]~-Z'R sa0 1) 告诉我们我们的代码是怎样做的,而不是做了什么(也就是How而不是What)51Testing软件测试网_\ b+X6Y7n$W
51Testing软件测试网Gnlq$l`?2) 给我们一点点自欺欺人的自我安慰(我们这些代码是做了测试的。。。你看测试覆盖率摆在那里的)51Testing软件测试网-U zc+u-w{H
(N T~H1SI"a0 改进版Log法51Testing软件测试网QEk%x(jJX
E7J u8f/K{2h L0 这种方法之所以称之为改进版主要是因为我们采用了让代码自己记录行为的方法——我们需要写一个新的Graphics的子类,就叫做CustomGraphics吧,然后重写其中的一些方法,比如在DrawLine方法,我们需要可能需要这样干:
o6vL2P] D3OLH3z/O2v t051Testing软件测试网*hOK'cOYEH Ypublic override void DrawLIne(int x1, int y1, int x2, int y2)51Testing软件测试网TT*^^T"ZGT
8AI:hi)H2R0 {
1? Z5pH8@GM:Ok051Testing软件测试网8H3Yt)t.P4^.ezbase.DrawLine(int x1, int y1, int x2, int y2);51Testing软件测试网G*Ue y9H^
51Testing软件测试网u)Z)v/jHI0Y$J*UActionLog("DrawLIne(int " + x1
Q[6wo6o omgVT k0;ky5K ^*L#F:n0 +", int " + y1
1Nk_G0C2@0(Z[&H)^~L8}^0 +", int " + x251Testing软件测试网4W`{?"|:yCt
6AX5nTC%zEQ0 +", int " + y2
l|6YBU}051Testing软件测试网\*ff!p;G.o#} [;Ypl+")");51Testing软件测试网1AG#d/}Ql/SA!_(oY"O
io z"iw)Or5m0 }
OD9dty-i7| m0?0G!_B(m| Ea"j0 或许这个样子会更好:51Testing软件测试网D+u-R-{I.Do0\:h
` KZ*jU!g)y0 public string ActionSaveAsText = "Do nothing";51Testing软件测试网mu:jbDC'v ~6B
Sc"xC*lM Q0 public override void DrawLIne(int x1, int y1, int x2, int y2)51Testing软件测试网X6Aj+Ed3F'H6T.h4Q
51Testing软件测试网S[sq-mp P{
,y:x?c2WB051Testing软件测试网P%D6~1W5Hsz+X#Jbase.DrawLine(int x1, int y1, int x2, int y2);