举例说明如何使用JMockit提供的模拟功能?

525 阅读9分钟

学习创建和注入模拟,在JUnit测试中使用JMockit库创建期望和验证。我们将从JMockit的基本概念开始,并举例说明,随后将深入探讨高级概念。

目录

  1. 1.JMockit的核心概念
  2. 2.一个简单的JMockit测试实例
  3. 3.创建和注入Mock
  4. 4.记录期望值
  5. 5.编写验证
  6. 6.结语

1.JMockit的核心概念

1.1.核心功能

JMockit是一个开源软件,包含对嘲讽、伪造和集成测试的支持,以及一个代码覆盖工具。它 用于嘲弄测试边界之外的外部依赖,类似于Mockito和其他此类嘲弄库

JMockit最重要的特点是它可以让我们模拟任何东西,甚至是其他库很难模拟的东西,如私有方法、构造函数静态最终方法。它甚至还允许对成员字段初始化块进行模拟。

1.2.测试阶段

EasyMock类似,JMockit在测试中也使用了记录-重放-验证的模式,在模拟和SUT**(被测系统**)被定义后。

  • 记录。在这一步,我们记录来自模拟对象的期望。我们定义模拟对象的行为,即要调用的方法,返回值和我们期望它被调用的次数。
  • 回放。在这一步,我们执行SUT*(被测系统*)中的实际测试代码
  • 验证:在这一步,我们验证所有的期望是否被执行

一个典型的JMockit测试将看起来像这样。

public class TestClass {

	@Tested
	private Service service;

	@Injectable
	private Dao dao;

	@Mock
	private Component component;

	@Test
	public void testSUT() {
	   // Test data initialization, if any

	   new Expectations() {{ 
	       // define expected behaviour for mocks and injectables
	   }};

	   // test service operations

	   new Verifications() {{ 
	       // verify mocks and injectables
	   }};

	   // assertions
	}	
}

1.3.声明性期望和验证

JMockit允许以一种非常详细和声明的方式来定义期望和验证。这些非常容易与测试代码的其他部分区分开来。

其他嘲讽库,一般来说,提供static 方法,如expect()andThenReturn()times() 来指定期望,verify() 来在测试执行后验证期望。

MockAPI.expect(mock.method(argumentMatcher)).andThenReturn(value).times(1);

相比之下,JMockit使用以下类来表达它们。

  • 期待值。一个期望块代表了一组对特定的模拟方法/构造器的调用,与给定的测试有关。
  • 验证。一个常规的无序块,用于检查在重放期间至少有一个匹配的调用发生。
  • 顺序验证(VerificationsInOrder)。当我们想在重放阶段测试调用的实际相对顺序时,应该使用它。
  • FullVerfications。如果我们想对测试中涉及的模拟类型/实体的所有调用进行验证。它将确保没有任何调用是未被验证的。

我们将在本教程的后面再次重温这些类。

2.一个简单的JMockit测试实例

2.1.Maven的依赖性

首先在应用程序中加入JMockit依赖项。如果还没有包括,也要添加JUnit依赖项。

<dependency>
    <groupId>org.jmockit</groupId>
    <artifactId>jmockit</artifactId>
    <version>1.49</version>
</dependency>

2.2.被测系统

为了演示JMockit语法,我们创建了一个典型的用例,RecordService调用RecordDao来保存一条记录,并向NotificationService发送通知。RecordService使用SequenceGenerator类来获取下一条记录的ID。

你可以浏览GitHub资源库中的代码,其链接在本教程的末尾。

2.3.测试演示

为了测试RecordService.saveRecord()方法,我们需要注入RecordDaoSequenceGenerator作为它的依赖项。RecordService在运行时获得NotificationService实例,所以我们可以简单地模拟它,让运行时用一个模拟来替换它。

接下来,我们将创建一些期望值,执行测试代码,最后,执行验证来结束测试。我们可以使用额外的JUnit断言来验证额外的测试结果。

public class JMockitDemoTests {

  @Injectable
  RecordDao mockDao;	// Dependency

  @Injectable
  SequenceGenerator mockGenerator; // Dependency

  @Tested
  RecordService service;	//System Under Test

