单元测试是在编程和重构中被极力推荐使用的,可以大大的提高开发的效率(这里的效率需要综合计算后续可维护性,已经出现BUG重复修改等下,而不简单单是开发时间)。但是实际上编写测试代码也是需要耗费很多的时间和精力的,有的时候甚至要比编写代码本身花费的时间还要多,所以如何写好单元测试用例是一门很深奥的学问。
好的测试用例应该具有以下四种特性:
正确性 程序中的每一项功能都是测试来验证它的正确性,是后续维护代码、重构代码的保证。
设计性 编写单元测试将使我们从调用者观察、思考。特别是先写测试(test-first),迫使我们把程序设计成易于调用和可测试的,即迫使我们解除软件中的耦合。
指导性 单元测试应该具有指导性,类似于文档,是对函数或类使用的最佳实践。而且,这份文档是可编译、可运行的,并且它保持最新,永远与代码同步。
回归性 自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地的快速运行测试。
后续我将用一系列的博客,来记录如何对于Java代码来做单元测试,本文先从最基础的JUnit做起,先来学习基本的工具的使用。
JUnit 功能点 JUnit支持对数组、对象的断言,可以通过assertArrayEquals
、assertEquals
、assertFalse
、assertNotNull
、assertNotSame
、assertNull
、assertSame
等方法进行比较, JUnit还支持更复杂的Matchers用法,是通过集成了hamcrest 的jar包来实现了如下功能,源码如下,可见,采用直接调用调用的方式:1
2
3
public static <T> void assertThat (String reason, T actual, Matcher<? super T> matcher) {
MatcherAssert.assertThat(reason, actual, matcher);
}
示例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.hamcrest.core.CombinableMatcher;
import org.junit.Test;
public class AssertionsTest {
@Test
public void testAssert () {
byte [] expected = "trial" .getBytes();
byte [] actual = "trial" .getBytes();
assertArrayEquals("failure - byte arrays not same" , expected, actual);
assertNotSame("should not be same Object" , new Object(), new Object());
}
@Test
public void testAssertThatBothContainsString () {
assertThat("albumen" , both(containsString("a" )).and(containsString("b" )));
assertThat(Arrays.asList("one" , "two" , "three" ), hasItems("one" , "three" ));
assertThat(Arrays.asList("fun" , "ban" , "net" ), everyItem(containsString("n" )));
assertThat("good" , allOf(equalTo("good" ), startsWith("good" )));
assertThat("good" , not(allOf(equalTo("bad" ), equalTo("good" ))));
assertThat("good" , anyOf(equalTo("bad" ), equalTo("good" )));
assertThat(7 , not(CombinableMatcher.<Integer>either(equalTo(3 )).or(equalTo(4 ))));
assertThat(new Object(), not(sameInstance(new Object())));
}
}
TIPS:
关于Matchers and assertThat ,这里提供了更细致的分析。
有些第三方的扩展Mather也很有意思,例如JSON Matcher 1
2
3
4
assertThat("{\"age\":43, \"friend_ids\":[16, 52, 23]}" ,
sameJSONAs("{\"friend_ids\":[52, 23, 16]}" )
.allowingExtraUnexpectedFields()
.allowingAnyArrayOrdering());
当一个测试类使用@RunWith注解时,JUnit会调用注解中的类执行测试用例。JUnit默认使用的Runner是JUnit4.class
,还提供了Suite.class
、Parameterized.class
、Categories.class
,我将在本文依次介绍。当然我们还有其他常用的来自于第三方提供的如SpringJUnit4ClassRunner.class
、MockitoJUnitRunner.class
、PowerMockRunner.class
,我们后续也会说到。
使用@RunWith(Suite.class)
可以将多个用例打包,放在一起执行。示例如下:1
2
3
4
5
6
7
8
9
10
11
12
13
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith (Suite.class)
@Suite .SuiteClasses({
TestFeatureLogin.class,
TestFeatureLogout.class,
TestFeatureNavigate.class,
TestFeatureUpdate.class
})
public class FeatureTestSuite {
}
使用@RunWith(Parameterized.class)
可以像函数一样,传入多个入参和出参的对应,并逐一校验是否正确。例如,下面的取和函数int sum(int a, int b)
,我们可能想测试1+2=3,2+3=5,3+6=9等多种,如果要一个一个写@Test,比较繁琐,这时候可以使用参数化来简化代码书写。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@RunWith(Parameterized.class)
public class ParameterizedTest {
@Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
{1, 2, 3}, {2, 3, 5}, {3, 6, 9}
});
}
private int a;
private int b;
private int result;
// 构造函数和@Parameters注解标注的集合的赋值顺序必须一致
public ParameterizedTest(int a, int b, int result) {
this.a = a;
this.b = b;
this.result = result;
}
@Test
public void test() {
assertEquals(result, sum(a, b));
}
private int sum(int a, int b) {
return a + b;
}
}
TIPS: 1,如果不想使用构造函数,还可以使用@Parameter
注解来替代构造函数,但需要被注解的参数是public的 1
2
3
4
@Parameter
public int fInput;
@Parameter (1 )
public int fExpected;
2,单个参数的时候,@Parameters
标注的方法可以稍作简化(版本要大于4.12-beta-3)。1
2
3
4
@Parameters
public static Iterable<? extends Object> data() {
return Arrays.asList("first test", "second test");
}
使用@RunWith(Categories.class)
可以分类,使用方法类似Hibernate Validator的Group,fastxml的MixInAnnotations.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public interface FastTests { }
public interface SlowTests { }
public class A {
@Test
public void a () {
fail();
}
@Category (SlowTests.class)
@Test
public void b () {}
}
@Category ({SlowTests.class, FastTests.class})
public class B {
@Test
public void c () {}
}
@RunWith (Categories.class)
@Categories .IncludeCategory(SlowTests.class)
@Suite .SuiteClasses({A.class, B.class})
public class SlowTestSuite {
}
@RunWith (Categories.class)
@Categories .IncludeCategory(SlowTests.class)
@Categories .ExcludeCategory(FastTests.class)
@Suite .SuiteClasses({A.class, B.class})
public class SlowTestSuite {
}
自动JUnit4.11,使用@FixMethodOrder
可以标改变用例的执行顺序,但是一般不会使用,因为如果用例有执行依赖,那么用例必定设定的不太合适。JUnit目前提供了以下几种默认实现:MethodSorters.DEFAULT
: 默认是按照用例函数名的hashcode比较MethodSorters.JVM
: 让JVM决定顺序MethodSorters.NAME_ASCENDING
: 按照用例函数名称升序
使用@RunWith(Categories.class)
可以分类,使用方法类似Hibernate Validator的Group,fastxml的MixInAnnotations.1
2
3
4
@Test (expected = IndexOutOfBoundsException.class)
public void empty () {
new ArrayList<Object>().get(0 );
}
或者使用try catch1
2
3
4
5
6
7
8
9
@Test
public void testExceptionMessage () {
try {
new ArrayList<Object>().get(0 );
fail("Expected an IndexOutOfBoundsException to be thrown" );
} catch (IndexOutOfBoundsException anIndexOutOfBoundsException) {
assertThat(anIndexOutOfBoundsException.getMessage(), is("Index: 0, Size: 0" ));
}
}
或者使用ExpectedException Rule
,Rule的作用会在下面说,暂时可以理解为,一种加在所有测试方法上的校验,下面的每一个用例都得满足这个条件。1
2
3
4
5
6
7
8
9
10
11
12
@Rule public ExpectedException thrown = ExpectedException.none();
@Test
public void shouldTestExceptionMessage () throws IndexOutOfBoundsException {
List<Object> list = new ArrayList<Object>();
thrown.expect(IndexOutOfBoundsException.class);
thrown.expectMessage("Index: 0, Size: 0" );
list.get(0 );
}
很简单,直接看代码喽1
2
@Test (timeout=1000 )
public void testWithTimeout () {}
使用Timeout Rule
,校验这个类的所有方法的运行时间都不能超过10秒钟,不然就会标志为失败。 这里需要注意的一点是Rule需要是public field1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class HasGlobalTimeout {
@Rule public Timeout globalTimeout = Timeout.seconds(10 );
@Test
public void testSleepForTooLong () throws Exception {
TimeUnit.SECONDS.sleep(100 );
}
@Test
public void testBlockForever () throws Exception {
CountDownLatch latch = new CountDownLatch(1 );
latch.await();
}
}
TIPS: 1,需要注意的是,Timeout Rule的时间是包括@Before和@After的时间的,所以,@After方法是存在不被执行的可能的。如果想在@After里面清空或者释放资源,要注意了。
一个JUnit Rule就是一个实现了TestRule的类,这些类的作用有点类似于@Before、@After(是用来在每个测试方法的执行前后执行一些代码的一个方法),但是Rule更倾向于校验。是指满足某种条件用例才会执行成功的一种校验。例如上面提到的ExpectedException Rule
,Timeout Rule
,分别致力于异常校验、超时校验,JUnit还提供了其他的Rule,如:
TemporaryFolder
:规则允许创建文件或文件夹,在测试方法(无论是通过还是失败)执行完成时会被删除。默认情况下,如果资源无法被删除是不会抛出异常的。参考
ExternalResource
:ExternalResource是规则(例如TemporaryFolder)的基础类,在测试之前建立一个外部资源(一个文件,套接字[socket],服务[server],数据库连接,等等),并且保证tear it down.
ErrorCollector
:ErrorCollector 规则允许在第一个问题发现之后继续运行(例如,收集下面不正确的行到一个表中,然后一次性报告所有错误。)
Verifier
:Verifier是规则(例如ErrorCollector)的一个基类,无论测试的执行情况是什么,只要验证框架不通过,则视为测试失败。
TestWatchman/TestWatcher
:TestWatchman 是JUnit 4.7引入的,但是已经过时,被 TestWatcher [4.9引入] 所取代。TestWatcher 是一个规则的基础类,that take note of the testing action,而不用修改测试类。例如,这个类将保存一个日志在一个通过以及失败的测试:
TestName
:TestName 规则使得当前测试的名字在测试内部是可利用的:
ClassRule
:The ClassRule annotation extends the idea of method-level Rules, adding static fields that can affect the operation of a whole class. Any subclass of ParentRunner, including the standard BlockJUnit4ClassRunner and Suite classes, will support ClassRules.
RuleChain
:规则链,多个规则的组合
Custom Rules
:自定义规则
使用@Ignore注解即可
假设有点类似于java里面的if,如果assumeThat
不成立,后面的代码将不会执行,且UT不会报错。 1
2
3
4
5
6
7
8
9
10
11
12
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assume.assumeThat;
public class AssumptionsTest {
@Test
public void assume () {
String filePath = "parent" + File.separator + "child.cfg" ;
assumeThat(File.separatorChar, is("/" ));
assertThat(filePath, is("parent/child.cfg" ));
}
}
Test fixtures,一般用于在测试前后: 1,准备输入数据、创建Mock对象 2,从数据库中取数据 3,做一些公用的操作
其执行流程一般为:1
2
3
4
5
6
7
8
9
10
11
@BeforeClass
@Before
@Test
@After
@Before
@Test
@After
@AfterClass
Theories是指更灵活且富有表现力的断言(assertions)和假设(assumptions)。@Test集中于一种特定的场景,而@Theory则可能覆盖无限的潜在场景。很多情况下,Theories会比Parameterized.class
更好用,因为写法更简单。
例如如下测试,每一个@DataPoint
标注的变量都会去执行@Theory
标注的测试,所以,该方法执行了两次。带有/
的名字是不测试无法通过assumeThat
,其测试结果被忽略1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import org.junit.experimental.theories.DataPoint;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsNot.not;
import static org.junit.Assume.assumeThat;
@RunWith (Theories.class)
public class TheoriesTest {
@DataPoint
public static String GOOD_USERNAME = "optimus" ;
@DataPoint
public static String USERNAME_WITH_SLASH = "optimus/prime" ;
@Theory
public void filenameIncludesUsername (String username) {
assumeThat(username, not(containsString("/" )));
assertThat(new User(username).configFileName(), containsString(username));
}
private class User {
String username;
String configFileName;
User(String username) {
this .username = username;
this .configFileName = "config/" + username;
}
String configFileName () {
return configFileName;
}
}
}
多参数测试1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.vther.junit;
import org.junit.Test;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.experimental.theories.suppliers.TestedOn;
import org.junit.runner.RunWith;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assume.assumeThat;
@RunWith (Theories.class)
public class DollarTest {
@Test
public void multiplyByAnInteger () {
assertThat(new Dollar(5 ).times(2 ).getAmount(), is(10 ));
}
@Theory
public void multiplyIsInverseOfDivideWithInlineDataPoints (
@TestedOn(ints = {0 , 5 , 10 }) int amount,
@TestedOn (ints = {0 , 1 , 2 }) int m) {
assumeThat(m, not(0 ));
assertThat(new Dollar(amount).times(m).divideBy(m).getAmount(), is(amount));
}
class Dollar {
private int amount;
Dollar(int amount) {
this .amount = amount;
}
Dollar times (int i) {
amount = amount * i;
return this ;
}
Dollar divideBy (int m) {
this .amount = amount / m;
return this ;
}
int getAmount () {
return amount;
}
}
}
当然我们可以使用自定义注解来测试一个区间,比如金额在-100到100的金额测试,我们需要按照以下步骤来做:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 1,自定义一个注解,使用@ParametersSuppliedBy定义其参数来源
@Retention(RetentionPolicy.RUNTIME)
@ParametersSuppliedBy(BetweenSupplier.class)
public @interface Between {
int first(); // 起始值
int last(); // 结束值
}
// 2,定义参数来源
public static class BetweenSupplier extends ParameterSupplier {
@Override
public List<PotentialAssignment> getValueSources(ParameterSignature sig) {
Between annotation = sig.getAnnotation(Between.class);
return IntStream.rangeClosed(annotation.first(), annotation.last())
.mapToObj(i -> PotentialAssignment.forValue("ints", i))
.collect(Collectors.toList());
}
}
// 3,进行测试
@Theory
public void multiplyIsInverseOfDivideWithInlineDataPointsByCustomAnnotation(
@Between(first = -100, last = 100) int amount,
@Between(first = -100, last = 100) int m) {
assumeThat(m, not(0));
assertThat(new Dollar(amount).times(m).divideBy(m).getAmount(), is(amount));
}
JUnit对多线程的支持很差,我们可以使用ConcurrentUnit 来测试。但是如果只是简单测试多个线程能不能在固定时间内完成,可以使用以下方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static void assertConcurrent(final String message, final List<? extends Runnable> runnables, final int maxTimeoutSeconds) throws InterruptedException {
final int numThreads = runnables.size();
final List<Throwable> exceptions = Collections.synchronizedList(new ArrayList<Throwable>());
final ExecutorService threadPool = Executors.newFixedThreadPool(numThreads);
try {
final CountDownLatch allExecutorThreadsReady = new CountDownLatch(numThreads);
final CountDownLatch afterInitBlocker = new CountDownLatch(1);
final CountDownLatch allDone = new CountDownLatch(numThreads);
for (final Runnable submittedTestRunnable : runnables) {
threadPool.submit(new Runnable() {
public void run() {
allExecutorThreadsReady.countDown();
try {
afterInitBlocker.await();
submittedTestRunnable.run();
} catch (final Throwable e) {
exceptions.add(e);
} finally {
allDone.countDown();
}
}
});
}
// wait until all threads are ready
assertTrue("Timeout initializing threads! Perform long lasting initializations before passing runnables to assertConcurrent", allExecutorThreadsReady.await(runnables.size() * 10, TimeUnit.MILLISECONDS));
// start all test runners
afterInitBlocker.countDown();
assertTrue(message +" timeout! More than" + maxTimeoutSeconds + "seconds", allDone.await(maxTimeoutSeconds, TimeUnit.SECONDS));
} finally {
threadPool.shutdownNow();
}
assertTrue(message + "failed with exception(s)" + exceptions, exceptions.isEmpty());
}
使用maven-surefire-plugin可以完成对JUnit的用例覆盖率统计等功能。具体可以参考maven插件说明。
使用Infinitest插件 即可完成每次修改代码都进行UT测试。