简介

  1. 自动化开发人员测试和测试独立
  2. 使用模拟对象进行测试
  3. 使用模拟对象进行测试的工具
  4. 常见的使用模拟对象的问题
  5. 一个测试用例
    1. 使用 Expectations API
    2. 使用 Mockups API
  6. 使用 JMockit 运行测试用例
    1. 使用 JUnit Ant 任务运行测试用例
    2. 使用 Maven 运行测试用例

在这个教程中我们将会通过使用测试用例例子进行 JMockit 可以使用的 API 学习。主要的用来进行模拟注入的 API是Expectations API。其次是 Mockups API,主要是用来进行解决伪装使用的,为了避免处理复杂的外部组件。

尽管这个教程比较完整,它仍然不能具体的覆盖所有的已经发布的APIs。 一个完成和详细的定义关于所有的公开类,方法等 是通过API 文档进行提供的。 每一个版本的工具文档都能够在 "jmockit.github.io/api1x" 文件夹下发现, 里面包括了完整的分发zip 文件。 这些主要的库文件"jmockit.jar" (和同等的maven文件中), 包含了 Java 源文件 (包含Javadoc 组件)对于每个Java IDE都能够轻松的从中获取到 API 源码 和文档。

一个独立的章节包含了代码覆盖测试的工具。

自动化开发人员测试和测试独立

软件测试被软件开发人员编写,用来测试他们自己的代码对于与一个成功的软件开发来说是非常重要的。这些测试总是被通过使用测试框架来编写的,例如 JUnit 或者 TestNG; JMockit 拥有同时支持这两种测试框架。

自动化的开发人员测试可以被分为两种类别:

  1. 单元测试,被用来测试相对于独立其他系统的一个类或者是一个组件。
  2. 集成测试,被用来测试系统操作用由一个单元和它的依赖完成(其他的类/组件将会与这些测试进行交互)。

即使集成测试还包括了与多个单元的交互,特殊的测试可能不关心测试所有的组件,层或者是其他子系统。这种独立测试代码排除不相关部分的能力是非常有用的。

使用模拟对象进行测试

一个通用的和强大的技术用来测试代码在一个独立的环境的被称为模拟。传统上来说一个模拟对象是一个单独的定义和实现针对单个测试或者相关测试集合的类实例。这个实例在测试的时候被注入到依赖它的代码中。每一个模拟对象的行为应该在测试代码或者是测试代码使用的保持一致,因此所有的测试用例都能够通过。然后这不仅仅是模拟对象的唯一功能。在每一个测试结束断言中,模拟对象还可以添加附加的断言。

在传统的模拟对象工具例如EasyMock, jMock, and Mockito (在下一个章节将会详细介绍)以上的描述总是成立的。JMockit不仅仅包括这些传统的用法,还包括允许模拟对象的方法和构造函数,在"实际" (非模拟) 的类中, 用来减少实例化模拟对象在测试和测试环境下传递他们。此外,通过在测试环境下新建的对象将会被执行按照测试定义的模拟的行为,当方法和构造函数被在实际类中进行调用的时候。使用 JMockit, 原始的已经存在方法或构造函数的实现将会被临时的模拟进行替换,通常用来作为单个测试用例的时候。这种模拟方式相当的通用,因此不仅仅是公开的实例方法,而且包括 final 和 static 方法, 包括构造函数都可以被他们的实现进行替换,因此这些被称为可以模拟。

模拟对于单元测试来说是相当的有用的,但是可以被用来做集成测试。例如你可能需要测试一个展现层对象,在和他与其他同层的类进行交互的时候,不需要实际依赖于调用到其他系统层的代码,例如业务逻辑或者是基础架构层。

使用模拟对象进行测试的工具

可以用来作为模拟对象的测试工具包括 EasyMock 和 jMock, 他们都是基于java.lang.reflect.Proxy, 通过在运行时给定一个对象接口生成一个实现。它还可以通过针对具体的类通过 基于CGLIB的子类生成。 每一个工具都包含许多 API 来表达在被调用的方法或者死结尾的预期和校验。很少看到在 JUnit tests中使用像EasyMock/jMock的预期那样的校验代码,往往是使用 JUnit自己的断言方法。

JMockit 有自己定义的期望 API, 和其他的 APIs类似,不过远甚于它们提供了模拟所有类型的方法,构造函数和类型的能力(接口,抽象类,最终的或非最终类,从JRE中的类,枚举等)。

