仅仅创建一个假的枚举值可能还不够,你最终还需要操作一个由编译器创建的整数数组。
实际上要创建一个假枚举值,您甚至不需要任何模拟框架。您可以使用 Objenesis 创建枚举类的新实例(是的,这可行),然后使用普通的旧 Java 反射来设置私有字段 name 和 ordinal,并且您已经有了新的枚举实例。
使用 Spock 框架进行测试,如下所示:
given:
def getPrivateFinalFieldForSetting = { clazz, fieldName ->
def result = clazz.getDeclaredField(fieldName)
result.accessible = true
def modifiers = Field.getDeclaredFields0(false).find { it.name == 'modifiers' }
modifiers.accessible = true
modifiers.setInt(result, result.modifiers & ~FINAL)
result
}
and:
def originalEnumValues = MyEnum.values()
MyEnum NON_EXISTENT = ObjenesisHelper.newInstance(MyEnumy)
getPrivateFinalFieldForSetting.curry(Enum).with {
it('name').set(NON_EXISTENT, "NON_EXISTENT")
it('ordinal').setInt(NON_EXISTENT, originalEnumValues.size())
}
如果您还希望 MyEnum.values() 方法返回新枚举,您现在可以使用 JMockit 模拟 values() 调用,如
new MockUp<MyEnum>() {
@Mock
MyEnum[] values() {
[*originalEnumValues, NON_EXISTENT] as MyEnum[]
}
}
或者您可以再次使用普通的旧反射来操作 $VALUES 字段,例如:
given:
getPrivateFinalFieldForSetting.curry(MyEnum).with {
it('$VALUES').set(null, [*originalEnumValues, NON_EXISTENT] as MyEnum[])
}
expect:
true // your test here
cleanup:
getPrivateFinalFieldForSetting.curry(MyEnum).with {
it('$VALUES').set(null, originalEnumValues)
}
只要您不处理 switch 表达式,而是处理一些 ifs 或类似的表达式,那么仅第一部分或第一和第二部分对您来说可能就足够了。
但是,如果您正在处理 switch 表达式,例如。 G。想要 100% 覆盖 default 的情况,如果枚举扩展会引发异常,事情会变得更复杂一些,同时也更容易一些。
有点复杂,因为您需要进行一些认真的反射来操作编译器在编译器生成的合成匿名内部类中生成的合成字段,因此您在做什么并不明显并且您一定会编译器的实际实现,因此这可能会在任何 Java 版本中随时中断,或者即使您对相同的 Java 版本使用不同的编译器。 Java 6 和 Java 8 实际上已经不同了。
简单一点,因为你可以忘记这个答案的前两部分,因为你根本不需要创建一个新的枚举实例,你只需要操作一个int[],你需要操作无论如何做你想要的测试。
我最近在https://www.javaspecialists.eu/archive/Issue161.html 发现了一篇非常好的文章。
那里的大部分信息仍然有效,只是现在包含开关映射的内部类不再是命名内部类,而是匿名类,因此您不能再使用getDeclaredClasses,而是需要使用不同的方法如下所示。
基本上总结,字节码级别的开关不适用于枚举,而仅适用于整数。所以编译器所做的是,它创建一个匿名内部类(根据文章写作,以前是一个命名的内部类,这是 Java 6 与 Java 8),它包含一个名为 $SwitchMap$net$kautler$MyEnum 的静态最终 int[] 字段,该字段已填充在 MyEnum#ordinal() 值的索引处使用整数 1、2、3、...。
这意味着当代码到达实际的开关时,它确实
switch(<anonymous class here>.$SwitchMap$net$kautler$MyEnum[myEnumVariable.ordinal()]) {
case 1: break;
case 2: break;
default: throw new AssertionError("Missing switch case for: " + myEnumVariable);
}
如果现在myEnumVariable 将具有在上述第一步中创建的值NON_EXISTENT,如果您将ordinal 设置为大于编译器生成的数组的某个值,您将获得ArrayIndexOutOfBoundsException,或者您将如果没有,则获取其他 switch-case 值之一,在这两种情况下,这都无助于测试所需的 default 案例。
您现在可以获取此 int[] 字段并将其修复为包含 NON_EXISTENT 枚举实例的 orinal 的映射。但正如我之前所说,对于这个用例,测试default 案例,您根本不需要前两个步骤。相反,您可以简单地将任何现有枚举实例提供给被测代码,并简单地操作映射int[],从而触发default 案例。
所以这个测试用例所需要的实际上就是这个,再次用 Spock (Groovy) 代码编写,但您也可以轻松地将其改编为 Java:
given:
def getPrivateFinalFieldForSetting = { clazz, fieldName ->
def result = clazz.getDeclaredField(fieldName)
result.accessible = true
def modifiers = Field.getDeclaredFields0(false).find { it.name == 'modifiers' }
modifiers.accessible = true
modifiers.setInt(result, result.modifiers & ~FINAL)
result
}
and:
def switchMapField
def originalSwitchMap
def namePrefix = ClassThatContainsTheSwitchExpression.name
def classLoader = ClassThatContainsTheSwitchExpression.classLoader
for (int i = 1; ; i++) {
def clazz = classLoader.loadClass("$namePrefix\$$i")
try {
switchMapField = getPrivateFinalFieldForSetting(clazz, '$SwitchMap$net$kautler$MyEnum')
if (switchMapField) {
originalSwitchMap = switchMapField.get(null)
def switchMap = new int[originalSwitchMap.size()]
Arrays.fill(switchMap, Integer.MAX_VALUE)
switchMapField.set(null, switchMap)
break
}
} catch (NoSuchFieldException ignore) {
// try next class
}
}
when:
testee.triggerSwitchExpression()
then:
AssertionError ae = thrown()
ae.message == "Unhandled switch case for enum value 'MY_ENUM_VALUE'"
cleanup:
switchMapField.set(null, originalSwitchMap)
在这种情况下,您根本不需要任何模拟框架。实际上它无论如何都不会帮助你,因为我知道没有任何模拟框架允许你模拟数组访问。您可以使用 JMockit 或任何模拟框架来模拟 ordinal() 的返回值,但这会再次导致不同的 switch-branch 或 AIOOBE。
我刚刚展示的这段代码的作用是:
- 它循环遍历包含 switch 表达式的类中的匿名类
- 在搜索字段中使用 switch 映射
- 如果没有找到该字段,则尝试下一个类
- 如果
ClassNotFoundException 被Class.forName 抛出,测试失败,这是有意的,因为这意味着您使用遵循不同策略或命名模式的编译器编译代码,因此您需要添加更多智能涵盖用于打开枚举值的不同编译器策略。因为如果找到带有该字段的类,break 就会离开 for 循环,测试可以继续。当然,整个策略取决于匿名类从 1 开始编号并且没有间隙,但我希望这是一个非常安全的假设。如果您处理的编译器并非如此,则需要相应地调整搜索算法。
- 如果找到 switch map 字段,则创建一个新的相同大小的 int 数组
- 新数组填充有
Integer.MAX_VALUE,只要您没有包含 2,147,483,647 个值的枚举,通常应该触发 default 情况
- 将新数组分配给切换映射字段
- 使用
break 留下for 循环
- 现在可以完成实际测试,触发要评估的 switch 表达式
- 最后(如果您不使用 Spock,则在
finally 块中,如果您正在使用 Spock,则在 cleanup 块中)以确保这不会影响同一类上的其他测试,放置原始切换映射回到切换地图字段