昨天我们研究到junit的MethodRule对象。虽然我不知道这个东东究竟是否这个版本新加的东西(因为自从4.1版本以后我好久没看过它的源代码了),不过既然我还不懂得它,就有研究的必要了 :D

首先来看看MethodRule的翻译:
一个MethodRule就是对测试的运行及报告方式的一种替代方案。一个测试方法可以同时实施多个不同的MethodRule。执行测试方法的Statement对象会依次传递给各个Rule注解,并且每一个规则都会返回一个替换了的或者修改过的Statement对象,并且如果存在下一个规则的话,会把这个Statement抛给它。

并且我发现junit默认提供了下列的MethodRule,当然我们也可以写自己的:
[list]
[*][b]ErrorCollector[/b] : 收集一个测试方法里面的多个错误
[*][b]ExpectedException[/b] : 对抛出的异常提供灵活的判断
[*][b]ExternalResource[/b] : 对外部资源的操控,例如启动或者停止一个服务器
[*][b]TemporaryFolder[/b] : 进行测试期间相关的测试操作。例如创建测试所需的临时文件,并且在测试完结之后删除它们。
[*][b]TestName[/b] : 在测试方法中记住测试的名称
[*][b]TestWatchman[/b] : 在方法执行的事件中添加额外的逻辑
[*][b]Timeout[/b] : 当测试的执行超过特定时间后导致测试失败
[*][b]Verifier[/b] : 假如对象状态不正确时让测试失败
[/list]

就我经验来说,它尝试把以前的expect,timeout等标记进行了整合,并且添加了一些其它的扩展。这套规则机制除了把以前凌乱的测试扩展的控制机制统一起来之外,还允许你对测试的策略进行自定义。可见这个版本的junit更加简单并且灵活。

MethodRule里面就只有一个方法:

Statement apply(Statement base, FrameworkMethod method, Object target);


其中,base就是上面提到的执行测试方法的Statement对象,method为要执行的测试方法,target则是测试类的实例。

返回结果,根据javadoc的描述,当然就是一个Statement对象了。。。

现在我们进一步看看各个默认Rule的详细情况,它们都在org.junit.rules里面:
[img]http://dl.iteye.com/upload/attachment/182501/50591715-67ee-31c8-a19e-0c66ce7ca8d5.gif[/img]

[b]ErrorCollector[/b]
作用:允许你把测试过程的所有异常都交给它,然后在最后让它一次性对所有异常进行汇报。
示例:
        collector.addError(new Throwable("second thing went wrong"));
collector.checkThat(getResult(), not(containsString("ERROR!")));
// all lines will run, and then a combined failure logged at the end.
}
}
[/code]

在上面的例子里面,我们并不是像传统的做法那样直接把异常往外抛直接导致一个异常的Statement中断测试,而上放到这个ErrorCollector里面,最后通过Assert.checkThat方法一次性对结果进行判断是否通过。这种做法无疑是一个非常节省时间的进步,不再需要像以前那样无法一次性尝试走完测试才结束用例。

