JMockit 使用指南

这篇具有很好参考价值的文章主要介绍了JMockit 使用指南。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


本文主要内容

  • 如何在 SpringBoot 中配置使用 JMockit
  • 如何 mock / faking 依赖的对象
  • 如何对行为 mock
  • 如何 Verification

JMockit 之所以强大,是因其使用了 javaagent 对类的字节码做了修改,在 JVM 的所有 mock 工具中,它是功能最强大的。同时注解又是最少的。


配置

在 SpringBoot 项目中使用 JMockit 隔离代码做单元测试,需要做以下配置

  1. 引入 JMockit 依赖。
<dependencies>
   <dependency>
      <groupId>org.jmockit</groupId>
      <artifactId>jmockit</artifactId>
      <version>${jmockit.version}</version>
      <scope>test</scope>
   </dependency>
</dependencies>
  1. 配置 javaagent。
<plugins>
   <plugin>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>2.22.2</version> <!-- or some other version -->
      <configuration>
         <argLine>
            -javaagent:"${settings.localRepository}"/org/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar
         </argLine>
      </configuration>
   </plugin>
</plugins>

注意 {jmockit.version} 配置在 properties 标签下

<properties> 
    <jmockit.version>1.49</jmockit.version>
    <skipTests>true</skipTests>    <!--同时加上这个,在 compile 过程中,将忽略测试代码编译-->
</properties>

模拟

模拟类型与实例

模拟提供一种将被测对象与其依赖隔离开来的机制。在单元测试时,仅需使用注解的方式将被测对象的依赖作为测试类的 field 属性或测试类中某个方法的参数引入到测试类。在执行单元测试时,被测对象的依赖将被重定向到 mock 对象中。如

class TheTestClass() {
    
    @Mocked
    private FirstDependency firstDependency;  // 作为 field 被引入到测试类
    
    // Note: the test method can not have a return value.
    @Test
    void testMethod(@Mocked SecondDenpency seconDependency) {  // 作为参数被引入到测试类
        // your test code
        ...
    }
}

使用 JMockit 模拟的对象作为方法参数在测试类使用时,该模拟对象会被自动创建并传递给当前使用的单元测试框架,如 JUnit/TestNG,因此当模拟对象作为参数时,该参数永不为 null

JMockit 共提供 3 种不同的注解用于模拟被测对象的依赖。

  • @Mocked
    该注解针对的是类型,需要特别强调的是该注解有点霸道,被 @Mocked 注解的类型的所有实例都将被 mock ,以及该类的所有父类(除 java.lang.Object 外)都会被递归 mock,所有子类也将被递归 mock,除 private 修饰的方法外,所有方法也将被 mock。如
public abstract class AbsDemoClass {
	public void handle() {}
}
@Slf4j
public class DemoClass extend AbsDemoClass {
	
	@Override
	public void handle(String args) {
		log.info("args length is {}", args.length());
	}
}

@ExtendWith(JMockitExtension.class)
class DemoClassTest {

    @Mocked
    private TargetMockClass instance1;

    @Test
    public void testPublish() {
        AbsDemoClass demo1 = new DemoClass();
        AbsDemoClass demo2 = new DemoClass();
        demo1.handle(null); // demo1 为用户 new 的对象,但是自动被 mock 对象覆盖了,不执行真实方法
        demo2.handle(null); // demo2 为用户 new 的对象,但是自动被 mock 对象覆盖了,不执行真实方法
        instance1.handle(null);
    }
}
  • @Injectable
    该注解针对的是实例,仅影响被注解的实例。其余实例不受影响。如
@ExtendWith(JMockitExtension.class)
class DemoClassTest {

    @Mocked
    private TargetMockClass instance1;

