我们都有个习惯,常常不乐意去写个简单的单元测试程序来验证自己的代码。对自己的程序一直非常有自信,或存在侥幸心理每次运行通过后就直接扔给测试组测试了。然而每次测试组的BUG提交过来后就会发现自己的程序还存在许多没有想到的漏洞。但是每次修改好BUG以后还是怀着侥幸心理,认为这次不会有bug了。然后又一次自信地提交,结果又败了。因为这样反复几次后。开发者花在找BUG和修复BUG的这些时间加起来已经比他开发这个模块花的时间还要多了。虽然项目经理已经预留了修改BUG和单元测试的时间。但是开发者却习惯性地在写好代码后就认为任务完成了。 然后等问题出来了bug改了很多次还是修复不了的时候才和项目经理说“我碰到预想不到的问题,可能要延期发布我的代码“。如果这个项目不可延期,痛苦的加班就无法避免了。
BUG是不可避免的,只是每次在修复一个BUG之前基本上无法知道这个BUG是哪段代码引起。每次定位BUG可能会耗去你一个小时还是一天,这还要取决于你的水平了。但是如果你的每段核心程序都有单元测试代码。你将不需要靠你的经验去判断或猜测BUG是由哪段程序引起。你只要运行你的单元测试方法。通过简单判断测试方法的结果就可以轻松定位BUG了。所以从表面上看,为每个单元程序都编写测试代码似乎是增加了工作量,但是其实这些代码不仅为你织起了一张保护网,而且还可以帮助你快速定位错误从而使你大大减少修复BUG的时间。而且这还有利你的身体健康,你将不会因为找不出BUG而痛苦不已,也将不用废寝忘食地加班了。而且项目的进度也将尽在掌握。
其实单元测试不仅能保证项目进度还能优化你的设计。有些开发者会说,写单元测试代码太费劲了,比写业务代码还麻烦。可是如果强迫开发者必须写单元测试代码的时候。聪明且又想‘偷懒’的开发人员为了将来可以更方便地编写测试代码。唯一的办法就是通过优化设计,尽可能得将业务代码设计成更容易测试的代码。慢慢地开发者就会发现。自己设计的程序耦合度也越来越低。每个单元程序的输入输出,业务内容和异常情况都会尽可能变得简单。最后发现自己的编程习惯和设计能力也越来越老练了。
其实容易测试的代码基本上可以和设计良好的代码划等号。因为一个单元测试用例其实就是一个单元的最早用户。容易使用显然意味着良好的设计。
单元测试的目的 测试当前所写的代码是否是正确的, 例如输入一组数据, 会输出期望的数据; 输入错误数据, 会产生错误异常等。
在单元测试中, 我们需要保证被测系统是独立的,即当被测系统通过测试时,那么它在任何环境下都是能够正常工作的。编写单元测试时, 仅仅需要关注单个类就可以了,而不需要关注例如数据库服务、Web 服务等组件。
模块 | 说明 |
---|---|
Assertions | 断言,单元测试中不可或缺的组成部分 |
Test Runners | 应该如何执行测试 |
Aggregating tests in Suites | 如何将多个相关测试组合到一个测试套件中 |
Test Execution Order | 指定运行单元测试的顺序 |
Exception Testing | 如何在单元测试中指定预期的异常 |
Matchers and assertThat | 如何使用Hamcrest匹配器和更具描述性的断言 |
Ignoring Tests | 如何禁用测试方法或类 |
Timeout for Tests | 如何指定测试的最长执行时间 |
Parameterized Tests | 编写可以使用不同参数值多次执行的测试 |
Assumptions with Assume | 类似于断言,但没有使测试失败 |
Rules | 停止扩展抽象测试类并开始编写测试规则 |
Theories | 使用随机生成的数据编写更像科学实验的测试 |
Test Fixtures | 在每个方法和每个类的基础上指定设置和清理方法 |
Categories | 将测试分组在一起以便于测试过滤 |
Multithreaded code and Concurrency | 并发代码测试的基本思路 |
以上4个注解只能修饰方法,对应模块是
Test Fixtures
。用于执行测试用例之前,对资源的初始化以及资源清理等工作。这么做的目的是为了避免多个测试用例相互影响。
以上2个注解可以修饰域和方法,对应模块是
Rules
。加Class
的目的用于修饰static
域或方法。
当需要临时禁用一个/组测试用例时,可以在已经标注@Test的方法中继续标注@Ignore,则该测试用例会在执行时被忽略。
此类允许用户选择测试类内方法的执行顺序。
@Test 修饰
public
(Junit5 以后能支持包访问权限)的方法,但凡测试用例抛出不可预期的异常即认定为测试用例执行失败。
假设是在断言之前增加前提条件,只有当条件成立时断言才会执行。 否则会抛出假设不通过的异常(但不会判定为测试用例失败,而是认为是忽略)。
import static org.junit.Assume.*
@Test public void filenameIncludesUsername() {
assumeThat(File.separatorChar, is('/'));
assertThat(new User("optimus").configFileName(), is("configfiles/optimus.cfg"));
}
@Test public void correctBehaviorWhenFilenameIsNull() {
assumeTrue(bugFixed("13356")); // bugFixed is not included in JUnit
assertThat(parse(null), is(new NullDocument()));
}
JUnit为所有原始类型、对象和数组(原语或对象)提供了重载断言方法。参数顺序是期望值,其次是实际值。可选地,第一个参数可以是在失败时输出的字符串消息。
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.both;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.everyItem;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import org.hamcrest.core.CombinableMatcher;
import org.junit.Test;
public class AssertTests {
@Test
public void testAssertArrayEquals() {
byte[] expected = "trial".getBytes();
byte[] actual = "trial".getBytes();
assertArrayEquals("failure - byte arrays not same", expected, actual);
}
@Test
public void testAssertEquals() {
assertEquals("failure - strings are not equal", "text", "text");
}
@Test
public void testAssertFalse() {
assertFalse("failure - should be false", false);
}
@Test
public void testAssertNotNull() {
assertNotNull("should not be null", new Object());
}
@Test
public void testAssertNotSame() {
assertNotSame("should not be same Object", new Object(), new Object());
}
@Test
public void testAssertNull() {
assertNull("should be null", null);
}
@Test
public void testAssertSame() {
Integer aNumber = Integer.valueOf(768);
assertSame("should be same", aNumber, aNumber);
}
// JUnit Matchers assertThat
@Test
public void testAssertThatBothContainsString() {
assertThat("albumen", both(containsString("a")).and(containsString("b")));
}
@Test
public void testAssertThatHasItems() {
assertThat(Arrays.asList("one", "two", "three"), hasItems("one", "three"));
}
@Test
public void testAssertThatEveryItemContainsString() {
assertThat(Arrays.asList(new String[] { "fun", "ban", "net" }), everyItem(containsString("n")));
}
// Core Hamcrest Matchers with assertThat
@Test
public void testAssertThatHamcrestCoreMatchers() {
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())));
}
@Test
public void testAssertTrue() {
assertTrue("failure - should be true", true);
}
}
@Test(expected = IndexOutOfBoundsException.class)
public void empty() {
new ArrayList<Object>().get(0);
}
@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
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); // execution will never get past this line
}
表达形式如下:
assertThat([value], [matcher statement]);
assertThat(x, is(3));
assertThat(x, is(not(4)));
assertThat(responseString, either(containsString("color")).or(containsString("colour")));
assertThat(myList, hasItem("3"));
它的好处是非常灵活,并且外部有扩展实现(org.hamcrest
)可以无缝使用。
虽然对开发人员来说,这套Matchers
的设计显得有些画蛇添足。但对测试人员来讲,这套设计可以减少很多麻烦。
按需取用即可。
我们都知道@Test
修饰方法是不能加参数的,否则在执行时会抛出异常。但是的确存在需要参数的情况,可以使用以下方式进行实现。
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.Collection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class FibonacciTest {
@Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {
{ 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 }
});
}
private int fInput;
private int fExpected;
public FibonacciTest(int input, int expected) {
this.fInput = input;
this.fExpected = expected;
}
@Test
public void test() {
assertEquals(fExpected, Fibonacci.compute(fInput));
}
}
示例代码实现了使用7组参数输入,来验证斐波那契数列的合法性。
在做单元测试的时候,我们会发现我们要测试的方法会引用很多外部依赖的对象,比如:(发送邮件,网络通讯,记录Log, 文件系统 之类的)。 而我们没法控制这些外部依赖的对象。 为了解决这个问题,我们需要用到Stub和Mock来模拟这些外部依赖的对象,从而控制它们。
JUnit是单元测试框架,可以轻松的完成关联依赖关系少或者比较简单的类的单元测试,但是对于关联到其它比较复杂的类或对运行环境有要求的类的单元测试,模拟环境或者配置环境会非常耗时,实施单元测试比较困难。而这些“mock框架”(Mockito 、jmock 、 powermock、EasyMock),可以通过mock框架模拟一个对象的行为,从而隔离开我们不关心的其他对象,使得测试变得简单。(例如service调用dao,即service依赖dao,我们可以通过mock dao来模拟真实的dao调用,从而能达到测试service的目的。)
模拟对象(Mock Object)可以取代真实对象的位置,用于测试一些与真实对象进行交互或依赖于真实对象的功能,模拟对象的背后目的就是创建一个轻量级的、可控制的对象来代替测试中需要的真实对象,模拟真实对象的行为和功能。
Mockito简单运用说明
when(mock.someMethod()).thenReturn(value)
设定mock对象某个方法调用时的返回值。可以连续设定返回值,即when(mock.someMethod()).thenReturn(value1).thenReturn(value2)
,第一次调用时返回value1
,第二次返回value2
。也可以表示为如下:when(mock.someMethod()).thenReturn(value1,value2)
。when(mock.someMethod()).thenThrow(new RuntimeException());
doReturn(value).when(mock.someMethod()) doThrow(new RuntimeException()).when(mock.someMethod())
doNothing().when(mock.someMethod()) doThrow(new RuntimeException()).when(mock.someMethod()) doNothing().doThrow(new RuntimeException()).when(mock.someMethod())
verify(mock,times(n)).someMethod(argument)
,n为被调用的次数,如果超过或少于n都算失败。除了times(n),还有never(),atLease(n),atMost(n)。verify(mock, timeout(100)).someMethod();
verify(mock, timeout(100).times(1)).someMethod();
JUnitCore
是main()
方法入口类,所有单元测试用例由这里开始执行。
public class JUnitCore {
private final RunNotifier notifier = new RunNotifier();
public static void main(String... args) {
Result result = new JUnitCore().runMain(new RealSystem(), args);
System.exit(result.wasSuccessful() ? 0 : 1);
}
// ignore
}
args
是测试类的类名,通过执行runMain()
方法得到单元测试结果result
。
public class Result implements Serializable {
private static final ObjectStreamField[] serialPersistentFields =
ObjectStreamClass.lookup(SerializedForm.class).getFields();
private final AtomicInteger count;
private final AtomicInteger ignoreCount;
private final CopyOnWriteArrayList<Failure> failures;
private final AtomicLong runTime;
private final AtomicLong startTime;
// ignore
}
继续看一眼Failure
这个类的构成:
public class Failure implements Serializable {
private final Description fDescription;
private final Throwable fThrownException;
// ignore
}
阅读源码我的做法是:先从顶层开始闭环,再逐渐向下分析,切勿在第一层架构上就深入到第二层第三层等,先闭合每一层再逐步深入。
在0层阶段,我们得到如下结论:传入测试类的类名数组,经过内部处理后,返回测试用例执行结果。这些结果包含:执行次数、忽略次数、失败信息描述及异常、执行开始时间、执行运行时间。
public class JUnitCore {
Result runMain(JUnitSystem system, String... args) {
// step 2.1
JUnitCommandLineParseResult jUnitCommandLineParseResult = JUnitCommandLineParseResult.parse(args);
// step 2.2
RunListener listener = new TextListener(system);
addListener(listener);
// step 2.3
return run(jUnitCommandLineParseResult.createRequest(defaultComputer()));
}
}
先看JUnitCommandLineParseResult
的数据结构,在跟踪一眼
class JUnitCommandLineParseResult {
private final List<String> filterSpecs = new ArrayList<String>();
private final List<Class<?>> classes = new ArrayList<Class<?>>();
private final List<Throwable> parserErrors = new ArrayList<Throwable>();
void parseParameters(String[] args) {
for (String arg : args) {
try {
classes.add(Classes.getClass(arg));
} catch (ClassNotFoundException e) {
parserErrors.add(new IllegalArgumentException("Could not find class [" + arg + "]", e));
}
// ignore
}
classes
由字符串构建成Class<?>
对象,目的必然是反射。parserErrors
是上一步构建Class<?>
对象失败时,存储异常信息的容器。filterSpecs
尚未调用到,先忽略。至此对所有传入的args
校验和初始化算式完成了。接着初始化了TextListener
对象并添加到RunNotifier
中,目的是执行测试用例时候控制台的输出日志。
前期的准备工作已经做好了,剩下的就是准备真正命令对象,在JUnit
中它的定义是org.junit.runner.Request
。最后在调用一下JUnitCore.run()
方法就完成调用了。
在1层阶段,我们看到对args
的预处理。JUnit
设计人员使用org.junit.runner.Request
来作为命令对象(命令模式),JUnitCore
作为门面类揽下:创建Request
,调度Request
,以及生命周期回调管理等一系列脏活。
综上我们可以推断出阅读的重点在:
Request
的构成?支持哪些Request?Request
?调用后Result
是否有再加工?NotifyListener
生命周期?Request
的构成?支持哪些Request?class JUnitCommandLineParseResult {
public Request createRequest(Computer computer) {
if (parserErrors.isEmpty()) {
Request request = Request.classes(
computer, classes.toArray(new Class<?>[classes.size()]));
return applyFilterSpecs(request);
} else {
return errorReport(new InitializationError(parserErrors));
}
}
// ignore
}
异常分支暂不深入看,且看正常情况下的两步:
Request.classes()
构建了Request
对象applyFilterSpecs()
似乎是过滤了某些Specs(特征?)
public abstract class Request {
public static Request classes(Computer computer, Class<?>... classes) {
try {
AllDefaultPossibilitiesBuilder builder = new AllDefaultPossibilitiesBuilder(true);
Runner suite = computer.getSuite(builder, classes);
return runner(suite);
} catch (InitializationError e) {
throw new RuntimeException(
"Bug in saff's brain: Suite constructor, called as above, should always complete");
}
}
public static Request runner(final Runner runner) {
return new Request() {
@Override
public Runner getRunner() {
return runner;
}
};
}
}
千回百转computer.getSuite()
最终还是回到了AllDefaultPossibilitiesBuilder.runnerForClass()
来构建Runner
对象。
public class AllDefaultPossibilitiesBuilder extends RunnerBuilder {
@Override
public Runner runnerForClass(Class<?> testClass) throws Throwable {
List<RunnerBuilder> builders = Arrays.asList(
ignoredBuilder(),
annotatedBuilder(),
suiteMethodBuilder(),
junit3Builder(),
junit4Builder());
for (RunnerBuilder each : builders) {
Runner runner = each.safeRunnerForClass(testClass);
if (runner != null) {
return runner;
}
}
return null;
}
// ignore
}
each.safeRunnerForClass(testClass);
方法会依据你当前所配置的@RunWith
注解来选择实现方法。目前我们使用@RunWith(Junit4.class)
。
public class JUnit4Builder extends RunnerBuilder {
@Override
public Runner runnerForClass(Class<?> testClass) throws Throwable {
return new BlockJUnit4ClassRunner(testClass);
}
}
划重点:到此我们得知默认情况下,单元测试最终创建的
Runner
都是BlockJUnit4ClassRunner
类型,而Request
又仅是对Runner
的封装,所以只需要精读BlockJUnit4ClassRunner
方法即可。
Request
对象已经准备妥当,接着程序执行到applyFilterSpecs()
方法。
class JUnitCommandLineParseResult {
private Request applyFilterSpecs(Request request) {
try {
for (String filterSpec : filterSpecs) {
Filter filter = FilterFactories.createFilterFromFilterSpec(
request, filterSpec);
request = request.filterWith(filter);
}
return request;
} catch (FilterNotCreatedException e) {
return errorReport(e);
}
}
// ignore
}
啥都不看,凭感觉猜就知道是过滤某些请求(对应注解@Ignore)。但咱们还是务实一点,看看代码。
public abstract class Request {
public Request filterWith(Filter filter) {
return new FilterRequest(this, filter);
}
// ignore
}
这结构熟不熟悉?典型的装饰器模式——将Filter
的职责装饰到原来的Request
对象上。
public final class FilterRequest extends Request {
// ignore
@Override
public Runner getRunner() {
try {
Runner runner = request.getRunner();
fFilter.apply(runner);
return runner;
} catch (NoTestsRemainException e) {
return new ErrorReportingRunner(Filter.class, new Exception(String
.format("No tests found matching %s from %s", fFilter
.describe(), request.toString())));
}
}
}
public abstract class Filter {
public void apply(Object child) throws NoTestsRemainException {
if (!(child instanceof Filterable)) {
return;
}
Filterable filterable = (Filterable) child;
filterable.filter(this);
}
// ignore
}
至此Request
对象的构成已经完全透明,在JUnit
中有如下几种:
基于以上的分析,我们知道要实现:对测试用例进行特定排序,并且过滤掉部分用例的需求
是非常容易实现的 —— 装饰器。
Request
?调用后Result
是否有再加工?public class JUnitCore {
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;
}
// ignore
}
执行runner.run(notifier);
的前后环绕notifier
通知,执行完removeListener
避免内存泄露。生命周期回调这块太直接,直接略过。跟一下runner.run(notifier)
看看。
基于上一个段落的分析,我们知道Runner
的实例类型是BlockJUnit4ClassRunner
,所以直接看它的run()
方法。
BlockJUnit4ClassRunner
继承自ParentRunner
:
public abstract class ParentRunner<T> extends Runner implements Filterable,
Sortable {
protected Statement childrenInvoker(final RunNotifier notifier) {
return new Statement() {
@Override
public void evaluate() {
runChildren(notifier);
}
};
}
protected Statement classBlock(final RunNotifier notifier) {
Statement statement = childrenInvoker(notifier);
if (!areAllChildrenIgnored()) {
statement = withBeforeClasses(statement);
statement = withAfterClasses(statement);
statement = withClassRules(statement);
}
return statement;
}
@Override
public void run(final RunNotifier notifier) {
EachTestNotifier testNotifier = new EachTestNotifier(notifier,
getDescription());
try {
Statement statement = classBlock(notifier);
statement.evaluate();
} catch (AssumptionViolatedException e) {
testNotifier.addFailedAssumption(e);
} catch (StoppedByUserException e) {
throw e;
} catch (Throwable e) {
testNotifier.addFailure(e);
}
}
// ignore
}
这个方法的返回类型是void
,并且例外了两种异常:
AssumptionViolatedException
表明假设不成立,无任何异常抛出。StoppedByUserException
用户主动停止单元测试,单独抛出异常。testNotifier
接口异常。最最最最重要的部分就是与Statement
相关联的部分,这部分是单元测试的核心功能。classBlock
方法做的事情:将测试类中的测试用例映射成Statement
对象,并按照@Before
>@Test
>@After
的顺序构建职责链。
构建完成后调用statement.evaluate()
,这是最后的挣扎调用了。所有的evaluate()
都会进到方法:
// ParentRunner.class
private volatile RunnerScheduler scheduler = new RunnerScheduler() {
public void schedule(Runnable childStatement) {
childStatement.run();
}
public void finished() {
// do nothing
}
};
private void runChildren(final RunNotifier notifier) {
final RunnerScheduler currentScheduler = scheduler;
try {
for (final T each : getFilteredChildren()) {
currentScheduler.schedule(new Runnable() {
public void run() {
ParentRunner.this.runChild(each, notifier);
}
});
}
} finally {
currentScheduler.finished();
}
}
public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> {
@Override
protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
Description description = describeChild(method);
if (isIgnored(method)) {
notifier.fireTestIgnored(description);
} else {
runLeaf(methodBlock(method), description, notifier);
}
}
/**
* Runs a {@link Statement} that represents a leaf (aka atomic) test.
*/
protected final void runLeaf(Statement statement, Description description,
RunNotifier notifier) {
EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description);
eachNotifier.fireTestStarted();
try {
statement.evaluate();
} catch (AssumptionViolatedException e) {
eachNotifier.addFailedAssumption(e);
} catch (Throwable e) {
eachNotifier.addFailure(e);
} finally {
eachNotifier.fireTestFinished();
}
}
protected Statement methodBlock(FrameworkMethod method) {
Object test;
try {
test = new ReflectiveCallable() {
@Override
protected Object runReflectiveCall() throws Throwable {
return createTest();
}
}.run();
} catch (Throwable e) {
return new Fail(e);
}
Statement statement = methodInvoker(method, test);
statement = possiblyExpectingExceptions(method, test, statement);
statement = withPotentialTimeout(method, test, statement);
statement = withBefores(method, test, statement);
statement = withAfters(method, test, statement);
statement = withRules(method, test, statement);
return statement;
}
}
执行evaluate()
调用是整个JUnit
中最简单的事情了,复杂性体现在构建Statement
的职责链上,比如:前面对基本@Test
的用例的构建,到现在在methodBlock()
中追加@Timeout @ExpectingException
相应的处理。
单元测试不是来恶心开发者的,它是帮助开发者尽早发现问题的利器。因为问题越往后发现,它的修复成本就会越高。
GitHub上绝大多数优秀的项目单元测试的覆盖率都是90%以上,在这些项目(前端、后端、客户端)里面,我们可以从中学到丰富的测试技巧。所以,不能说不知道怎么写单元测试噢~