本文共 10712 字,大约阅读时间需要 35 分钟。
前面一章介绍了JUnit的一些基本用法,本章来介绍关于JUnit更高级的用法,这些功能我们可能并不一定会用到,但是了解它,对JUnit会有更深刻的认识。
大家刚开始使用JUnit的时候,可能会跟我一样有一个疑问,JUnit没有main()方法,那它是怎么开始执行的呢?众所周知,不管是什么程序,都必须有一个程序执行入口,而这个入口通常是main()方法。显然,JUnit能直接执行某个测试方法,那么它肯定会有一个程序执行入口。没错,其实在org.junit.runner包下,有个JUnitCore.java类,这个类有一个标准的main()方法,这个其实就是JUnit程序的执行入口,其代码如下:
public static void main(String... args) { Result result = new JUnitCore().runMain(new RealSystem(), args); System.exit(result.wasSuccessful() ? 0 : 1);}
通过分析里面的runMain()方法,可以找到最终的执行代码如下:
public Result run(Runner runner) { Result result = new Result(); RunListener listener = result.createListener(); notifier.addFirstListener(listener); try { notifier.fireTestRunStarted(runner.getDescription()); runner.run(notifier); notifier.fireTestRunFinished(result); } finally { removeListener(listener); } return result; }
可以看到,所有的单元测试方法都是通过Runner来执行的。Runner只是一个抽象类,它是用来跑测试用例并通知结果的,JUnit提供了很多Runner的实现类,可以根据不同的情况选择不同的test runner。
通过@RunWith注解,可以为我们的测试用例选定一个特定的Runner来执行。
Suite翻译过来是测试套件,意思是让我们将一批其他的测试类聚集在一起,然后一起执行,这样就达到了同时运行多个测试类的目的。
如上图所示,假设我们有3个测试类:TestLogin, TestLogout, TestUpdate,使用Suite编写一个TestSuite类,我们可以将这3个测试类组合起来一起执行。TestSuite类代码如下:
@RunWith(Suite.class)@Suite.SuiteClasses({ TestLogin.class, TestLogout.class, TestUpdate.class})public class TestSuite { //不需要有任何实现方法}
执行运行TestSuite,相当于同时执行了这3个测试类。
Suite还可以进行嵌套,即一个测试Suite里包含另外一个测试Suite。@RunWith(Suite.class)@Suite.SuiteClasses(TestSuite.class)public class TestSuite2 {}
我们常规的测试方法都是public void修饰的,不能带有任何输入参数。但是有时我们需要在测试方法里输入参数,甚至可能需要指定批量的参数,如果使用常规的模式,那就需要为每一种参数写一个测试方法,这显然不是我们所期望的。使用Parameterized这个test runner就能实现这个目的。
我们有一个待测试类,菲波那切函数,代码如下:
public class Fibonacci { public static int compute(int n) { int result = 0; if(n <= 1) { result = n; } else { result = compute(n -1) + compute(n - 2); } return result; }}
针对这个函数,我们需要多个输入参数来验证是否正确,来看看怎么实现这个目的。
//指定Parameterized作为test runner@RunWith(Parameterized.class)public class TestParams { //这里是配置参数的数据源,该方法必须是public static修饰的,且必须返回一个可迭代的数组或者集合 //JUnit会自动迭代该数据源,自动为参数赋值,所需参数以及参数赋值顺序由构造器决定。 @Parameterized.Parameters public static List getParams() { return Arrays.asList(new Integer[][]{ { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 } }); } private int input; private int expected; //在构造函数里,指定了2个输入参数,JUnit会在迭代数据源的时候,自动传入这2个参数。 //例如:当获取到数据源的第3条数据{2,1}时,input=2,expected=1 public TestParams(int input, int expected) { this.input = input; this.expected = expected; } @Test public void testFibonacci() { System.out.println(input + "," + expected); Assert.assertEquals(expected, Fibonacci.compute(input)); }}
执行该测试类,可以看到执行过程中的打印结果:
0,01,12,13,24,35,56,8
@RunWith(Parameterized.class)public class TestParams2 { @Parameterized.Parameters public static List getParams() { return Arrays.asList(new Integer[][]{ { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 } }); } //这里必须是public,不能是private @Parameterized.Parameter public int input; //注解括号里的参数,用来指定参数的顺序,默认为0 @Parameterized.Parameter(1) public int expected; @Test public void testFibonacci() { System.out.println(input + "," + expected); Assert.assertEquals(expected, Fibonacci.compute(input)); }}
Categories继承自Suite,但是比Suite功能更加强大,它能对测试类中的测试方法进行分类执行。当你想把不同测试类中的测试方法分在一组,Categories就很管用。
public interface CategoryMarker { public interface FastTests { /* category marker */ } public interface SlowTests { /* category marker */ }}
public class A { @Test public void a() { System.out.println("method a() called in class A"); } //标记该测试方法的类别 @Category(CategoryMarker.SlowTests.class) @Test public void b() { System.out.println("method b() called in class A"); }}
@Category({CategoryMarker.FastTests.class, CategoryMarker.SlowTests.class})public class B { @Test public void c() { System.out.println("method c() called in class B"); }}
@RunWith(Categories.class)@Categories.IncludeCategory(CategoryMarker.SlowTests.class)@Suite.SuiteClasses({A.class, B.class}) //Categories本身继承自Suitepublic class SlowTestSuite { //如果不加@Categories.IncludeCategory注解,效果与Suite一样}//执行结果,打印信息如下:method b() called in class Amethod c() called in class B
@RunWith(Categories.class)@Categories.IncludeCategory({CategoryMarker.SlowTests.class}) //指定包含的类别@Categories.ExcludeCategory({CategoryMarker.FastTests.class}) //需要排除的类别@Suite.SuiteClasses({A.class, B.class})public class SlowTestSuite2 {}//执行结果,打印信息如下:method b() called in class A
timeout用来测试一个方法能不能在规定时间内完成,当为一个测试方法指定了timeout属性后,该方法会运行在一个单独的线程里执行。如果测试方法运行时间超过了指定的timeout时间,测试则会失败,并且JUnit会中断执行该测试方法的线程。
//该方法会在一个单独的线程中执行,单位为毫秒,这里超时时间为2秒 @Test(timeout = 2000) public void testTimeout() { System.out.println("timeout method called in thread " + Thread.currentThread().getName()); } //该方法默认会在主线程中执行 @Test public void testNormalMethod() { System.out.println("normal method called in thread " + Thread.currentThread().getName()); } //该方法指定了timeout时间为1秒,实际运行时会超过1秒,该方法测试无法通过 @Test(timeout = 1000) public void testTimeout2() { try { Thread.sleep(1200); } catch (InterruptedException e) { e.printStackTrace(); } }
执行后打印结果如下:
timeout method called in thread Time-limited testnormal method called in thread main
expected属性是用来测试异常的。例如:
new ArrayList
这段代码应该抛出一个IndexOutOfBoundsException异常信息,如果我们想验证这段代码是否抛出了异常,我们可以这样写:
@Test(expected = IndexOutOfBoundsException.class) public void empty() { new ArrayList
@Rule是JUnit4的新特性,它能够灵活地扩展每个测试方法的行为,为他们提供一些额外的功能。下面是JUnit提供的一些基础的的test rule,所有的rule都实现了TestRule这个接口类。除此外,可以自定义test rule。
在测试方法内部能知道当前的方法名。
public class NameRuleTest { //用@Rule注解来标记一个TestRule,注意必须是public修饰的 @Rule public final TestName name = new TestName(); @Test public void testA() { assertEquals("testA", name.getMethodName()); } @Test public void testB() { assertEquals("testB", name.getMethodName()); }}
与@Test注解里的属性timeout类似,但这里是针对同一测试类里的所有测试方法都使用同样的超时时间。
public class TimeoutRuleTest { @Rule public final Timeout globalTimeout = Timeout.millis(20); @Test public void testInfiniteLoop1() { for(;;) {} } @Test public void testInfiniteLoop2() { for(;;) {} }}
与@Test的属性expected作用类似,用来测试异常,但是它更灵活方便。
public class ExpectedExceptionTest { @Rule public final ExpectedException exception = ExpectedException.none(); //不抛出任何异常 @Test public void throwsNothing() { } //抛出指定的异常 @Test public void throwsIndexOutOfBoundsException() { exception.expect(IndexOutOfBoundsException.class); new ArrayList().get(0); } @Test public void throwsNullPointerException() { exception.expect(NullPointerException.class); exception.expectMessage(startsWith("null pointer")); throw new NullPointerException("null pointer......oh my god."); }}
该rule能够创建文件以及文件夹,并且在测试方法结束的时候自动删除掉创建的文件,无论测试通过或者失败。
public class TemporaryFolderTest { @Rule public final TemporaryFolder folder = new TemporaryFolder(); private static File file; @Before public void setUp() throws IOException { file = folder.newFile("test.txt"); } @Test public void testFileCreation() throws IOException { System.out.println("testFileCreation file exists : " + file.exists()); } @After public void tearDown() { System.out.println("tearDown file exists : " + file.exists()); } @AfterClass public static void finish() { System.out.println("finish file exists : " + file.exists()); }}
测试执行后打印结果如下:
testFileCreation file exists : truetearDown file exists : truefinish file exists : false //说明最后文件被删除掉了
实现了类似@Before、@After注解提供的功能,能在方法执行前与结束后做一些额外的操作。
public class UserExternalTest { @Rule public final ExternalResource externalResource = new ExternalResource() { @Override protected void after() { super.after(); System.out.println("---after---"); } @Override protected void before() throws Throwable { super.before(); System.out.println("---before---"); } }; @Test public void testMethod() throws IOException { System.out.println("---test method---"); }}
执行后打印结果如下:
---before------test method------after---
自定义rule必须实现TestRule接口。
下面我们来写一个能够让测试方法重复执行的rule:public class RepeatRule implements TestRule { //这里定义一个注解,用于动态在测试方法里指定重复次数 @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface Repeat { int count(); } @Override public Statement apply(final Statement base, final Description description) { Statement repeatStatement = new Statement() { @Override public void evaluate() throws Throwable { Repeat repeat = description.getAnnotation(Repeat.class); //如果有@Repeat注解,则会重复执行指定次数 if(repeat != null) { for(int i=0; i < repeat.count(); i++) { base.evaluate(); } } else { //如果没有注解,则不会重复执行 base.evaluate(); } } }; return repeatStatement; }}
public class RepeatTest { @Rule public final RepeatRule repeatRule = new RepeatRule(); //该方法重复执行5次 @RepeatRule.Repeat(count = 5) @Test public void testMethod() throws IOException { System.out.println("---test method---"); } @Test public void testMethod2() throws IOException { System.out.println("---test method2---"); }}
执行结果如下:
---test method2------test method------test method------test method------test method------test method---
本文主要介绍了JUnit自身提供的几个主要的test runner,还有@Test的timeout跟expected属性,最后介绍了一些常规的Rules使用方法。test runner是一个很重要的概念,因为在Android中进行单元测试时,通常都是JUnit结合其他测试框架来一起完成,例如mockito、robolectric,它们都有自己实现的一套test runner,我们必须使用这些框架提供的test runner才能发挥这些框架的最大作用。
系列文章:
转载地址:http://iiill.baihongyu.com/