Mockito 也能 Mock final 类和 final 方法了

以实际 Java 项目中的单元测试 Mock 框架基本是 Mockito 2 了,因为它有一个十分流畅的 API。Mockito  2也为 JUnit 5 配上了 MockitoExtension, 所以 JUnit 5 下使用 Mockito 2 的关节也打通了。但在我们享受 Mockito 2 便利的同时,与 JMockit 相比局限性就很明显,因为 Mockito 2 是通过创建匿名子类来进行 Mock 的,所以任何子类对父类无法突破的方面它都存在。譬如,final 类, final 方法, 私有方法, 静态方法, 构造函数都是无法通过子类型进行重写的。所以除非特别需要,在 Mockito 2 无法胜任时都求助于 JMockit,JMockit 借助于 javaagent 取得了 JVM 的高控制权才得已为所欲为。

当 Mockito 来到了 2.1.0 版本,它也觉得不能对以上所有的限制置若罔闻, 首先带给我们的突破是它也可以 Mock final 类和 final 方法,虽然仍处于孵化器中,但毕竟是应用在单元测试中,能用就很不错了,只要以后不被拿走就行。这是官方对它的介绍 Mock the unmockable: opt-in mocking of final classes/methods

下面我亲自操作一遍,并给出更全方位的测试样例 阅读全文 >>

JUnit 中是以测试方法为一个独立的生命周期

在研究 JUnit 5 新特性的时候,学习到其中有一节 Test Instance Lifecycle, 才意识到对 JUnit 的理解一直存在一个误区,以为 JUnit 是以测试类为一个生命周期的,其实不然。不管是 JUnit 5 还是 JUnit 4 或更早的版本,JUnit 都是以测试方法为一个独立的生命周期。

只是到了 JUnit 5 提供了方法来把生命周期由方法改为测试类,对于单个测试类可以使用注解 @TestInstance(Lifecycle.PER_CLASS) 来指定用一个测试实例来跑所有的测试方法,这就意味着测试类中的成员变量只被初始化一次。@TestInstance 的 Lifecycle 默认是 PER_METHOD, JUnit 4 就是 PER_METHOD, 而且是不能改的。如果在 JUnit 5 中改变为 PER_CLASS, 恐怕反而会出许多乱子,每个测试方法本就该是完全独立的。

比如在同一个类中多个测试方法使用了同一个实例变量的情况下,总会用一个  @After 方法来复位该实例变量,现在才知道那是多余的。像下面的代码

阅读全文 >>

Java 单元测试如何断言(检查)控制台输出

关于在 JUnit 单元测试中如何断言某个函数的控制台输出已是我一个长久的问题. 虽然有控制台输出的函数有了副作用, 不能称之为一个纯函数, 在讲求函数式编程的今天, 纯函数是最好测试的, 所谓的 Data In, Data Out. 但总还是有这样的需求, 比如自己实现的某个日志框架的 Appender, 需要验证它向控制台的输出内容.

我先前在项目中的办法是, 先把把标准输出定向到一个 ByteArrayOutputStream 中去, 完后把这个流转成字符串来断言它的内容, 最后恢复标准输出为 System.out, 代码如下:

ByteArrayOutputStream output = new ByteArrayOutputStream();
System.setOut(new PrintStream(output));

System.out.print("Hello");