有其他组的伪装工具工具,依赖于显示的期望验证而不是隐式的验证: Mockito 和 Unitils Mock。 这些伪装apis的通用特征是他们直接使用对伪装对象的调用来定义期望。 在 EasyMock 和 jMock的案例中, 这些调用只能在测试单元下进行调用, 在所谓的测试录制阶段。 在 Mockito 和 Unitils Mock的案例中, 在其他方面,调用甚至能够爱测试单元之后进行。 (录制和再重放阶段之间T,在测试单元真实执行他们所模拟的调用时。) JMockit提供验证API, 一个对期望 API的自然扩展, 能够允许显示的验证期望在验证阶段。

使用传统的模拟对象的问题

传统的使用模拟对象为了到达独立的解决方案需要强加明确的在测试代码级别的约束。JMockit被创建作为规避这种约束的替代,通过使用 java.lang.instrument Java 6+ 的包进行字节码技术 (还使用了 - 较少的程度上 - 反射 , 动态代理, 和自定义类的载入)。

针对伪装对象的主要约束是需要模拟的可选类不是要实现一个独立的接口,就是所有的需要被模拟的方法必须为可以被重载。此外,依赖的实例化必须依赖于外部的依赖单元,这样才能有一个代理对象(模拟对象)被传入代替实际上每个依赖的实现。此外代理类不能被简单的初始化使用new操作符在客户端代码中,因为构造方法的调用不能通过传统的技术进行拦截。

总结来看,有些一个传统的模拟方法在设计使用中的一些限制:

  1. 应用类必须实现一个独立接口 (为了使用 java.lang.reflect.Proxy) 或者是不被什么 final (为了能够动态生成一个包含重写方法的子类的)。在第二个情况下,需要被模拟的实例方法不能为最终的。 显然,建立java接口只用来模拟的实现是不需要的。分离的接口(或者是更普通的抽象)只有当在生成代码中有多个实现的时候才被建立。 在 Java, 标记类和方法为final是可选的,即使大多数的类不是被设计为扩展子类用的。申明他们为最终类是社区或者是通常的java编码实践中所推荐的(像书中 "Effective Java, 2nd edition", 和 "Practical API Design")。 此外,它允许静态的分析工具 (例如Checkstyle, PMD, FindBugs, 或者 你的 Java IDE) 提供了有用的对代码的警告 (例如,关于一个最终方法申明了将抛出一个特殊的需要检查的异常,但是实际上并没有抛出; 这种警告不会出现在一个给定为非最终的方法中,由于它的子类可能重写它,可能抛出异常)。
  2. 非静态方法的模拟行为有时需要被调用。 在实际中,许多的 APIs 包括静态方法最为入口或者是一个工厂方法。为了能够偶尔模拟他们是一个很实际难以避免的开销,比如建立包装类而这些类是不存在的。
  3. 需要被测试的类需要提供一些方法注入依赖的模拟实例。这通常意味着需要附加的 setter 方法 或者是 构造方法被建立在依赖的类中。 通常一个依赖的类不能够简单的通过使用new操作获取到他们的依赖实例,甚至在自然条件上做这些。依赖注入是一个技术名称意味着从使用中分离出配置, 为了使用对承认的多个实现进行依赖,有其中的一个被选中通过一些配置的代码。 很不幸的是,一些开发人员选了这种技术,建立了多个独立的接口包含各自独立的实现,或者是包含了一个较多数量的不必要的配置代码。过分使用依赖注入框架或者是容器是相当的危险的,当使用无状态的单例对象代替了适当的有状态的短生命周期的对象。 使用 JMockit, 任何设计都将被在独立的环境下测试,不会限制开发人员的自由。 对于可测试使使用传统的伪装的消极影响的设计是无关紧要的,当使用了新的方式。 结果,可测试变成无关紧要的在应用涉及中,允许开发人员避免这种分隔接口,工厂,依赖注入等的复杂性,尽管他们对系统的需求是多么无理。

一个例子

在我们自己讨论每一个模拟 API的方法在如下章节的时候,让我们来看一个快速的例子。 考虑到一个业务服务类提供了包括如下步骤的业务操作:

  1. 找到需要被操作所需要的持久化的实体
  2. 持久化一个新的实体状态、
  3. 发送一个提醒消息给一个有兴趣的部门

第一二步需要使用应用数据库,其通过使用简单的 API 来使用持久化系统。 第三步骤可以通过使用一个第三方的API来发送邮件,例子中是使用 Apache的 通用邮件库。

因此,这个服务类包含了两个独立的依赖,一个是持久化,另外一个是邮件。为了进行业务操作的单元测试来验证恰当的和这些依赖进行合作,我们使用了模拟API。对于整个工作解决方案的完整源码 - 包含所有的测试 - 可以在线查看。

