1、介绍
2、目标
3、JUnit设计
3.1、从测试用例TestCase开始
3.2、在run()方法中填写方法体
3.3、用TestResult对象报告结果
3.4、No stupid subclasses - TestCase again
3.5、不用担心是一个测试用例还是许多测试用例-TestSuite
3.6、概要
4、结论
1、介绍
在较早的文章(Test Infected: Programmers Love Writing Tests)中,我们描述了如何用一个简单的框架编写可重复的测试;本文则说明这个框架是如何构造的。
仔细地学习JUnit框架,从中可以看出我们是如何设计这个框架的。我们看到不同层次的JUnit教程,但在本文中,我们希望更清楚地说明问题。弄清JUnit的设计思路是非常有价值的。
我们先讨论一下Junit的目标,这些目标会在JUnit的每个细小之处得到体现。围绕着JUnit的目标,我们给出Junit框架的设计和实现。我们会用模式和程序实现例子来描述这个设计。我们还会看到,在开发这个框架时,当然还有其它的可选途径。
2、目标
什么是JUnit的目标?
首先,我们回到开发的前提假设。我们假设如果一个程序不能自动测试,那么它就不会工作。但有更多的假设认为,如果开发人员保证程序能工作,那么它就会永远正常工作,与与这个假设相比,我们的假设实在是太保守了。
从这个观点出发,开发人员编写了代码,并进行了调试,还不能说他的工作完成了,他必须编写测试脚本,证明程序工作正常。然而,每个人都很忙,没有时间去进行测试工作。他们会说,我编写程序代码的时间都很紧,那有时间去写测试代码呢?
因此,首要的目标就是,构建一个测试框架,在这个框架里,开发人员能编写测试代码。框架要使用熟悉的工具,无需花很多精力就可以掌握。它还要消除不必要的代码,除了必须的测试代码外,消除重复劳动。
如果仅仅这些是测试要作的,那么你在调试器中写一个表达式就可以实现。但是,测试不仅仅这些。
虽然你的程序工作很好,但这不够,因为你不能保证集成后的即使一分钟内你的程序是否还会正常,你更不能保证5年内它还是否正常,那时你已经离开很久了。
因此,测试的第二个目标就是创建测试,并能保留这些测试,将来它们也是有价值的,其它的人可以执行这些测试,并验证测试结果。有可能的话,还要把不同人的测试收集在一起,一起执行,且不用担心它们之间互相干扰。
最后,还要能用已有的测试创建新的测试。每次创建新的测试设置或测试钳(test fixture)是很花费代价的,框架能复用测试设置,执行不同的测试。
3、JUnit设计
最早,JUnit的设计思路源于"用模式生成架构(Patterns Generate Architectures)"一文。它的思想就是,从0开始设计一个系统,一个一个地应用模式,直到最后构造出这个系统的架构,这样就完成一个系统的设计。我们马上提出要解决的架构问题,用模式来解决这个问题,并说明如何在JUnit中应用这些模式的。
3.1、从测试用例TestCase开始
首先我们创建一个对象来表示基础概念:测试用例(TestCase)。测试用例常常就存在于开发人员的头脑中,他们用不同的方式实现测试用例:
· 打印语句
· 调试表达式
· 测试脚本
如何我们想很容易地操纵测试,那么就必须把测试作为对象。开发人员脑海中的测试是模糊的,测试作为对象,就使得测试更具体了,测试就可以长久保留以便将来有用,这是测试框架的目标之一。同时,对象开发人员习惯于对象,因此把测试作为对象就能达到让编写测试代码更具吸引力的目的。
在这里,命令模式(command)满足我们的需要。该模式把请求封装成对象,即为请求操作生成一个对象,这个对象中有一个“执行(execute)”方法。命令模式中,请求者不是直接调用命令执行者,而是通过一个命令对象去调用执行者,具体说,先为命令请求生成一个命令对象,然后动态地在这个命令对象中设置命令执行者,最后用命令对象的execute方法调用命令执行者。这是TestCase类定义代码:〔此处译者有添加〕
public abstract class TestCase implements Test {
...
}
因为我们希望通过继承复用这个类,我门把它定义成“public abstract”。现在我们先不管它实现Test接口,在此时的设计里,你只要把TestCase看成是一个单个的类就行了。
每个TestCase有一个名字属性,当测试出现故障时,可以用它来识别是哪个测试用例。
public abstract class TestCase implements Test {
private final String fName;
public TestCase(String name) {
fName= name;
}
public abstract void run();
…
}
为了说明JUnit的演化进程,我们用图来表示各个设计阶段的架构。我们用简单的符号,灰色路标符号表明所使用的模式。当这个类在模式中的角色很明显时,就在路标中只指明模式名称;如果这个类在模式中的角色不清晰,则在路标中还注明该类对应的参与模式。这个路标符号避免了混乱,见图1所示。
图1 TestCase类应用了命令模式
3.2、在run()方法中填写方法体
下面要解决的问题就是给出一个方便的地方,让开发人员放置测试用的设置代码和测试代码。
TestCase定义为抽象的,表示开发人员要继承TestCase来创建自己的测试用例。如果我们象刚才那样,只在TestCase中放置一个变量,没有任何方法,那么第一个目标,即易于编写测试代这个目标就难以达到。
对于所有的测试,有一个通用的结构,在这个结构中,可以设置测试钳夹(fixture),在测试钳夹下运行一些代码,检查运行结果,然后清除测试钳夹。这表明,每个测试都运行在不同的钳夹下,一个测试的结果不会影响其它的测试结果,这点符合测试框架的价值最大化的目标。
模板方法(template method)模式很好地解决了上面提出的问题。模板方法模式的意图就是,在父类中定义一个算法的操作的骨架,将具体的步骤推迟到子类中实现。模板方法在子类中重新定义一个算法的特定步骤,不用改变这个算法的结构,这正好是我们的要求。我们只要求开发人员知道如何编写fixture(即setup和teardown)代码,知道如何编写测试代码。fixtue代码和测试代码的执行顺序对所有的测试都是一样的,不管fixture代码和测试代码是如何编写的。
这就是我们需要的模板方法:
public void run() {
setUp();
runTest();
tearDown();
}
这个模板方法的默认实现就是什么也不作。
protected void runTest() {
}
protected void setUp() {
}
protected void tearDown() {
}
既然setUp和tearDown方法要能被覆写,同时还要能被框架调用,因此定义成保护的。这个阶段的设计
如图2所示。
图2 TestCase.run()方法应用了模板方法模式
3.3、用TestResult对象报告结果
如果一个TestCase在原始森林中运行,大概没人关心它的测试结果。你运行测试是要得到一个测试记录,说明测试作了什么,什么没有作。
如果一个测试成功和失败的机会是相同的,或者我们只运行一个测试,那么我们只用在测试中设置一个标志,当测试结束后检查这个标志即可。然而,测试成功和失败机会是不均衡的,测试通常是成功的,因此我们只注重于测试故障的记录,对于成功的记录我们只做一个总概。
在SmallTalk Best Practice Patterns中,有一个叫“收集参数(collecting parameter)”的模式,当你需要在多个方法中收集结果时,你可以传给方法一个参数或对象,用这个对象收集这些方法的执行结果。我们创建一个新对象,测试结果(TestResult),去收集测试的结果。
public class TestResult extends Object {
protected int fRunTests;
public TestResult() {
fRunTests= 0;
}
}
这里一个简单的TestResult版本,它只是计数测试运行的数量。为了使用TestResult,我们必须把它作为参数传给TestCase.run()方法,并通知TestResult当前测试已经开始。
public void run(TestResult result) {
result.startTest(this); //通知TestResult测试开始
setUp();
runTest();
tearDown();
}
TestResult会跟踪计数运行了多少个测试:
public synchronized void startTest(Tes
此博客为记录今天在线上发现分布式缓存服务的一个BUG的过程与相关解决。
问题:
收到线上服务报警,有更新失败的操作,与是上线环境查看相应日志(服务由于并发的原因,偶尔性失败,没有影响)
但是发现服务所占资源比平时高一些,进行了一些排查!
确认服务线程:
top –c 指令查看如下:
进程13603服务,使用CPU与MEM都比较大,保存其相应的堆栈信息快照:
jstack 13603 > test.log
由于进程比较多,需要查看当前服务所占资源比较高的PID。
top –Hs指定如下显示:
发现13729线程所占资源一直在90%以上,奇怪了。有什么线程会一直占用如此高的CPU。
通过本机的计算器快速查出13729对应的十六进制为:35a1
然后less test.log日志,查看35a1线程的堆栈信息,发现正在运行的a.run线程(a为问题线程的代号)。
此时大概发现问题在哪个线程上了,a线程是我们分布式redis服务的一个维护线程,正常来说,此线程应该一直是在wait状态,怎么会在运行状态,且一直运行。
仔细查看此线程相关逻辑,逻辑本身没有问题,但是如果会导致死循环的话,只有整个服务reload时并且此服务相关配制没有修改。
此时查看线上相关BUG日志,确定在前几天有buffer reload的相关操作,自己也记起来有这么一次事件。
结合以上环境,稍微整理下思路,发现这是reload服务,此维护进程没有reInited引起的BUG,由于reload此操作比较少,且出现影响后,由于维护线程也是单线程服务,所以BUG运行了很长时间了。
之所以会出现这样的BUG,也是不应该。需要好好检查!
以上为一次线上BUG的查证与相应的解决的流程,记录下,希望对别人也有点帮忙,如何去发现问题,并验证问题。
一、简介
JUnit是一个开源的java单元测试框架。在1997年,由 Erich Gamma 和 Kent Beck 开发完成。这两个牛人中 Erich Gamma 是 GOF 之一;Kent Beck 则在 XP 中有重要的贡献(你觉得眼熟一点都不奇怪)。
正如常言道:“麻雀虽小,五脏俱全。” JUnit设计的非常小巧,但是功能却非常强大。
下面是JUnit一些特性的总结:
1) 提供的API可以让你写出测试结果明确的可重用单元测试用例
2) 提供了三种方式来显示你的测试结果,而且还可以扩展
3) 提供了单元测试用例成批运行的功能
4) 超轻量级而且使用简单,没有商业性的欺骗和无用的向导
5) 整个框架设计良好,易扩展
对不同性质的被测对象,如Class,Jsp,Servlet,Ejb等,Junit有不同的使用技巧。由于本文的性质,以下仅以Class测试为例。
下面我们就叩开JUnit的大门吧!
二、下载
点击http://www.junit.org可以下载到最新版本的JUnit,本文使用的为3.8.1版。至于安装或者配置之类,你只需要轻松的将下载下来的压缩包中的jar文件,放到你工程的classpath中就可以了。
这样,你的系统中就可以使用JUnit编写单元测试代码了(是不是很简单)!
三、HelloWorld
记得在几乎每本语言教学书上都能找到HelloWorld这个入门代码。今天在这里,我们也从一个简单到根本不用单元测试的例子入手。这是一个只会做两数加减的超级简单的计算器(小学一年级必备极品)。代码如下:
public class SampleCalculator
{
public int add(int augend , int addend)
{
return augend + addend ;
}
public int subtration(int minuend , int subtrahend)
{
return minuend - subtrahend ;
}
}
将上面的代码编译通过。下面就是我为上面程序写的一个单元测试用例:
//请注意这个程序里面类名和方法名的特征
public class TestSample extends TestCase
{
public void testAdd()
{
SampleCalculator calculator = new SampleCalculator();
int result = calculator.add(50 , 20);
assertEquals(70 , result);
}
public void testSubtration()
{
SampleCalculator calculator = new SampleCalculator();
int result = calculator.subtration(50 , 20);
assertEquals(30 , result);
}
}
好了,在DOS命令行里面输入javac -classpath .;junit.jar TestSample.java 将测试类编译通过。然后再输入java -classpath .;junit.jar junit.swingui.TestRunner TestSample 运行测试类,你会看到如下的窗口。
上图中,绿色说明单元测试通过,没有错误产生;如果是红色的,则就是说测试失败了。这样一个简单的单元测试就完成了,是不是很容易啊?
按照框架规定:编写的所有测试类,必须继承自junit.framework.TestCase类;里面的测试方法,命名应该以Test开头,必须是public void 而且不能有参数;而且为了测试查错方便,尽量一个TestXXX方法对一个功能单一的方法进行测试;使用assertEquals等junit.framework.TestCase中的断言方法来判断测试结果正确与否。
你可以对比着上面测试类中的实现来体会下规定——很简单!而且你在这个测试类中有加入多少个测试方法,就会运行多少个测试方法。
四、向前一步
学完了HelloWorld,你已经可以编写标准的单元测试用例了。但是还有一些细节,这里还要说明一下。不要急,很快的!
你在看上面的代码的时候,是不是注意到每个TestXXX方法中都有一条SampleCalculator初始化语句?这很明显不符合编码规范。你可能正要将它提取出来放到构造函数里面去。且慢!在JUnit中的初始化是建议在Setup方法中作的。JUnit提供了一对方法,一个在运行测试方法前初始化一些必备条件而另一个就是测试完毕后去掉初始化的条件(见下图)。
另外你是否注意到,上面弹出窗口的一个细节,在绿条下面有Errors、Failures统计。这两者有何区别呢?
Failures作为单元测试所期望发生的错误,它预示你的代码有bug,不过也可能是你的单元测试代码有逻辑错误(注意是逻辑错误)。Errors不是你所期待的,发生了Error你可以按照下面的顺序来检查:
检查测试所需的环境,如:数据库连接
检查单元测试代码
检查你的系统代码
五、成批运行test case