关于TDD--以测试为主导的开发(0分)

  • 主题发起人 主题发起人 xfz8124
  • 开始时间 开始时间
X

xfz8124

Unregistered / Unconfirmed
GUEST, unregistred user!
  学习TDD已有半年以上了,一开始的时候,看完了Kent Back的《解析极限编程--掏腰包变化》这本书-中译本,以及《探索极限编程》等一些相关的TDD的书,也看了Kent Beck最新的一本《测试驱动开发》中译本,可能由于文化背景的差异吧,Kent举了个例子,但我看了却没多少味道,有些收获但是不大。同时,也在互联网上疯狂搜索关于TDD的资料,但很不幸,找到的资料少之又少,最流行的还是那个DUnit原带的说明文档,有个叫谢慧强的人做了翻译,但是像其中测试1+1是不是等于2这样的问题,对学习者的用处仅仅展示了DUunit何时报错及如何报错的问题。

  对比于1+1=2成立否这样的简单问题,在我们软件中的逻辑要复杂的多,那又怎么测试呢?而且更多的时候,测试的期望不能简单的用函数返回值来确定,因为大家也都知道,函数是有副作用的,在执行的过程中会修改一些东西,然后返回一个值,有时候我们在有意得利用这种副作用,如何测试?

基于组件的开发,对每个组件,作为黑箱来看,大体分两部分,输入和输出,输入表现为设定苦于属性值,调用一系列流程性函数,输出表现为对属性的读取,触发的事件,对磁盘文件系统的操作,等等,如何测试?

  你怎么证明一个控件的事件发生了呢,而且其中的实际参数值正是组件编写者期望认为的那样?比如,你怎么证明每个窗体在初始化时一定会触发OnCreate事件?当然你在Form.OnCreate里写上Showmessage也能行,但那不是TDD,TDD是不弹出消息的,并且基本上只需要非常少的鼠标操作,对每个TestCase,流程都是一步到位的。

  从网上对TDD资料存在情况看,国内外使用TDD的团队或个人并不多,但是Kent Back, Matin Fowler这些人,颠覆了我对软件开发的认识思想,有TDD开发经验的人可以剪烛西窗,或奇文共赏,本人在掌握了一些TDD开发经验之后,也在期待有人能够提出更有价值的问题,对没有听说TDD的人,或从来没有尝试过这种开发方式的,我不想理会。

  本人近期在开发一个教师测评卡算分模块,及动态FR报表模块,正在开发之中。评分模块基本定型,其TDD源码发于下(不含单元源码,仅提供DCU文件):


unit TestUnit_MarkSp;
{设计目标:评分单元测试
建立日期:2006.02.14
作者:月夜风筝
}

interface

uses
TestFramework, Variants, Classes, Dialogs, Windows, Unit_MarkSp, Forms, SysUtils, Controls,
StdCtrls, Graphics, Messages, IniFiles;
const
Cons_Codever = '1.13 @ 06.02.28';

type

TestTMarkSp = class(TTestCase)
strict private
FMarkSp: TMarkSp;
function VerifyCaseResult(AKeyFile: string; AMarker: TMarkSp): Boolean;
public
procedure SetUp; override;
procedure TearDown; override;
published
procedure Test_OnePaper_SurePos_perpendicular;
procedure Test_Twopaper_SurePos_perpendicular;
procedure Test_TwoPaper_DynamicPos_perpendicular;
procedure Test_TwoPaper_DynamicPos_MoreTeacher_perpendicular;
end;

function LoadCaseData(AInputFile: string; AMarker: TMarkSp): Boolean;


implementation

function LoadCaseData(AInputFile: string; AMarker: TMarkSp): Boolean;
const
Options = 'Options';
var
CardMen, CardAmt, CardItem: Integer;
ItemName, ItemKeys, ItemCosts: string;
TEvalDtList, TIDList: string;

i: Integer;
ini: TIniFile;
ReadString: string;
Sects, Idents, Values: TStrings;
EvalListStart, EvalListLen, IDListStart, IDListLen: Integer;
begin
Sects:= TStringList.Create;
Idents:= TStringList.Create;
Values:= TStringList.Create;

AMarker.ClearData;

ini:= TIniFile.Create(AInputFile);

CardMen:= ini.ReadInteger(Options, 'CardMen', -1);
CardItem:= ini.ReadInteger(Options, 'CardItem', -1);
CardAmt:= ini.ReadInteger(Options, 'CardAmt', -1);
AMarker.SetCardSizeAmount(CardMen, CardItem, CardAmt);