package jmockit.tutorial.domain;

import java.math.*;
import java.util.*;
import org.apache.commons.mail.*;
import static jmockit.tutorial.persistence.Database.*;

public final class MyBusinessService
{
   private final EntityX data;

   public MyBusinessService(EntityX data) { this.data = data; }

   public void doBusinessOperationXyz() throws EmailException
   {
      List<EntityX> items =
(1)      find("select item from EntityX item where item.someProperty = ?1", data.getSomeProperty());

      // Compute or obtain from another service a total value for the new persistent entity:
      BigDecimal total = ...
      data.setTotal(total);

(2)   persist(data);

      sendNotificationEmail(items);
   }

   private void sendNotificationEmail(List<EntityX> items) throws EmailException
   {
      Email email = new SimpleEmail();
      email.setSubject("Notification about processing of ...");
(3)   email.addTo(data.getCustomerEmail());

      // Other e-mail parameters, such as the host name of the mail server, have defaults defined
      // through external configuration.

      String message = buildNotificationMessage(items);
      email.setMsg(message);

(4)   email.send();
   }

   private String buildNotificationMessage(List<EntityX> items) { ... }
}

数据库类包括了一些静态方法和一个私有的构造函数;查找和持久化的方法是必须的,因此我们这里就没有罗列出来 (假设他们是通过 ORM API来实现的, 比如 JPA)。

因此,我们改如何不需要任何修改已经存在应用代码的情况下进行 "doBusinessOperationXyz" 方法的单元测试呢?JMockit 提供了两种不同的模拟APIs, 每一种都能满足你的需求。我们将看到每种的写法的不同例子。 在每一个情况下, 一个 JUnit测试例子将会验证单元测试对外部依赖所兴趣的调用。这些调用都是通过点 (1)-(4) 如上进行标注。

使用期望 API

首先来看看期望 API。

package jmockit.tutorial.domain;

import org.apache.commons.mail.*;
import jmockit.tutorial.persistence.*;

import org.junit.*;
import mockit.*;

public final class MyBusinessService_ExpectationsAPI_Test
{
    @Mocked(stubOutClassInitialization = true) final Database unused = null;
    @Mocked SimpleEmail anyEmail;

    @Test
    public void doBusinessOperationXyz() throws Exception
    {
      final EntityX data = new EntityX(5, "abc", "[email protected]");
      final EntityX existingItem = new EntityX(1, "AX5", "[email protected]");

      new Expectations() {{
           Database.find(withSubstring("select"), any); (1)
         result = existingItem; // automatically wrapped in a list of one item
      }};

      new MyBusinessService(data).doBusinessOperationXyz();

        new Verifications() {{ Database.persist(data); }};(2)
        new Verifications() {{ anyEmail.send(); times = 1; }};(4)
    }

    @Test(expected = EmailException.class)
    public void doBusinessOperationXyzWithInvalidEmailAddress() throws Exception
    {
      new Expectations() {{
            anyEmail.addTo((String) withNotNull()); result = new EmailException(); (3)
      }};

      EntityX data = new EntityX(5, "abc", "[email protected]");
      new MyBusinessService(data).doBusinessOperationXyz();
    }
}

在行为导向的模拟 API像 JMockit 期望那样, 每一个测试都可以被分为三个连续的步骤: 录制,重放,验证。 在期望的录制块中定义录制, 在期望验证块中进行验证; 重放指的是在测试的内部代码中进行调用。注意,只有模拟方法的调用次数; 隶属于类或者实例中的方法 (或 构造方法)和相关的模拟属性或者模拟参数将不被模拟,因此不能被录制,更不能进行验证或者是重放了。

正如上面的实例测试,录制和验证期望可以通过调用所需要方法在一个录制或者是验证的块中来实现 (包括构造方法,及时没有在这里显示)。 这些方法的参数匹配可以通过 API的属性例如 "any" 和 "anyString", 或者是通过 API 方法 比如 "withNotNull()"。 在重放中所匹配的调用返回值 (或 抛出的异常)可以在录制阶段通过result属性进行定义。调用次数的约束可以被定义,不仅仅可以在录制阶段也可以在验证阶段, 通过 API 属性赋值比如 "times = 1"。

使用 Mockups API

接下来,让我们看使用 Mockups API 进行编写的例子。

package jmockit.tutorial.domain;

import java.util.*;
import org.apache.commons.mail.*;
import jmockit.tutorial.persistence.*;

import static org.junit.Assert.*;
import org.junit.*;
import mockit.*;

public final class MyBusinessService_MockupsAPI_Test
{
    public static final class MockDatabase extends MockUp<Database>
    {
      @Mock
      public void $clinit() { /* do nothing */ }