    @Test
    public void testPublish() {
        AbsDemoClass demo1 = new DemoClass();
        AbsDemoClass demo2 = new DemoClass();
        demo1.handle(null);  // 调用真实方法,抛出了异常
        demo2.handle(null); 
        instance1.handle(null);
    }
}
  • @Capturing
    该注解使用较少。主要用于子类 / 实现类的 mock。如当我们就仅知道父类或接口,但需要控制它的所有子类行为、或子类存在多个实现时,就使用 @Capturing。建本节最后一小节——模拟未实现的类。

在测试类中,被模拟的对象未使用会报错么?
答:不会


期望

期望是指一组与测试相关的特定模拟方法 / 构造函数的调用。期望可能涵盖对同一方法或构造函数的多个不同调用,但无需涵盖该类的所有方法调用,换句话说需要什么方法即对该方法建立期望即可。特定调用是否与给定期望匹配不仅取决于方法/构造函数签名,还取决于运行时,例如调用方法的实例、参数值、已匹配的调用数量。因此,可以为给定的期望指定几种类型的匹配约束。

当我们涉及一个或多个调用参数时,可以为每个参数指定一个确切的参数值。如可以为 String 参数指定值 “test string”,从而导致期望仅匹配那些在相应参数中具有此精确值的调用。我们也可以指定更宽松的约束来匹配整组不同的参数值,而不是指定精确的参数值。

如下代码片段显示了对 Dependency#someMethod(int, String) 的期望,它将使用指定的确切参数值匹配对此方法的调用。

@Test
void testMethod(@Mocked Dependency mockInstance) {
   ...
   new Expectations() {{
      ...
      // An expectation for an instance method:
      mockInstance.someMethod(1, "test"); result = "mocked";
      ...
   }};
   // 注意,这里仅是建立了一个期望(stub),并不会发生真实的调用
}

在测试类中,建立了期望的方法未使用,会报错么?
答:会,将抛出 miss invocations 异常


录制-回放-验证

任何一个单元测试用例都可以被划分为三个互相独立的阶段,每个按顺序依次执行,任意时刻仅会执行其中一个。分别为

  • 录制 record
  • 回放 replay
  • 验证 verify

在代码块中的表现形式如

@Test
void testMethod() {
   // 1. 准备,测试所需数据及依赖对象
   ...
   // 2. 回放,即调用真实的方法进行测试,通常调用的是 public方法,切记不要调用 private 方法。
   ...
   // 3. 验证,验证方法调用次数,执行结果等
   //    the test did its job.
   ...
}

录制技巧

  • 录制有返回值的方法
    当给定的方法不是 non-void 返回类型时,返回值可通过 Expectations 的 result来设定。如存在方法 DemoClass#doSomething(String args)
class DemoClass {
    
    public String doSomething(String args) {
        // process args
        // do something with args
        try {
            ...
            return "string value"
        } catch (YourException e) {
            throw new YourException("exception message");
        }
    }
}

对该方法建立期望并获得返回值可使用如下方式。

@Test
void testDoSomething(@Mocked DemoClass demoClass){

    new Expectations() {
        {
            demoClass.doSomething(anyString);   // 注意该处的参数,可以是 anyString,也可以是精确值
            result = "your expectation value";  
        }
        ...
    };
    
    // replay
}
  • 录制异常
    当需要对调用方法抛出异常测试时,可使用类似的方式,如
@Test
void testDoSomething(@Mocked DemoClass demoClass){

    new Expectations() {
        {
            demoClass.doSomething(anyString);
            result = new YourException();
        }
        ...
    };
    
    // replay
}
  • **灵活匹配方法参数的录制 **
    JMockit提供了如 anywith 等前缀对象对方法进行灵活 mock。如上述用例中,当希望传入任何字符串时均返回同一指定值,可使用 anyString,以 any 前缀开始的类型还有
    JMockit 使用指南
    当参数不是基本类型时,可使用以 with 前缀的类型来实现,如 withInstanceOf(Class<T> clazz),以 with 为前缀的类型还有
    JMockit 使用指南

指定调用计数