ini.ReadSection('ItemNames', Sects);
ini.ReadSection('ItemKeys', Idents);
ini.ReadSection('ItemCosts', Values);
for i:= 0 to Idents.Count -1 do
begin
ItemName:= Sects;
ItemKeys:= Idents;
ItemCosts:= Values;
ItemName:= ini.ReadString('ItemNames', ItemName, '');
ItemKeys:= ini.ReadString('ItemKeys', ItemKeys, '');
ItemCosts:= ini.ReadString('ItemCosts', ItemCosts, '');
AMarker.RegisItem(ItemName, ItemKeys, ItemCosts);
end;
AMarker.RegisDone;

EvalListStart:= ini.ReadInteger(Options, 'EvalListStart', -1);
EvalListLen:= ini.ReadInteger(Options, 'EvalListLen', -1);
IDListStart:= ini.ReadInteger(Options, 'IDListStart', -1);
IDListLen:= ini.ReadInteger(Options, 'IDListLen', -1);
ini.ReadSection('ReadStrings', Idents);
for i:= 0 to Idents.Count - 1 do
begin
ReadString:= Idents;
ReadString:= ini.ReadString('ReadStrings', ReadString, '');
TEvalDtList:= Copy(ReadString, EvalListStart, EvalListLen);
TIDList:= Copy(ReadString, IDListStart, IDListLen);
AMarker.AddPaper(TEvalDtList, TIDList);
end;
ini.Free;

AMarker.ExecCalc;

Values.Free;
Idents.Free;
Sects.Free;
end;

function TestTMarkSp.VerifyCaseResult(AKeyFile: string; AMarker: TMarkSp): Boolean;
var
i, j: Integer;
ini: TIniFile;
Sects: TStrings;

ATeacherID: Integer;
Verify_Score: Double;
Ret_Score: Double;
AKeyChar: Char;
Verify_HitCount: Integer;
Ret_HitCount: Integer;
begin
ini:= TIniFile.Create(AKeyFile);
Sects:= TStringList.Create;
ini.ReadSections(Sects);
for i:= 0 to Sects.Count - 1 do
begin
ATeacherID:= StrToInt(Copy(Sects, 4, 4));
Ret_Score := AMarker.GetScore(ATeacherID);
Verify_Score:= ini.ReadInteger(Sects, 'Score', -1);
if Ret_Score <> Verify_Score then
Fail('EspF:分数非预期!');

ATeacherID:= StrToInt(Copy(Sects, 4, 4));
for j:= Ord('A') to Ord('D') do
begin
AKeyChar:= Chr(j);
Ret_HitCount := AMarker.GetVotedCount(ATeacherID, AKeyChar);
Verify_HitCount:= ini.ReadInteger(Sects, AKeyChar, -1);
if Ret_HitCount <> Verify_HitCount then
Fail('EspF:命中数非预期!');
end;
end;
Sects.Free;
ini.Free;
end;

procedure TestTMarkSp.SetUp;
begin
FMarkSp := TMarkSp.Create;
end;

procedure TestTMarkSp.TearDown;
begin
FMarkSp.Free;
FMarkSp := nil;
end;

procedure TestTMarkSp.Test_OnePaper_SurePos_perpendicular;
begin
LoadCaseData(ExtractFilePath(ParamStr(0)) + 'Input/Case 0 - 虚拟数据(1张卡).ini', FMarkSp);
VerifyCaseResult(ExtractFilePath(ParamStr(0)) + 'Verify/Case 0 - 虚拟数据(1张卡)Key.ini', FMarkSp);
end;

procedure TestTMarkSp.Test_Twopaper_SurePos_perpendicular;
begin
LoadCaseData(ExtractFilePath(ParamStr(0)) + 'Input/Case 1 - 虚拟数据(2张卡).ini', FMarkSp);
VerifyCaseResult(ExtractFilePath(ParamStr(0)) + 'Verify/Case 1 - 虚拟数据(2张卡)Key.ini', FMarkSp);
end;

procedure TestTMarkSp.Test_TwoPaper_DynamicPos_perpendicular;
begin
LoadCaseData(ExtractFilePath(ParamStr(0)) + 'Input/Case 2 - 虚拟数据(2张卡).ini', FMarkSp);
VerifyCaseResult(ExtractFilePath(ParamStr(0)) + 'Verify/Case 2 - 虚拟数据(2张卡)Key.ini', FMarkSp);
end;

procedure TestTMarkSp.Test_TwoPaper_DynamicPos_MoreTeacher_perpendicular;
begin
LoadCaseData(ExtractFilePath(ParamStr(0)) + 'Input/Case 3 - 虚拟数据(2张卡).ini', FMarkSp);
VerifyCaseResult(ExtractFilePath(ParamStr(0)) + 'Verify/Case 3 - 虚拟数据(2张卡)Key.ini', FMarkSp);
end;

initialization
RegisterTest(TestTMarkSp.Suite);
end.

下面的是记事本文件Case 0 - 虚拟数据(1张卡).ini,也就是测试用例的输入内容
Case 0 - 虚拟数据(1张卡).ini
[Options]
CardMen = 15
CardItem = 9
CardAmt = 1
EvalListStart = 65
EvalListLen = 1000
IDListStart = 5
IDListLen = 60