assertThat(output.toString(), is("Hello");
System.setOut(System.out);

这样也能完成任务, 本质也是对的, 但稍显复杂了些. 今天读 Spring in Action 一书, 发现它用了 StandardOutputStreamLog 这个 JUnit 的 @Rule, 来自于 System Rules. 其实 StandardOutputStreamLog 类已不推荐使用, 取而代之的是 SystemOutRule, 所以应用 SystemOutRule 来断言控制台输出的测试方法就是 阅读全文 >>

让 Scala 测试方法名中的空格不再显示为 $u0020

在 Scala 中借助于斜撇号我们可以使用关键字或任何字符为作为变量或方法名,例如下面的方法都是合法的:

scala> def if: Unit = {}
if: Unit

scala> def 我是谁?: Unit = {}
我是谁$qmark: Unit

scala> def just do it: Unit = {}
just$u0020do$u0020it: Unit

对了,Scala 2.11.6 在显示有些字符时会进行编码,像上面的 ? 和 空格。而用 2.11.7 的 Scala  控制台下居然原样显示,不编码(这是在进一步试难时发现的)。

我们在用 Scala 写 JUnit 风格的单元测试, 由于 JUnit 不支持像 TestNG 那样在方法名上加描述,所以方法名必须完成自我描述。依照严格的方法命名的话,势必要用驼峰方式或下划线把单词分开,如

def getOneHunderIfSixtyPlusFouty: Unit = { ... }
def get_one_hunder_if_sixty_plus_forty: Unit = { ... }

注:如果用 Scala 写 Spec 测试代码另当别论,因为有些情况下必须用 JUnit 风格(就用 JMockit 时) 阅读全文 >>

Scala + JUnit 怎么使用 @Rule

JUnit 是个很著名的飞行模式测试框架,即使到了 Scala 中还是免不了要用 JUnit Style 的测试方式,基于 Spec 的方式并不处处行得通,比如想要在 Scala 中使用 JMockit 框架时。

JUnit 提供给我们有两个扩展点,RunnerRule, Runner 扩展点一般被各种框架劫持了,自己搞个 @RunWith(SomeRunner.class) 可能让你无法在测试中应用框架。于是剩下了 Rule 是个更自由的扩展点,这里不讲述怎么定制自己的 Rule,而是怎么用它,怎么在 Scala 中用它。之前的一篇 JUnit 4 如何正确测试异常 中使用了 ExpectedException 这个 Rule。

Rule 的要求是: Annotates fields that reference rules or methods that return a rule. A field must be public, not static, and a subtype of TestRule (preferred) or MethodRule. A method must be public, not static, and must return a subtype of TestRule (preferred) or MethodRule. 属性或方法必须是 public 非静态的,它们的类型或返回类型必须分别是 TestRuleMethedRule

这里尝试以 Java 的方式使用另一个 Rule,TestName, 可以得到当前测试方法的名称 阅读全文 >>

JMockit 一个 Expectations 中 Mock 多个方法

从 JMockit 系列的开篇 JMockit 之 Expectations 中了解到了一个最基本的 Mock 的写法,这里记录下在一个 Expectations 中如何同时 Mock 多个方法。基本框架是这样的:

        new Expectations(MyService.class, ExternalService.class) {
            {
                MyService.prefix("Unmi");
                result = "Welcome to website: ";
                
                ExternalService.suffix("Unmi");
                result = "http://unmi.cc";
            }
        };

Java 语法告诉我们 new Expectations(){{......}} 省略号处的代码会在 Expectations 匿名类实例初始化时被调用,那么其中对 result 的赋值便是新创建的 Expectations 匿名类实例的 result 的属性值,那两次的 result 赋值难道不是以最后一个为准吗,有点文章了。先来跑个例子,见识一下现象,由三个类组成,分别是: 阅读全文 >>

JMockit Mock 私有方法和私有属性

前面说过 JMockit 因身处前线,所以简直无不可,本节例子演示 JMockit 怎么 Mock 私有方法和私有属性,示例虽然是静态方法和属性,但因采用的是反射手法,所以这种 Deencapsulation 的 Mock 手段同样适用于公有的方法或属性,无论是否静态。

本文所用 JMockit 版本为 1.6, 可能网上所搜索的方法与此有所不同,请注意 JMockit 版本差异。仍需重复一下,运行 JMockit 的例子 classpath 上必须让 jmockit.jar 在 junit.jar 之前,或用 javaagent 参数来加载 jmockit.jar,并且 junit 要 4.8 及以上版本.

1. Mock 私有方法(非静态类似) 阅读全文 >>

JMockit 之 Expectations

TDD 推求测试先行,不光在自己代码未实现时可以先做好测试,即使平台依赖或第三方接口未准备好我们也能先行一步的,这就要对接口依赖进行 Mock。同时 Mock 也使得我们的测试代码在运行当中不至于随着第三方接口的沦陷而坠入深渊。

Java 中 Mock 工具也不少,像通用 EasyMock, jMock, Mockito, Unitils Mock, PowerMock, 再比如偏专业的 HttpMock, StrutsMock 等。但 JMock 与前面各位相比简直是全能选手,对 final/static/native/private 方法都能 Mock,功能上还远不止这些了,可以看看一个对比图 https://code.google.com/p/jmockit/wiki/MockingToolkitComparisonMatrix

JMockit 是基于 Java5 的 java.lang.instrument 包开发的,所以它才能夺得先机,也可陷得更深。自然它要求 JDK5 及以上,JUnit 4.8 及以上版本。命令行下原来用 -javaagent:/.../lib/jmockit.jar 加载 JMockit,现在发现把 jmockit.jar 放在 classpath 下就 OK 的,但是必须放在 junit.jar 包之前,否则你会看到这个 java.lang.IllegalStateException: JMockit wasn't properly initialized; check that jmockit.jar precedes junit.jar in the classpath。 JMockit 有两种 Mock 方式:

1. Behavior-oriented(Expectations & Verifications)  --- 基于代码执行行为的模仿,象黑盒测试
2. State-oriented(MockUp<GenericType>)   --- 侵入类内部,随意模仿,似白盒,可以说是能为所欲为

此篇体验下第一种 Mock 方式,在测试代码中最直观就是那个 new Expectations(...){{result = some;}},下面来看个实际的例子。应用场景是 阅读全文 >>

扩展 JUnit 4,使用定制的 Runner

JUnit 的测试用例总是由 Runner 去执行,JUnit 提供了 @RunWith 这个测试类的 Annotation, 可来指定自定义的 Runner。如果未指定特别的  Runner,那么会采用默认的 Runner,可能不同的环境,如 Eclipse,控制台下会有不同的默认 Runner。

如果不清楚 Runner 是什么,那么可能见过 @RunWith(SpringJUnit4ClassRunner.class) 这个东西,它有助你加载 Spring 的配置文件,及与 Spring 相关的事物。

那么自定义的 Runner 有什么用呢?它可以截获到 @BeforeClass, @AfterClass, @Before, @After 这些事件,也就是能在测试类开始和结束执行前后,每个测试方法的执行前后处理点事情。

比如说从外部读取内容进行初始化测试数据,而且 JUnit 本身就提供了 @RunWith(Parameterized.class)  这个参数化 Runner,用了为带参数测试方法循环填充数据进行测试。JUnit 的参数化测试比 C# 还是要笨拙一些,C# 直接用方法注解一行行设置参数,我想 JUnit 稍加定制的话也行的。 阅读全文 >>

JUnit 4 如何正确测试异常

本篇讲述如何在 JUnit 4 下正确测试异常,我会从 try..catch 的方式谈起,然后说到 @Test(expected=Exception.class), 最后论及 @Rules public ExpectedException 的实现方式,最终基本可确定用 @Rules 是最方便的。

我们在用 JUnit 测试方法异常的时候,最容易想到的办法就是用 try...catch 去捕获异常,需要断言以下几个条件:

1. 确实抛出的异常
2. 抛出异常的 Class 类型
3. 抛出异常的具体类型,一般检查异常的 message 属性中包含的字符串的断定

所以常用的代码你可能会这么写:

这里被测试的方法是 Password.validate() 方法是否抛出了相应的异常,注意这里别漏 try 中的

fail("No Exception thrown.") 阅读全文 >>