  // NotificationService can be mocked in test scope
  @Test
  public void testSaveRecord(@Mocked NotificationService notificationService) {

    Record record = new Record();
    record.setName("Test Record");

    //Register Expectations
    new Expectations() {{
      mockGenerator.getNext();
      result = 100L;
      times = 1;
    }};

    new Expectations() {{
      mockDao.saveRecord(record);
      result = record;
      times = 1;
    }};

    new Expectations() {{
      notificationService.sendNotification(anyString);
      result = true;
      times = 1;
    }};


    //Test code
    Record savedRecord = service.saveRecord(record);

    // Verifications
    new Verifications() {{ // a "verification block"
      mockGenerator.getNext();
      times = 1;
    }};

    new Verifications() {{
      mockDao.saveRecord(record);
      times = 1;
    }};

    new Verifications() {{
      notificationService.sendNotification(anyString);
      times = 1;
    }};

    //Additional assertions
    assertEquals("Test Record", savedRecord.getName());
    assertEquals(100L, savedRecord.getId());
  }
}

3.创建和注入Mock

值得一提的是,JMockit允许在记录-重放-验证流程的不同阶段有不同的模拟对象。例如,我们可以对一个依赖关系有两个模拟对象,并在期望和验证中分别使用它们

与其他嘲弄API不同的是,这些被嘲弄的对象不一定是被测试的代码在调用其依赖的实例方法时使用的对象。

@Mocked Dependency mockDependency;

@Test
public void testCase(@Mocked Dependency anotherMockDependency)
{
	new Expectations() {{ 
      mockDependency.operation();
   }};

   // Call the code under test

   new Verifications() {{ 
      anotherMockDependency.operation();
   }};
}

JMockit允许以不同的方式为SUT创建和注入模拟对象。让我们来了解一下它们。

3.1.嘲讽相关注解

嘲弄依赖关系的主要注解如下。

3.1.1.@Mocked和@Capturing

当在一个字段上使用时,@Mocked将在测试执行期间为该特定类的每一个新对象创建模拟实例。在内部,它将模拟被模拟类所有实例的所有方法和构造函数。

@Mocked Dependency mockDependency;

@Capturing 与*@Mocked的行为类似,但另外,@Capturing*会模拟每个扩展或实现注释字段类型的子类

在下面的例子中,JMockit将模拟Dependency的所有实例,以及它的任何子类。如果Dependency是一个接口,那么JMockit将模拟其所有的实现类。

@Capturing Dependency mockDependency;

请注意,只用@Mocked或*@Capturing***注释的模拟字段不被考虑用于注入。

3.1.2.@Injectable和@Tested

在执行测试方法之前,@Tested 注解会触发自动实例化和注入其他模拟和可注入对象。一个实例将使用被测类的合适的构造函数被创建,同时确保其内部的*@Injectable*依赖被正确地注入(如果适用)。

与*@Mocked@Capturing相反,@Injectable*只创建一个模拟的实例。

注意,在初始化被测类时,JMockit支持两种形式的注入:即构造函数注入字段注入

在下面的例子中,dep1dep2将被注入到SUT中。

public class TestClass {

   @Tested SUT tested;

   @Injectable Dependency dep1;
   @Injectable AnotherDependency dep2;
}

3.2.测试类和方法范围的模拟

JMockit允许在类级和测试方法级创建模拟,通过传递模拟作为测试参数。方法级模拟有助于为一个测试创建一个模拟,从而有助于进一步限制测试的边界。

public class TestClass {

	//Class scoped mock
  @Mocked Dependency mock;

  //Method scoped mock
  @Test
	public void testCase(@Mocked AnotherDependency anotherMock)
	{
		//test code
	}
}

4.记录期望值

4.1.匹配方法调用

JMockit在记录期望方面非常灵活。我们可以在一个期望块中记录多个方法调用,同时,我们也可以在一个测试方法中记录多个期望块。

public TestClass {

	new Expectations() {{
		mock.method1();
		mock.method2();
		anotherMock.method3();
	}};

	new Expectations() {{
		someOtherMock.method();
	}};
}

4.2.匹配参数

在方法调用中使用准确的参数,将在重放阶段匹配准确的参数值。使用equals() 方法检查对象型参数是否相等。同样地,如果数组列表类型的参数都是相同的大小,并且包含相似的元素,那么数组和列表类型的参数会被视为等值。