[ReadStrings]
0 = A101000100020003000400050006000700080009001000110012001300140015ADBABDCBCADBADDDDDCBBCDADADCDAADCCCDDCCDADDCABDCDADABBCACDDBADDBDCDDAADADBAACCDCACAADCCCBCDDBBADDACBCABBCDADCAADCDABCADBDCDBCDDCABDDBBA

[ItemNames]
0 = a
1 = b
2 = c
3 = d
4 = e
5 = f
6 = g
7 = h
8 = i

[ItemKeys]
0 = A,B,C,D
1 = A,B,C,D
2 = A,B,C,D
3 = A,B,C,D
4 = A,B,C,D
5 = A,B,C,D
6 = A,B,C,D
7 = A,B,C,D
8 = A,B,C,D

[ItemCosts]
0 = 4,3,2,1
1 = 4,3,2,1
2 = 4,3,2,1
3 = 4,3,2,1
4 = 4,3,2,1
5 = 4,3,2,1
6 = 4,3,2,1
7 = 4,3,2,1
8 = 4,3,2,1

下面是Key文件Case 0 - 虚拟数据(1张卡)Key.ini,也是用于组件输出结果校验的文件,里面存储了预期理论值
Case 0 - 虚拟数据(1张卡)Key.ini
[ID_0001]
A=2
B=3
C=2
D=2
Score=23
[ID_0002]
A=2
B=1
C=0
D=6
Score=17
[ID_0014]
A=1
B=2
C=2
D=4
Score=18
[ID_0015]
A=2
B=3
C=1
D=3
Score=22
 
注意:在上面的源码中,凡包含AMarker调用的代码行,对测试来说,指明了所要开发的模块的特征
 
meiyou kan dong
 
等待高手的出现。。。。。。
 
你说的“对比于1+1=2成立否这样的简单问题,在我们软件中的逻辑要复杂的多,那又怎么测试呢?而且更多的时候,测试的期望不能简单的用函数返回值来确定”。

TDD 及xp中是这样说的, 写一点点测试, 再写一点点代码, 由测试来驱动开发,不是先写好代码,再去测试代码。 他们是颠覆了我们的认知。
李维的《面向对象程序设计实践-delphi版》 中, 讲述的是 先做好主要的设计, 或者先做一点设计, 然后测试你的设计。 这本书我还没有看完,但是我觉得这样似乎不够敏捷。 当然我也只是在学习, 我的工作中,基本上不敢大着胆子,写一点点测试,再写一点点代码。 顶多, 在有不确定的代码时,test一下。 主要可能是我顾虑我的时间,没有完全熟练应 用之前, 摸索可能降低我的效率。
Kent Back的《测试驱动开发》我看了前面几章, 我觉得还是不错,只是自己没有真正应用于实践, 尤其是它的重构部分, 我觉得,掌握重构,并有良好的 重构习惯。 才可以游刃有余地实践Tdd 。
蔽人愚见, 楼下斧正。
 
  以我认为,让TDD发挥效力,需要三种基本技能:设计或模式(一开始只要应付最简场景,通过重构向模式演化)、重构、测试本身也算一种基本技能。最先实践TDD的程序员,最大的困惑不是TDD实用不实用,而是如果设计测试体系结构和逻辑的问题--我这里指的不是TestFramWork,而是用TestFramWork所设计出来的体系结构和逻辑。
  他们是颠覆了我们的认知,事实确实如此,而这也是TDD的思想所必需。
  为什么“写一点点测试,再写一点点代码”,这种方式不敏捷?因为你对你的测试缺少信心。测试本身就是正确的吗?测试本身也是一项软件工程,其中有也Bug,按照TDD的理念,测试代码不需要写的太好,Ctrl-C,Ctrl_V就可以,等到太乱的时候就先重构测试再开发别的,但是你在开发的模块就要认真一些。
  如果测试和正在开发的模块都是这样同时一点一点的写,可以说,你很可能体会不到TDD的实用之处,因为,你在编码的时候,不知道是在设计测试,还是在设计模块,TDD模式下,你不能同时设计这两件事,当在设计模块时,测试在发挥“检错和辅助Debug”的功能,当在设计测试时,测试本身表达了一种需求分析--也就是Xp的User Story概念
  花时间摸索是值得的,一开始确实不会有明显收效,还可能使开发效率下降,但是认可TDD思想的人,都不应错过大师给我们带来的机会。
 
因为每个测试用例的流程都是一个Story,所以建议你在写测试的时候,可以一次写好几个测试,每个测试代表一项用途,每个测试用例的通行都对模块的进程来说是一个有纪念意义的里程,当你开发完这一轮测试,再升级你的测试,然后再去升级设计模块
 
接受答案了.
 
后退
顶部