[b]ExpectedException[/b]
作用:在测试方法里面动态控制期待抛出的异常类型甚至其内容
示例:
[code="java">
// These tests all pass.
public static class HasExpectedException {
@Rule
public ExpectedException thrown= new ExpectedException();

@Test
public void throwsNothing() {
// no exception expected, none thrown: passes.
}

@Test
public void throwsNullPointerException() {
thrown.expect(NullPointerException.class);
throw new NullPointerException();
}

@Test
public void throwsNullPointerExceptionWithMessage() {
thrown.expect(NullPointerException.class);
thrown.expectMessage("happened?");
thrown.expectMessage(startsWith("What"));
throw new NullPointerException("What happened?");
}
}


看到最后一个例子的时候,只能惊讶这个东西真是神奇。不过出于使用习惯,个人觉得还是@expect标记比较实用。不过无可非议,当我们对那些只会抛出同一个异常,但是message会根据出错的情况而发生变化的场合使用这个规则是非常好的选择。

[b]ExternalResource[/b]
作用:在测试之前对外部资源进行初始化及在测试结束之后对这些资源进行清理的规则。
示例:

public static class UsesExternalResource {
Server myServer= new Server();

@Rule
public ExternalResource resource= new ExternalResource() {
@Override
protected void before() throws Throwable {
myServer.connect();
};

@Override
protected void after() {
myServer.disconnect();
};
};

@Test
public void testFoo() {
new Client().run(myServer);
}
}


个人感觉这个东西比较无聊。它要么是封装了@Before和@After,要么是封装了@BeforeClass和@AfterClass。虽然就字面理解它应该是实现了前者,但是为了让大家可以更好地理解我决定做个实验看看:

package com.amway.training.junit;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExternalResource;

public class ExternalResourceTest{
@Before
public void before()
{
System.out.println("Running in before()");
}

@Before
public void before2()
{
System.out.println("Running in before2()");
}

@BeforeClass
public static void beforeClass()
{
System.out.println("Running in beforeClass()");
}

@Rule
public ExternalResource resource = new ExternalResource() {

@Override
protected void after() {
System.out.println("Running in resource.after()");
}

@Override
protected void before() throws Throwable {
System.out.println("Running in resource.before()");
}

};

@Rule
public ExternalResource resource2 = new ExternalResource() {

@Override
protected void after() {
System.out.println("Running in resource2.after()");
}

@Override
protected void before() throws Throwable {
System.out.println("Running in resource2.before()");
}

};

@After
public void After()
{
System.out.println("Running in after()");
}

@AfterClass
public static void AfterClass()
{
System.out.println("Running in afterClass()");
}

@Test
public void aTest()
{
System.out.println("I'm a simple test");
}
}


结果如我所想的一样无聊。。。

Running in beforeClass()
Running in before()
Running in before2()
Running in resource2.before()
Running in resource.before()
I'm a simple test
Running in resource.after()
Running in resource2.after()
Running in after()
Running in afterClass()


可见,它只是@Before和@After的一套替代品。不过从让@Before的执行方法和@After的执行方法一一对应这点来说,使用这个Rule来进行外部资源管理的确是一个最佳实践。因为如果我们依靠@Before和@After来实现外部资源的初始化及清理,如果只有一对组合还好,一多了就会很难搞清谁先谁后。。。。

[b]TemporaryFolder[/b]
作用:帮我们在测试过程中创建特定的临时文件夹或文件用于测试,并且在测试结束后,无论是否成功,都会删除这些由它创建的东西。
示例:

public static class HasTempFolder {
@Rule
public TemporaryFolder folder= new TemporaryFolder();

@Test
public void testUsingTempFolder() throws IOException {
File createdFile= folder.newFile("myfile.txt");
File createdFolder= folder.newFolder("subfolder");
// ...
}
}


注意,必须通过TemporaryFolder的方法创建出来的临时文件及文件夹,才会在测试结束的时候被删除。让我们来解读一下它的代码就知道是怎么回事了:

public class TemporaryFolder extends ExternalResource {
private File folder;

@Override
protected void before() throws Throwable {
create();
}

@Override
protected void after() {
delete();
}

// testing purposes only
/**
* for testing purposes only. Do not use.
*/
public void create() throws IOException {
folder= File.createTempFile("junit", "");
folder.delete();
folder.mkdir();
}

/**
* Returns a new fresh file with the given name under the temporary folder.
*/
public File newFile(String fileName) throws IOException {
File file= new File(folder, fileName);
file.createNewFile();
return file;
}

/**
* Returns a new fresh folder with the given name under the temporary folder.
*/
public File newFolder(String folderName) {
File file= new File(folder, folderName);
file.mkdir();
return file;
}

/**
* @return the location of this temporary folder.
*/
public File getRoot() {
return folder;
}

/**
* Delete all files and folders under the temporary folder.
* Usually not called directly, since it is automatically applied
* by the {@link Rule}
*/
public void delete() {
recursiveDelete(folder);
}

private void recursiveDelete(File file) {
File[] files= file.listFiles();
if (files != null)
for (File each : files)
recursiveDelete(each);
file.delete();
}
}


可以看到,其实它是ExternalResource的一个子类,并且在里面有一个私有字段folder。这个字段会在before的时候通过create()方法在当前的测试类目录下被创建为junit的文件夹。并且在后续用newFolder和newFile创建目录的时候都会在此目录下进行创建。在after()推出测试方法时则会把这个folder及其子文件和文件夹通通删除。

所以,这个规则在多线程测试模式下要慎用!!一旦你的同一个目录下有两个测试在同时执行的时候,就可能会出现资源竞争的情况导致删除出问题,甚至还会有其它死锁之类的漏洞!

总体来说,当我们使用这个规则的时候看来是要经过自己重构才能够使用,把folder的名字用个不会重复的名称吧,是最简单的方法了。

[b]TestName[/b]
作用:让测试方法可以调用到当前测试的测试名变量
示例:

public class TestNameTest {
@Rule
public TestName name= new TestName();

@Test
public void testA() {
assertEquals("testA", name.getMethodName());
}

@Test
public void testB() {
assertEquals("testB", name.getMethodName());
}
}


很简单的一个例子。这个东西主要对于统一处理进程的日志显示会有帮助。后面的例子我们就会用到它。

[b]TestWatchman[/b]
作用:一个测试过程的观察者。只会记录测试过程的关键事件,但是不会对测试造成任何影响。
示例:

public static class WatchmanTest {
private static String watchedLog;

@Rule
public MethodRule watchman= new TestWatchman() {
@Override
public void failed(Throwable e, FrameworkMethod method) {
watchedLog+= method.getName() + " " + e.getClass().getSimpleName()
+ "\n";
}

@Override
public void succeeded(FrameworkMethod method) {
watchedLog+= method.getName() + " " + "success!\n";
}
};

@Test
public void fails() {
fail();
}

@Test
public void succeeds() {
}
}


查看它的apply方法源码如下:

public Statement apply(final Statement base, final FrameworkMethod method,Object target) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
starting(method);
try {
base.evaluate();
succeeded(method);
} catch (Throwable t) {
failed(t, method);
throw t;
} finally {
finished(method);
}
}
};
}


