模拟 BouncyCastle 类 - SecurityException

Mocking BouncyCastle classes - SecurityException

我正在尝试为使用 BouncyCastle 的 SignerInformation 的 class 编写单元测试 - 我想模拟它的一个实例,但尝试这样做会导致 java.lang.SecurityException。这是一个简化的工作示例:

SignerInformationConsumer.java

import org.bouncycastle.cms.SignerInformation;

public class SignerInformationConsumer {
    public String interact(SignerInformation si) {
        return si.getDigestAlgOID();
    }
}

SignerInformationConsumerTest.groovy

import org.bouncycastle.cms.SignerInformation
import spock.lang.Shared
import spock.lang.Specification

class SignerInformationConsumerTest extends Specification {

    @Shared
    SignerInformation si = Mock()

    def "should return valid array"() {
        given:
            SignerInformationConsumer test = new SignerInformationConsumer()
            si.digestAlgOID >> "aaa"
        when:
            String digest = test.interact(si)
        then:
            digest == "aaa"
    }
}

build.gradle

plugins {
    id 'java'
}

group 'test'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'org.spockframework', name: 'spock-core', version: '1.1-groovy-2.4'
    testCompile 'net.bytebuddy:byte-buddy:1.8.0'
    compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.60'
}

异常

java.lang.IllegalArgumentException: Could not create type

    at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:140)
    at net.bytebuddy.TypeCache$WithInlineExpunction.findOrInsert(TypeCache.java:346)
    at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:161)
    at net.bytebuddy.TypeCache$WithInlineExpunction.findOrInsert(TypeCache.java:355)
    at org.spockframework.mock.runtime.ProxyBasedMockFactory$ByteBuddyMockFactory.createMock(ProxyBasedMockFactory.java:108)
    at org.spockframework.mock.runtime.ProxyBasedMockFactory.create(ProxyBasedMockFactory.java:65)
    at org.spockframework.mock.runtime.JavaMockFactory.createInternal(JavaMockFactory.java:59)
    at org.spockframework.mock.runtime.JavaMockFactory.create(JavaMockFactory.java:40)
    at org.spockframework.mock.runtime.CompositeMockFactory.create(CompositeMockFactory.java:44)
    at org.spockframework.lang.SpecInternals.createMock(SpecInternals.java:51)
    at org.spockframework.lang.SpecInternals.createMockImpl(SpecInternals.java:296)
    at org.spockframework.lang.SpecInternals.createMockImpl(SpecInternals.java:286)
    at org.spockframework.lang.SpecInternals.MockImpl(SpecInternals.java:89)
    at TestTest.$spock_initializeSharedFields(TestTest.groovy:8)
Caused by: java.lang.IllegalStateException: Error invoking java.lang.ClassLoader#defineClass
    at net.bytebuddy.dynamic.loading.ClassInjector$UsingReflection$Dispatcher$Direct.defineClass(ClassInjector.java:412)
    at net.bytebuddy.dynamic.loading.ClassInjector$UsingReflection.inject(ClassInjector.java:185)
    at net.bytebuddy.dynamic.loading.ClassLoadingStrategy$Default$InjectionDispatcher.load(ClassLoadingStrategy.java:187)
    at net.bytebuddy.dynamic.loading.ClassLoadingStrategy$Default.load(ClassLoadingStrategy.java:120)
    at net.bytebuddy.dynamic.TypeResolutionStrategy$Passive.initialize(TypeResolutionStrategy.java:79)
    at net.bytebuddy.dynamic.DynamicType$Default$Unloaded.load(DynamicType.java:4457)
    at net.bytebuddy.dynamic.DynamicType$Default$Unloaded.load(DynamicType.java:4447)
    at org.spockframework.mock.runtime.ProxyBasedMockFactory$ByteBuddyMockFactory.call(ProxyBasedMockFactory.java:113)
    at org.spockframework.mock.runtime.ProxyBasedMockFactory$ByteBuddyMockFactory.call(ProxyBasedMockFactory.java:110)
    at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:138)
    ... 13 more
Caused by: java.lang.SecurityException: class "org.bouncycastle.cms.SignerInformation$SpockMock$bSXMi60o"'s signer information does not match signer information of other classes in the same package
    at java.lang.ClassLoader.checkCerts(ClassLoader.java:898)
    at java.lang.ClassLoader.preDefineClass(ClassLoader.java:668)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
    at net.bytebuddy.dynamic.loading.ClassInjector$UsingReflection$Dispatcher$Direct.defineClass(ClassInjector.java:408)
    ... 22 more

您能否就如何模拟此 class 或以不同方式测试行为提出一些解决方案?

您正在处理此处签名的 JAR。假设您不想全局禁用 Java 的安全功能,也不想创建所有 BouncyCastle JAR 的副本,删除它们的清单,我将向您展示一个解决方法。

问题在于 Spock 模拟(也包括其他模拟)是在与原始 class 相同的包中创建的。但是模拟是未签名的代码,因此是错误消息。现在你可以只 subclass 要模拟的 class 和模拟 subclass 代替。如果您在测试中的任何地方需要它们,只需确保 subclass 具有来自其父级的所有必要构造函数。

package de.scrum_master.Whosebug;

import org.bouncycastle.cms.SignerInformation;

public class SignerInformationConsumer {
  public String interact(SignerInformation si) {
    return si.getDigestAlgOID();
  }
}
package de.scrum_master.Whosebug

import org.bouncycastle.cms.SignerInformation
import spock.lang.Specification

class SignerInformationConsumerTest extends Specification {

  static class SignerInformationMock extends SignerInformation {
    protected SignerInformationMock(SignerInformation baseInfo) {
      super(baseInfo)
    }
  }

  //SignerInformation signerInformation = Spy(SignerInformationMock, useObjenesis: true)
  SignerInformation signerInformation = Mock(SignerInformationMock)

  def "should return valid array"() {
    given:
    SignerInformationConsumer signerInformationConsumer = new SignerInformationConsumer()
    signerInformation.getDigestAlgOID() >> "aaa"
    expect:
    signerInformationConsumer.interact(signerInformation) == "aaa"
  }
}

关于你的测试的几句话:

  • 不要使用 @Shared 因为测试应该相互独立。您可以将特征方法 A 的副作用转移到 B 中。共享变量应该只在非常罕见的情况下使用,例如如果您创建的对象在时间或资源方面非常昂贵。模拟绝对不是。因此,要么在您的功能方法内部创建模拟,要么,如果其他功能方法要使用相同的模拟定义,请使用不带 @Shared 的普通成员变量。当然,你可以忽略这个建议,但我仍然认为你应该遵循它。

  • 您的测试不测试应用程序,它只测试模拟。我希望您的真实测试用例看起来有所不同,因为这个测试,即使我让它为您工作,也只检查存根结果是否与测试开始时指定的一样。

  • 我的代码中的注释行向您展示了如果您出于任何原因需要(例如,您想使用真实对象并且只存根一个或一个),如何使用间谍而不是模拟几种方法)。在这种特殊情况下,您将需要 Objenesis 作为依赖项,否则您将得到一个异常,因为没有默认构造函数。或者,您必须创建间谍并包含构造函数参数。

  • 如果getDigestAlgOID()要在每个特征方法中进行相同的存根,您可以将存根部分从特征方法移动到模拟定义中,如下所示:

SignerInformation signerInformation = Mock(SignerInformationMock) {
  getDigestAlgOID() >> "aaa"
}