调用计数约束可用于建立期望时或验证结果时,Jmockit 提供了三个特殊字段用于计数约束。分别为:

  • times
  • minTimes
  • maxTimes
    需要注意的是,任何非负整数值对计数约束都有效,如果指定了 times = 0 或 maxTimes = 0,则与在重放期间发生的预期匹配的第一次调用(如果有)将导致测试失败。

验证

  • 不关心调用顺序的验证
    不关心方法调用顺序仅关心调用次数可使用时,可通过关键字 Verifications来建立验证。如
new Verifications() {
  {
      instance1.method(args...);
      times = your specified value;
  }
  {
      instance2.method(args...);
      times = your specified value;
  }
  ...
}
  • 验证调用顺序
    当需要验证方法调用顺序时,如 methodA()、在 methodB() 前被调用,则需使用 VerificationsInOrder 关键字。如
@Test
void verifyingExpectationsInOrder(@Mocked DependencyAbc abc) {
   // Somewhere inside the tested code:
   abc.aMethod();
   abc.doSomething("blah", 123);
   abc.anotherMethod(5);
   ...

   new VerificationsInOrder() {{
      // The order of these invocations must be the same as the order
      // of occurrence during replay of the matching invocations.
      abc.aMethod();
      abc.anotherMethod(anyInt);
   }};
}
  • 全验证
    有时可能需要对测试中涉及的模拟类型/实例的所有调用进行验证。在这种情况下,new FullVerifications() {...} 块将确保没有未验证的调用。如
@Test
void verifyAllInvocations(@Mocked Dependency mock) {
   // Code under test included here for easy reference:
   mock.setSomething(123);
   mock.setSomethingElse("anotherValue");
   mock.setSomething(45);
   mock.save();

   new FullVerifications() {{
      mock.setSomething(anyInt); // verifies two actual invocations
      mock.setSomethingElse(anyString);
      mock.save(); // if this verification (or any other above) is removed the test will fail
   }};
}

指定自定义结果

假设有一种场景,我们需要根据回放时接收到的参数来决定记录期望值的结果,应该要怎么做?答案是使用 Delegate()。如

@Tested CodeUnderTest cut;

@Test
void delegatingInvocationsToACustomDelegate(@Mocked DependencyAbc anyAbc) {
   new Expectations() {
       {
          anyAbc.intReturningMethod(anyInt, anyString);
          result = new Delegate() {
             int aDelegateMethod(int i, String s) {
                return i == 1 ? i : s.length();
             }
          };
       }
   };

   // Calls to "intReturningMethod(int, String)" will execute the delegate method above.
   cut.doSomething();
}

Delegate 接口是空的,仅用于告诉 JMockit 在重放时的实际调用应该委托给分配对象中的“委托”方法。该方法可以有任何名称,只要它是委托对象中唯一的非私有方法。至于委托方法的参数,要么与记录方法的参数相匹配,要么不存在。在任何情况下,委托方法都可以有一个 Invocation 类型的附加参数作为其第一个参数。在重放期间收到的 Invocation 对象将提供对被调用实例和实际调用参数以及其他功能的访问。委托方法的返回类型不必与记录的方法相同,但它应该兼容以避免以后发生 ClassCastException。

除此之外,构造方法也可以通过委托方法处理。如

@Test
void delegatingConstructorInvocations(@Mocked Collaborator anyCollaboratorInstance) {
   new Expectations() {{
      new Collaborator(anyInt);
      result = new Delegate() {
         void delegate(int i) { if (i < 1) throw new IllegalArgumentException(); }
      };
   }};

   // The first instantiation using "Collaborator(int)" will execute the delegate above.
   new Collaborator(4);
}

验证调用参数

可以通过一组特殊的 withCapture(...) 方法捕获调用参数以供以后验证。有三种不同的情况,每种都有自己特定的捕获方法:

  • 在一次调用中验证传递给模拟方法的参数:T withCapture()
@Test
void capturingArgumentsFromSingleInvocation(@Mocked Collaborator mock) {
   // Inside tested code:
   ...
   new Collaborator().doSomething(0.5, new int[2], "test");

   // Back in test code:
   new Verifications() {{
      double d;
      String s;
      mock.doSomething(d = withCapture(), null, s = withCapture());

      assertTrue(d > 0.0);
      assertTrue(s.length() > 1);
   }};
}