      @Mock(invocations = 1)
        public List<EntityX> find(String ql, Object... args)(1)
      {
         assertNotNull(ql);
         assertTrue(args.length > 0);
         return Arrays.asList(new EntityX(1, "AX5", "[email protected]"));
      }

      @Mock(maxInvocations = 1)
      public void persist(Object o) { assertNotNull(o); }(2)
    }

    @BeforeClass
    public static void mockUpPersistenceFacade()
    {
      // Applies the mock class by invoking its constructor:
      new MockDatabase();
    }

    final EntityX data = new EntityX(5, "abc", "5453-1");

    @Test
    public void doBusinessOperationXyz() throws Exception
    {
      // Defines and applies a mock class in one operation:
      new MockUp<Email>() {
         @Mock(invocations = 1)
         Email addTo(Invocation inv, String email)
         {
            assertEquals(data.getCustomerEmail(), email);
            return inv.getInvokedInstance();
         }

         @Mock(invocations = 1)
         String send() { return ""; }(4)
      };

      new MyBusinessService(data).doBusinessOperationXyz();
    }

    @Test(expected = EmailException.class)
    public void doBusinessOperationXyzWithInvalidEmailAddress() throws Exception
    {
      new MockUp<Email>() {
         @Mock
         Email addTo(String email) throws EmailException (3)
         {
            assertNotNull(email);
            throw new EmailException();
         }
      };

      new MyBusinessService(data).doBusinessOperationXyz();
    }
}

在这里,我们不采用对模拟类或实例的调用进行录制或者是调用,我们直接伪装了所感兴趣的方法和构造方法。

通常来说,大多数的测试都可以通过使用期望 API进行编写。 然而也有一些情况下,伪装API对于完成这些工作也是有用的。

使用 JMockit 运行测试用例

为了运行使用 JMockit APIs的测试, 可以使用你的 Java IDE, Ant/Maven 脚步, 等等,或者是你通常使用的方式。 原则上,任何 JDK 1.6 或者是更新的, 在 Windows, Mac OS X, 或者 Linux, 都能被使用。 JMockit 支持 (需要) 使用 JUnit 或 TestNG; 每一个测试框架的详细定义如下所示:

  • 针对 JUnit 4.5+ 测试套件, 确保 在classpath上 jmockit.jar 出现在 JUnit jar之前。此外,使用 @RunWith(JMockit.class)所注释的测试用例。 (关于 Eclipse 用户请注意: 当定义 jars的顺序在 classpath的时候, 确保使用的是 "Order and Export" 标签 在 "Java Build Path" 窗口中。 同样的, 确保 Eclipse 项目使用的JRE是从一个JDK中安装的而不是一个纯的 JRE, 因为后者缺少附加本地库的功能。)
  • 针对 TestNG 6.2+ 测试套件, 简单的添加 jmockit.jar 到 classpath下 (在任何位置)。 在其他的情况下 (像运行其他 JDK 的实现而不是 Oracle JDK), 你可能需要添加 "-javaagent:/jmockit.jar" 作为JVM初始化的参数。 这些可以通过在"Run/Debug Configuration"菜单 在 Eclipse 和 IntelliJ IDEA中, 或者 使用构建工具例如Ant 和 Maven进行。

使用 JUnit Ant 任务运行测试用例

当在build.xml脚步中使用 元素时, 使用一个独立的JVM实例是重要的。 例如,如下的一些内容:

<junit fork="yes" forkmode="once" dir="directoryContainingJars">
   <classpath path="jmockit.jar"/>

   <!-- To generate (if desired) a code coverage HTML report: -->
   <classpath path="jmockit-coverage.jar"/>

   <!-- Additional classpath entries, including the appropriate junit.jar -->

   <batchtest>
      <!-- filesets specifying the desired test classes -->
   </batchtest>
</junit>

使用 Maven 运行测试用例

JMockit安装包已经部署到了 Maven的中心仓库中。 为了在测试套件中使用它们, 添加如下的内容到你的 pom.xml 文件中:

<properties>
   <jmockit.version>desired version</jmockit.version>
</properties>

<dependencies>
   <dependency>
      <groupId>org.jmockit</groupId>
      <artifactId>jmockit</artifactId>
      <version>${jmockit.version}</version>
      <scope>test</scope>
   </dependency>
</dependencies>

确保所定义的版本是你所需要的。(当然这个所用的属性是可选的。)当使用JUnit, 这些依赖不要在"junit" 依赖之前。

针对更多的 JMockit Coverage在maven,可以看相关在那章中的内容。