零基础学软件测试 2010-11-14
测试方法存在几个问题:
l 如果测试不够详尽,那么bug就会遗留在代码中并潜在地造成严重的问题。
l 测试常常在所有代码编写完毕后编写,难以回头处理程序中的一些问题。
l 测试经常不是由编写代码的程序员编写,所以漏掉一些重要的测试时很有可能的。
l 如果测试编写人员依赖文档或其他东西而不是代码的话,当这些材料与代码不一致就会造成问题。
l 如果测试不是自动进行,它们极有可能不回被经常性地运行。
l 传统的纠正错误的方法极有可能在别的地方引入错误。
测试驱动开发解决了所有这些问题,还附带解决了其他一些问题:
l 在测试驱动开发中,由程序员来完成测试,在代码还在脑海中清晰可见的时候,对测试进行操作。代码是基于测试编写的,这保证了代码可测试性,有助于确保测试覆盖的完备性以及代码与测试的一致性。所有测试是自动的。我们频繁地以完全相同的方式运行测试。
l 全面彻底的测试覆盖意味着如果在调试阶段引入某个bug,测试集就能立刻发现并查明其位置。测试—调试周期就会被控制在相当短的时间内。
l 当系统发布时,详尽的测试集与其一同发布,从而使得将来对程序的修改和扩展更加容易。
获得的是测试得更彻底的代码。你的设计会更加简单,系统(自己)能够清晰地表达你的意图。测试本身就有助于对系统进行描述。你获得的是瑕疵率极低的系统,而且从始至终都是健壮的。
什么是测试驱动开发?
测试驱动开发(Test-Driven Development,TDD)是一种开发方式:
l 你要维护一套详尽的程序员测试集。
l 除非存在相关测试,否则不编写任何产品代码。
l 首先编写测试。
l 由测试决定需要编写怎样的代码。
编写单元测试是为了测试你所编写的代码是否能够工作,而编写程序员测试是为了定义代码工作的含义。
极限编程的原则之一就是在与之相关的一套测试还没有被编写出来之前,不要编写任何功能代码。这样做的原因是系统中一切都必须是可测试的。
先编写少量的测试,随后编写足够使其测试通过的代码,然后再多编写一些测试,接着再编写一些代码,编写测试,编写代码,编写测试,编写代码,等等以此类推。
只编写足够让测试通过的代码,编写最简单的但又能工作的代码。
什么是重构?
对如何做而不是做什么进行修改。
何时进行重构?
l 当存在重复的时候。
l 当我们觉得代码或代码所表达的意图不清楚的时候。
l 当我们察觉代码有味儿(code smells)的时候,即存在某种微妙的表明可能存在问题的迹象。
把重复的代码抽取到一个独立的方法中。如果重复出现在继承层次中,我们或许能够将重复代码推送到更高一级的层次上。如果出现重复的是某些代码的结构而不是具体细节,我们可以将不同的部分抽取出来,并把公共结构部分制作成模板方法。
采用测试驱动开发能够帮助我们清晰地表达自己的意图,因为在编写代码的时候,我们强迫自己考虑类的接口而不是其实现。我们有机会站在这个类的用户的立场上来判定什么才是最有意义的,而不是陷入具体的实现细节中去。
如果你觉得有必要编写一条注释的话,请首先考虑重构或重写代码。注释传达的是为什么这样做而不是怎么做。
一个类操作另一个类实例,需要把这两者加以合并,要么把实例移动到该类中,要么把行为移动到该类中。一个类对另一个类的内部细节知道的越少越好。让那些彼此了解的代码处于同一个位置。
classPoint
{
public:
Point(int x, int y);
int GetX() const;
int GetY() const;
void SetX(int x);
void SetY(int y);
void Translate(intdX, int dY);
private:
int _x, _y;
};
classShape
{
public:
Shape();
void Translate(intdX, int dY);
private:
Point_center;
};
voidShape::Translate(int dX, int dY)
{
/*_center.SetX(_center.GetX() + dX);
_center.SetX(_center.GetY() + dY);*/
_center.Translate(dX, dY);//降低耦合度
}
继承中也会同样的问题,子类过多地了解其祖先类的实现细节,超出了他们应该了解的。可以通过把继承换成委派或将祖先类的具体细节置为私有的而使这种关系松散化,即去耦。
类的尺寸过大,可以提取出子类并且采用多态。方法最好不能超过10行左右。当判断较多时可以使用多态。
如何进行重构?
提取类
当类太大或其行为逻辑组织分散时,将其切分成多块内聚的行为并在需要的情况下创建新类。把某组行为抽取到一个新类中去。需要某种行为的多重实现时,把易变的代码拆分到一个独立的类中,从这个类中提取接口,分别编写所要求的实现。
提取接口
对具体的实现进行抽象,以便更容易地使用一种称作模拟对象(Mock Objectz)的技术。
提取方法
当方法太长或逻辑过于复杂而不易理解时,将其中某些部分提取出来而形成各自独立的方法,将每种不同的功能代码拆分到各自的方法中。
用子类来代替类型代码
对每种类型分别设计子类,避免复杂的条件判断和switch语句。
用多态来代替条件判断
形成模板方法
在多个类中都有某种具有相同结构但不同细节的相似方法时,把这个具有相同结构的方法安置在超类中,并把差异化代码提取到独立的方法中并在子类中具体实现。多态会负责调用恰当的具体方法。
引入解释变量
把复杂的表达式切分成较为简单的片段,并使用命名良好的变量来更好地传递其中的意图。
使用工厂方法来代替构造方法
所有构造方法具有相同的名字,容易让人搞不清楚,对此我们给每个这样的方法起一个有意义的名字,换成工厂方法(静态方法),并将构造方法设置成私有的。
ClassRating
{
public:
Rating(int value)
{
Rating(value,“Anonymous”, “”);
}
Rating(int value, string source)
{
Rating(value,source, “”);
}
Rating(int value, string source, string review)
{
_value= value;
_source= source;
_review= review;
}
private:
int _value;
string_source;
string_review;
};
//工厂方法
classRating
{
public:
static RatingnewAnoymousRating(int value)
{
Rating(value,“Anonymous”, “”);
}
static Rating newRating(intvalue, string source)
{
Rating(value,source, “”);
}
static Rating newReview(intvalue, string source, string review)
{
Rating(value,source, review);
}
private:
Rating(int aRating, string aRatingSource, string aReivew);
};
使用委托来代替继承
只有当子类是特殊种类的超类,或子类对超类进行扩展而不仅仅是覆写超类的部分功能时,才使用继承。
使用符号常量来代替魔幻数字
使用命名良好的符号常量,需要改动时,到一个地方修改就行了。
使用卫述句来代替嵌套的条件判断
卫述句执行一种非常简单的动作,典型的是return语句。
Intfib(int i)
{
int result;
if (0 == i)
{
result= 0;
}
else if (I <= 2)
{
result= 1;
}
else
{
result= fib(I – 1) + fib(I –2);
}
return result;
}
intfib(int i)
{
if (0 == i) return 0;
if (I <= 2) return1;
return fib(I –1) + fib(I – 2);
}
意图导向的编程
名字
使用名词或名词短语作为类的名字
使用形容词或具有一般性的名词或名词短语来为接口命名
通常以-able来结尾,避免用字母”I”开头或结尾。
使用动词或动词短语作为方法名
使用公认的取值方法和赋值方法的命名习惯
如getX和setX来取得或修改一个名为x的变量。当你想要的值是对象的属性而不是某一项特征时往往去掉get前缀更清楚,如size()而不是getSize()。
不要在方法名中保留多余的信息
如果一个用来增加X实例的方法,一般会为这方法取名为add而不是addX,因为这个方法的参数就是一个X。这样做的好处是如果将来重构改变了参数的类型,方法名也不会与其参数脱节。
使用名词或名词短语作为变量名
简单
怎么简单怎么来
通过重构来使代码保持简单
用做有根据的假设
在我们编写测试(以及后来编写真实代码)的时候做些有意义而且可读性强的假设。
“不要注释”与正当的注释