对于灵活的参数匹配,我们可以使用以下两种方法中的一种。

4.2.1. any 字段

JMockit提供了一系列的任意参数匹配字段。它们支持每个原始类型(以及相应的封装类)一个,字符串一个,以及所有Objects一个。

new Expectations() {{

  mock.method1(anyInt);
  mock.method2(anyString);
  mock.method3(anyInt);
  mock.method4((List<?>) any);
  mockDao.saveRecord((Record) any);
}};

4.2.2. with 方法

我们可以从一系列这样的方法中使用一个withXYZ()方法来实现特定的用途。这些方法有withEqual(),withNotEqual(),withNull () , withNotNull(),withSubstring(),withPrefix(),withSuffix(),withMatch(regex),withSameInstance(),*withInstanceLike()withInstanceOf()*等。

new Expectations() {{

  mock.method1(withSubstring("xyz"));
  mock.method2(withSameInstance(record));
  mock.method3(withAny(1L));	//Any long value will match
  mock.method4((List<?>) withNotNull());
}};

4.3.匹配返回值

如果是非void的mock方法,我们可以在result 字段中记录返回值。对结果的赋值应该紧跟在识别记录期望的调用之后出现

new Expectations() {{
	mock.method1();
	result = value1;

	mock.method2();
	result = value2;
}};

如果我们在一个循环中调用一个方法,那么我们可以使用return(v1, v2, ...)方法或者给result字段分配一个值列表来期望多个返回值

new Expectations() {{
	mock.method();
	returns(value1, value2, value3);
}};

如果测试需要在方法被调用时抛出一个异常或错误,只需将所需的可抛出实例分配给结果

new Expectations() {{

	mock.method();
	result = new ApplicationException();
}};

4.3.匹配调用计数

JMockit提供了三个特殊的字段,只是匹配调用次数。任何小于或大于预期的下限或上限的调用,分别是,测试执行将自动失败。

  • times
  • minTimes
  • maxTimes
new Expectations() {{
	mock.method();
	result = value;
	times = 1;
}};

5.编写验证

5.1.验证

验证块中,我们可以使用与期望块中相同的步骤,除了返回值和抛出的异常。我们可以重复使用方法的调用和期望值的计算。

因此,编写验证的语法和期望值是一样的,你可以参考前面的章节。

new Verifications() {{
	mock.method();
	times = 1;
}};

5.2.顺序验证(VerificationsInOrder

如1.3节所述,这有助于在重放阶段测试调用的实际相对顺序。在这个块中,只需按照预期发生的顺序将调用写入一个或多个模拟。

@Test
public void testCase() {
	//Expectation

	//Test code
	mock.firstInvokeThis();
	mock.thenInvokeThis();
	mock.finallyInvokeThis();

	//Verification
	new VerificationsInOrder() {{
	  mock.firstInvokeThis();
	  mock.thenInvokeThis();
	  mock.finallyInvokeThis();
	}};
}

5.3.全面验证

在前面的验证模式中,JMockit验证了验证块中的所有调用必须在测试重放阶段至少执行一次。它不会抱怨那些发生在重放阶段但没有被添加到验证块中的调用。

在下面的例子中,*method3()*在测试中已经被执行,但在验证阶段没有被验证。该测试将被PASS。

@Test
public void testCase() {

	//Test code
	mock.method1();
	mock.method2();
	mock.method3();

	//Verification
	new VerificationsInOrder() {{
		mock.method1();
		mock.method2();
	}};
}

如果我们想完全控制模拟交互,我们可以使用FullVerifications。它有助于防止任何我们没有验证的方法被执行。

@Test
public void testCase() {

	//Test code
	mock.method1();
	mock.method2();
	mock.method3();

	//Verification
	new FullVerifications() {{
		mock.method1();
		mock.method2();
		mock.method3();		//If we remove this, the test will FAIL
	}};
}

6.总结

在本教程中,我们详细学习了如何使用JMockit提供的模拟功能。我们深入了解了记录-重放-验证阶段,并举例说明。

我们还学习了一些高级概念,如灵活的参数匹配和调用计数。