单元测试之JUnit4

JUnit4

JUnit是一个帮助编写和执行单元测试的框架。可能很多人都接触过单元测试,但是只是停留在copy别人的测试代码再改一下的状态,下文尝试较为体系列举JUnit4中比较关键的一些知识点。转载请注明来源「Bug总柴」

Assertions断言

判断结果是否满足预期,Junit有以下几种断言方法:assertArrayEqualsassertEqualsassertFalseassertNotNullassertNotSameassertNullassertSameassertTrueassertThat

Hamcrest Mathers

Hamcrest扩展JUnitassertThat的Matcher类型,支持以下matchers:

支持类型 常用matchers
Core anything、describedAs、is
Logical allOf、anyOf、not、both、either
Object equalTo、hasToString、instanceOf、isCompatibleType、notNullValue、nullValue、sameInstance、theInstance
Beans hasProperty
Collections array、hasEntry、hasKey、hasValue、hasItem、hasItems、hasItemInArray、everyItem
Number closeTo、greaterThan、greaterThanOrEqualTo、lessThan、lessThanOrEqualTo
Text equalToIgnoringCase、equalToIgnoringWhiteSpace、containsString、endsWith、startsWith

使用assertThat具有更好的可读性和出错信息,建议大多数情况下使用assertTaht来进行断言判断。

Runner执行器

Runner执行器用于组织和执行在一个类中的测试,可以在执行器中做一些必须的前置和后置工作。使用@RunWith注解可以指定测试的执行类。如果没有使用@RunWith指定测试执行器,默认会使用BlockJunit4ClassRunner。每个测试类只能指定一个Runner。除了默认的Runner还有以下的Runner:

Runner名称 作用 备注
Suite 将分布在多个类中的测试组合在一起作为一个测试执行 JUnit自带 文档
Categories 执行多个类具有某些类别标志的一组测试 JUnit自带 文档
Parameterized 使用同一种类型的多个数据重复执行同一个测试类的所有测试 JUnit自带 文档
Theories 使用多种类型的数据的排列组合执行同一个测试类的所有测试 JUnit自带 文档
SpringJUnit4ClassRunner 提供Spring上下文支持的测试执行类 继承自BlockJUnit4ClassRunner
MockitoJUnitRunner 最终会构造DefaultInternalRunner,根据mockito的注解在测试之前生成mock对象 详见mockito介绍
PowerMockRunner 最终通过PowerMockJUnit44RunnerDelegateImpl.executeTest()方法,将被@PrepareForTest@PrepareOnlyThisForTest@SuppressStaticInitializationFor标注的类使用MockClassLoader进行加载,实现对静态以及final对象的mock 详见powermock文档
AndroidJUnit4 会根据是否在Android设备执行,选择AndroidJUnit4ClassRunner或者RobolectricTestRunner(AndroidX版本)。在Android中执行测试时,可以获得运行的Instrumentation和Bundle参数,以及可以使用@UiThreadTest标记测试方法在UI线程执行 原理可以参阅这篇文章
RobolectricTestRunner 直接在安卓真机或者模拟器运行测试通常会比较慢,RobolectricTestRunner继承自SandboxTestRunner,以提供在JVM中的Android运行时环境 详见Robolectric官网
其他 其他的Runner可以看这里

Rule规则

使用Rule可以对一个或者一组测试的方法进行修改,可以向测试方法中添加额外逻辑来决定测试是否通过,也可以代替@Before@After@BeforeClass@AfterClass来实现初始化和清理工作。换句话而言,Rule相当于是相对测试方法独立的作用于测试方法中的额外处理逻辑。多个Rule可以顺序叠加。如果一个规则标注为@Rule则对测试类的每个方法生效,如果一个规则标注为@ClassRule则只会在整个测试类的所有方法开始之前和结束只会生效一次。以下常见的Rules如下:

Rules名称 作用 备注
ErrorCollector 使用ErrorCollector.checkThat()方法可以在执行完整个测试方法之后再报错,不会因为测试方法中的某一个错误而提前终止测试 JUnit自带 文档
ExpectedException 使用ExpectedException.expect()方法指定测试方法需要抛出的异常,当测试方法没有抛出异常或者抛弃不符合预期的异常时判定测试失败 JUnit自带 文档
ExternalResource 类似于@Before@After的效果,只是用了Rule来实现,可以声明发生在测试之前和测试之后的行为 JUnit自带文档
TemporaryFolder 在测试方法之前创建一个存放测试临时文件的目录,在测试结束后会自动删除 JUnit自带 文档
TestWatcher 可以用来监测测试方法执行的生命周期,包括开始、成功、错误、结束等 JUnit自带 文档
TestName 继承自TestWatcher,用来获取每个测试方法的名字 JUnit自带 文档
Timeout 将测试类中的每个测试方法都是用独立的线程执行,并等待一段时间。若等待时间内没有结果返回则报错。如果设置等待时间为0,则表示没有超时只是在线程中执行。 JUnit自带 文档
RuleChain 将多个Rule按照指定的顺序作用于测试方法中 JUnit自带 文档
Verifier ErrorCollector的基类,抽象类,表示可以在运行完测试方法后做一些验证操作 JUnit自带 文档
MockitoRule 是一个扩展MethodRule的接口,通过JUnitRule实现,会在执行测试方法之前,初始化所有mock对象。这个rule的作用与MockitoJUnitRunner类似 文档
PowerMockRule 最终通过PowerMockAgentTestInitializer.initialize()方法将被@PrepareForTest、@PrepareOnlyThisForTest、@SuppressStaticInitializationFor标注的类使用MockClassLoader进行加载,实现对静态以及final对象的mock,作用与PowerMockRunner类似 文档
ProviderTestRule 在测试方法之前对ContentProvider进行初始化,可以执行相应的数据库操作。 文档
ServiceTestRule 调用ServiceTestRule.startService()或者ServiceTestRule.bindService()在测试方法中建立Service连接,在测试结束后会自动关闭Service。不适用于IntentService,可以对其他Service进行测试。 文档
ActivityTestRule 可以自动在测试方法和@Before之前启动Activity,并在测试方法结束和@After之后结束Activity。也可以手动调用ActivityTestRule.launchActivity()ActivityTestRule.finishActivity() 文档
GrantPermissionRule 帮助在Android API 23及以上的环境申请运行时权限。申请权限时可以避免用户交互弹窗占用UI测试焦点。最终会调用PermissionRequester.requestPermissions()方法,通过执行UiAutomationShellCommand直接在shell中为当前target申请权限 文档
ActivityScenarioRule 作为ActivityTestRule的替代,在测试方法之前启动一个activity,并在测试方法之后结束activity。同时可以在测试方法中获得ActivityScenario ActivityScenarioRule文档 / ActivityScenario文档
InstantTaskExecutorRule 用于Architecture Components的测试,可以将默认使用的后台executor转为同步执行,让测试可以马上获得结果 文档
CountingTaskExecutorRule 可以使用CountingTaskExecutorRule.drainTasks()方法手动等待所有Architecture Components的后台任务执行完毕 文档
IntentsTestRule 在测试之前会初始化Espresso的Intent,可以使用Espresso Intents.intended()方法校验activity操作触发的intent espresso intent

测试默认执行流程源码分析

整个测试的执行过程是对Statement根据@BeforeClass@AfterClass@Before@After、Rules的按照装饰者模式进行的层层包装。最后会根据这些包装的规则一步一步执行测试。

// BlockJUnit4ClassRunner会继承ParentRunner
public abstract class ParentRunner<T> extends Runner implements Filterable, Sortable {

    // 执行测试
    @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);
        }
    }
    
    // 在执行类中测试的前后加上BeforeClass和AfterClass逻辑
    protected Statement classBlock(final RunNotifier notifier) {
        // 执行测试类中的测试方法
        Statement statement = childrenInvoker(notifier);
        if (!areAllChildrenIgnored()) {
            // 这里会对类中的测试加上Before和After的逻辑
            statement = withBeforeClasses(statement);
            statement = withAfterClasses(statement);
            statement = withClassRules(statement);
        }
        return statement;
    }
    
    protected Statement childrenInvoker(final RunNotifier notifier) {
        return new Statement() {
            @Override
            public void evaluate() {
                runChildren(notifier);
            }
        };
    }
    
    private void runChildren(final RunNotifier notifier) {
        final RunnerScheduler currentScheduler = scheduler;
        try {
            for (final T each : getFilteredChildren()) {
                currentScheduler.schedule(new Runnable() {
                    public void run() {
                        // 最终先执行runChild
                        ParentRunner.this.runChild(each, notifier);
                    }
                });
            }
        } finally {
            currentScheduler.finished();
        }
    }
}
// 默认JUnit4 Runner BlockJUnit4ClassRunner对runChild进行处理
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 {
            // 调用methodBlock加入Before/After以及Rules逻辑
            runLeaf(methodBlock(method), description, notifier);
        }
    }
    
    // 最终会调用After/Before以及Rule逻辑
    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);
        // 在测试的前后加上Before/After以及withRules
        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;
    }
}
// 对于Rule规则,最终会调用TestRule.apply()方法
public class RunRules extends Statement {
    private final Statement statement;

    public RunRules(Statement base, Iterable<TestRule> rules, Description description) {
        statement = applyAll(base, rules, description);
    }

    @Override
    public void evaluate() throws Throwable {
        statement.evaluate();
    }

    private static Statement applyAll(Statement result, Iterable<TestRule> rules,
            Description description) {
        for (TestRule each : rules) {
            // 顺序加上Rule的逻辑
            result = each.apply(result, description);
        }
        return result;
    }
}

JUnit后记

如果有能代替Runner的Rule,最好使用Rule,因为一个测试类可以指定多个Rule,但是只能声明一个Runner。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349