在多次调用中验证传递给模拟方法的参数: T withCapture(List<T>)

@Test
void capturingArgumentsFromMultipleInvocations(@Mocked Collaborator mock) {
   // Inside tested code:
   mock.doSomething(dataObject1);
   mock.doSomething(dataObject2);
   ...

   // Back in test code:
   new Verifications() {{
      List<DataObject> dataObjects = new ArrayList<>();
      mock.doSomething(withCapture(dataObjects));

      assertEquals(2, dataObjects.size());
      DataObject data1 = dataObjects.get(0);
      DataObject data2 = dataObjects.get(1);
      // Perform arbitrary assertions on data1 and data2.
   }};
}
  • 验证传递给模拟构造函数的参数:List<T> withCapture(T)
@Test
void capturingNewInstances(@Mocked Person mockedPerson) {
   // From the code under test:
   dao.create(new Person("Paul", 10));
   dao.create(new Person("Mary", 15));
   dao.create(new Person("Joe", 20));
   ...

   // Back in test code:
   new Verifications() {{
      // Captures the new instances created with a specific constructor.
      List<Person> personsInstantiated = withCapture(new Person(anyString, anyInt));

      // Now captures the instances of the same type passed to a method.
      List<Person> personsCreated = new ArrayList<>();
      dao.create(withCapture(personsCreated));

      // Finally, verifies both lists are the same.
      assertEquals(personsInstantiated, personsCreated);
   }};
}

联级模拟

存在这样的情况,联级创建一个自身对象,如

result = HttpUtil.createPost(url).addHeaders(header).setConnectionTimeout(2000).setReadTimeout(3000).body(params.toJSONString()).execute().body();

前半部分是创建一个 httpRequest 对象,后半部分是 http 执行并获取响应内容。在这种情况下,我们仅需 mock httpRequest 的 execute 方法,并返回一个 httpResponse 内容即可。


部分模拟

部分模拟有两种方式,一种是建立期望,但对于构造方法、静态方法不能使用期望方式实现,需使用
MockUp<T>(Class<T> argsClass) 的方式来模拟。


模拟未实现的类

如存在一个接口

public interface Service { 
    int doSomething(); 
}

及一个实现了该接口的实现类

final class ServiceImpl implements Service {

    @Override
    public int doSomething() { 
        return 1; 
    } 
}
public final class DemoClass {
   private final Service service1 = new ServiceImpl();
   private final Service service2 = new Service() {
       public int doSomething() { 
           return 2; 
       }
   };

   public int businessOperation() {
      return service1.doSomething() + service2.doSomething();
   }
}

如果不知道 Service 的具体实现(或 Service 是一个 abstract classs)时,要测试 businessOperation方法应当如何测试?

final class DemoClassTest {
   @Capturing Service anyService;

   @Test
   void mockingImplementationClassesFromAGivenBaseType() {
      new Expectations() {{ anyService.doSomething(); returns(3, 4); }};

      int result = new TestedUnit().businessOperation();

      assertEquals(7, result);
   }
}

其他

当被测对象中存在 @Value("${xxx}") 时,该如何 mock ?

由于当前我们单元测试并不加载任何 SpringBoot 上下文以及配置文件,因此针对这类私有属性,可以通过反射方式来赋值。如被测对象存在以下两个属性需要从配置文件获取。

@Value("${xxx.xxx.xxx}")
private String apiGatewayIdcs;

@Value("${spring.profiles.active}")
private String currentEnv;

可通过反射方式设定值,如

Field apiGatewayIdcs = SuperAndSubPermissionUtil.class.getDeclaredField("apiGatewayIdcs");
apiGatewayIdcs.setAccessible(true);
ReflectionUtils.setField(apiGatewayIdcs, superAndSubPermissionUtil, "wj");