可见我们可以监控4种状态:
[list]
[*]starting
[*]succeeded
[*]failed
[*]finished
[/list]

[b]Timeout[/b]
作用:对所有测试方法定义一个统一的timeout时间
示例:

public static class HasGlobalTimeout {
public static String log;

@Rule
public MethodRule globalTimeout= new Timeout(20);

@Test
public void testInfiniteLoop1() {
log+= "ran1";
for (;;) {
}
}

@Test
public void testInfiniteLoop2() {
log+= "ran2";
for (;;) {
}
}
}


以前要在@Test里面逐个定义timeout属性,现在可以统一在这个规则里面定义。老问题又来了。如果我又在@Test里面定义timeout,又在Timeout里面定义超时时间,哪个会生效呢?我们可以先来看看以下实例实例:

package com.amway.training.junit;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExternalResource;
import org.junit.rules.MethodRule;
import org.junit.rules.TestName;
import org.junit.rules.Timeout;

public class TimeoutTest {
@Rule
public MethodRule timeout = new Timeout(1000);

@Rule
public TestName testName = new TestName();

@Rule
public MethodRule extres = new ExternalResource(){

@Override
protected void after() {
end = System.currentTimeMillis();
System.out.println("Run time for ["+testName.getMethodName()+"] is:"+ (end-start));
end = start = 0;
}

@Override
protected void before() throws Throwable {
start = System.currentTimeMillis();
end=0;
}

};

private long start,end;

@Test
public void testWithoutSpecTimeout()
{
for(;;){}
}


@Test(timeout=100)
public void testWithSpecTimeout()
{
for(;;){}
}
}


结果为:
Run time for [testWithoutSpecTimeout] is:1000
Run time for [testWithSpecTimeout] is:109

结果就是,具体的@Test的timeout可以改写这个统一的timeout。来看看其apply方法的源码:

public Statement apply(Statement base, FrameworkMethod method, Object target) {
return new FailOnTimeout(base, fMillis);
}


然后再来看看FailOnTimeout这个类的evaluate方法:

@Override
public void evaluate() throws Throwable {
Thread thread= new Thread() {
@Override
public void run() {
try {
fNext.evaluate();
fFinished= true;
} catch (Throwable e) {
fThrown= e;
}
}
};
thread.start();
thread.join(fTimeout);
if (fFinished)
return;
if (fThrown != null)
throw fThrown;
Exception exception= new Exception(String.format(
"test timed out after %d milliseconds", fTimeout));
exception.setStackTrace(thread.getStackTrace());
throw exception;
}


正如大家所见,这个东西会执行fNext.evaluate()方法,而对于Timeout规则来说,这个fNext是由之前的规则传递过来的,回忆一下我们在核心分析时讨论过的methodBlock方法,它最先封装的就是@Test标记的方法。所以当两者同时定义了不同的timeout时间时,它会以@Test定义的优先。

[b]Verifier[/b]
作用:验证失败时可以控制测试直接失败。
示例:

package com.amway.training.junit;

import junit.framework.Assert;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.MethodRule;
import org.junit.rules.Verifier;

public class ErrorLogVerifier {
private StringBuffer errorLog = new StringBuffer();

@Rule
public MethodRule verifier = new Verifier() {
@Override
public void verify() {
Assert.assertTrue(errorLog.length()==0);
}
};

@Test
public void testThatWriteErrorLog() {
errorLog.append("some error!!");
}

@Test
public void testThatWontWriteErrorLog() {

}
}


结果:
[img]http://dl.iteye.com/upload/attachment/182530/c28afa3d-679c-3ee5-8a4e-f01c21fde48e.gif[/img]

其实。。。除非你有必要对某种异常进行统一的验证处理,否则没必要使用这个Rule。

我们还能够自己去实现自己的MethodRule。由于时间关系我就不详细说了,大家可以多参考默认的规则然后自己练练手。总体来说这个东西还是蛮有用的。

下一次我们来看看Statement的种类吧。
Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