Field currentEnv = SuperAndSubPermissionUtil.class.getDeclaredField("currentEnv");
currentEnv.setAccessible(true);
ReflectionUtils.setField(currentEnv, superAndSubPermissionUtil, "prd");

伪装

JMockit 工具包中,Faking API 支持创建假实现。通常,伪造的目标是要伪造的类中的某些些方法或某些构造函数,而大多数其他方法和构造函数保持不变。

假实现在依赖于外部组件或资源(如电子邮件或 Web 服务服务器、复杂库等)的测试中特别有用。通常,假实现将来自可重用的测试基础设施组件,而不是直接来自测试类。

用假实现替换真实实现对于使用这些依赖项的代码是完全透明的,并且可以在单个测试的范围内、单个测试类中的所有测试或整个测试运行中打开和关闭。

伪装方法及类

在 Faking API 的上下文中,假方法是假类中使用 @Mock 注释的任何方法。伪类是扩展 mockit.MockUp<T> 泛型基类的任何类,其中 T 是要伪造的类型。如伪造 javax.security.auth.login.LoginContext 类的若干方法。

public final class FakeLoginContext extends MockUp<LoginContext> {
   @Mock
   public void $init(String name, CallbackHandler callback) {
      assertEquals("test", name);
      assertNotNull(callback);
   }

   @Mock
   public void login() {}

   @Mock
   public Subject getSubject() { return null; }
}

每个 @Mock 方法必须有一个对应的“真实方法 / 构造函数”,在目标真实类中具有相同的签名。对于一个方法,签名由方法名和参数组成;对于构造函数,它只是参数,假方法具有特殊名称 “$init”
注意 :没有必要为真实类中的所有方法和构造函数使用假方法。任何此类方法或构造函数,如果在假类中不存在相应的假方法,则将简单地保持“原样”,也就是说,它不会被伪造。
应用伪装类

给定的假类必须应用于相应的真实类才能产生任何效果。这通常针对整个测试类或测试套件进行,但也可以针对单个测试进行。可以从测试类中的任何位置应用伪造:@BeforeClass 方法、@BeforeMethod / @Before / @BeforeEach 方法(TestNG / JUnit 4 / JUnit 5)或来自 @Test 方法。一旦应用了假类,所有假方法和真实类的构造函数的执行都会自动重定向到相应的假方法。如要应用上面的 FakeLoginContext 假类,我们只需实例化它:

@Test
public void applyingAFakeClass() throws Exception {
   new FakeLoginContext());

   // Inside an application class which creates a suitable CallbackHandler:
   new LoginContext("test", callbackHandler).login();

   ...
}

由于在测试方法中应用了伪造类,因此 FakeLoginContext 对 LoginContext 的伪造将仅对该特定测试有效。

当实例化 LoginContext 的构造函数调用执行时,会执行 FakeLoginContext 中对应的“$init”假方法。同样,当调用 LoginContext#login 方法时,会执行相应的 fake 方法,在这种情况下,由于该方法没有参数且返回类型为 void,因此它什么也不做。发生这些调用的假类实例是在测试的第一部分中创建的。

伪装未实现类

如上述 Service 接口,我们可以使用一下方式伪装并测试文章来源地址https://www.toymoban.com/news/detail-402793.html

@Test
public <T extends Service> void fakingImplementationClassesFromAGivenBaseType() {
   new MockUp<T>() {
      @Mock int doSomething() { return 7; }
   };

   int result = new DemoClass().businessOperation();

   assertEquals(14, result);
}

到了这里,关于JMockit 使用指南的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Sqlmap使用指南

    使用流程: 当给sqlmap一个url的时候,它会有如下流程: 1、判断可注入的参数 2、判断可用哪种SQL注入技术进行注入 3、识别出哪种数据库 4、根据用户选择,读取哪些数据 支持的模式(5种) 1、基于布尔的盲注,即可根据返回页面判断条件的真假的注入 2、基于时间的盲注,

    2024年02月08日
    浏览(42)
  • thegraph使用指南

    Ethereum mainnet Kovan Rinkeby Ropsten Goerli PoA-Core PoA-Sokol xDAI Matic Mumbai Fantom Binance Smart Chain Clover Avalanche Fuji Celo Celo-Alfajores Fuse Moonbeam Arbitrum One Arbitrum Testnet (on Rinkeby) Optimism Optimism Testnet (on Kovan) 安装yarn,node环境 设置yarn仓库 curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee

    2024年02月09日
    浏览(46)
  • Docker 网络使用指南

    目录 前言 Docker网络类型 Docker 网络概念 使用 Docker 网络 运行容器并连接到网络 检查网络 清理网络资源 Docker Compose网络 Docker 是一种容器化平台,它允许你将应用程序和它们的依赖项打包成一个称为容器的独立单元。Docker 提供了一种轻量级、可移植和自包含的方式来部署应用

    2024年02月05日
    浏览(41)
  • ZED使用指南(一)

    将相机插入USB3.0端口。 进入下载的文件夹: 使用chmod +x命令增加安装程序的执行权限,注意将名称替换为自己下载的版本: 运行: 安装开始时,会显示软件许可证Software License,按Enter看完后按q。 在安装过程中,需要回答有关依赖项dependencies,工具tools和示例samples的安装的

    2024年02月16日
    浏览(41)
  • myspl使用指南

    mysql数据库 使用命令行工具连接数据库 -u表示后面是用户名 -p表示后面是密码 -h表示后面是主机名,登录当前设备可省略。 如我们要登录本机用户名为root,密码为123456的账户: 按回车,然后再输入密码即可。 使用代码连接 显示mysql中的所有数据库(mysql中可以有很多个数据

    2024年02月10日
    浏览(38)
  • Ollydbg使用指南

    OllyDebug,简称OD,一种反汇编软件,动态追踪工具,将IDA与SoftICE结合起来的思想,Ring 3 级的调试器。OllyDebug的使用界面是可视化操作。 OD有很多民间的版本,菜单栏和和工具栏的配置可能有所差别。比较好用的是吾爱破解和吾爱汇编的中文Ollydbg版本。 深入学习OllyDBG的调试技

    2023年04月08日
    浏览(67)
  • Github的使用指南

    打开giuhub官网,右上角点击你的头像,随后点击 your repositories 点击New开始创建仓库 如下图为创建仓库的选项解释 出现如下界面就可以进行后续的git指令操作了 进入需上传项目的所在目录,打开git命令行,输入如下命令开始初始化git仓库,这将在你的项目文件夹中创建一个名

    2024年02月12日
    浏览(37)
  • CuteHttpFileSever使用指南

    浏览器访问,多端互通 局域网传输,速度很快 官方网址 我下载时比较慢,好在文件不大 个人蓝奏云网址 文件提取码:1lqd 官网下载说明 官网页面见下图 1 是windows内的安装包,根据自己电脑配置选择 2 是图形化界面的软件 以上两种选其一即可 图形化界面使用教程 双击图形

    2024年02月05日
    浏览(35)
  • Vim深入使用指南

    Vim是一款功能强大的文本编辑器,被广泛用于编写和编辑各种类型的文档和代码。 可以操作系统下载并安装Vim。在安装完成后,通过在终端中输入 vim 命令来启动Vim。 Vim有多个工作模式,每个模式有不同的功能和快捷键。以下是Vim的三个基本模式: 命令模式(Command mode) :

    2024年02月15日
    浏览(37)
  • sqlx库使用指南

    sqlx库使用指南 在项目中我们通常可能会使用database/sql连接MySQL数据库。本文借助使用sqlx实现批量插入数据的例子,介绍了sqlx中可能被你忽视了的sqlx.In和DB.NamedExec方法。 sqlx介绍 在项目中我们通常可能会使用database/sql连接MySQL数据库。sqlx可以认为是Go语言内置database/sql的超集

    2024年02月09日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包