仅创建伪枚举值是不够的,您最终还需要操作由编译器创建的整数数组。
实际上,要创建一个虚假的枚举值,您甚至不需要任何模拟框架。您可以只使用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
cleanup:
getPrivateFinalFieldForSetting.curry(MyEnum).with {
it('$VALUES').set(null, originalEnumValues)
}
只要您不处理switch
表达式,而是使用某些if
s或类似表达式,那么仅第一部分,第一部分和第二部分就足够了。
但是,如果您要处理switch
表达式,例如想要100%覆盖default
,要使枚举像您的示例那样被扩展,则抛出异常的情况情况将变得更加复杂,同时也更加容易。
稍微复杂一点,因为您需要认真思考一下以操作编译器在编译器生成的合成匿名innner类中生成的合成字段,因此您所做的工作并不十分明显,您必须绑定到实际的实现中因此,这可能会在任何Java版本中随时中断,甚至即使您对同一Java版本使用不同的编译器也是如此。实际上,Java 6和Java 8之间已经有所不同。
稍微容易一点,因为您可以忘记此答案的前两个部分,因为根本不需要创建新的枚举实例,您只需要操纵一个int[]
,无论如何都要进行操纵以进行测试想。
我最近在https://www.javaspecialists.eu/archive/Issue161.html上找到了一篇很好的文章。
那里的大多数信息仍然有效,只是现在包含开关映射的内部类不再是命名的内部类,而是匿名类,因此您不能再使用getDeclaredClasses
,而需要使用下面显示的其他方法。
基本上总结起来,在字节码级别启用开关不适用于枚举,而仅适用于整数。因此,编译器要做的是,它创建一个匿名内部类(根据本文,以前是一个命名的内部类,这是Java 6 vs. Java 8),其中包含一个静态的finalint[]
字段$SwitchMap$net$kautler$MyEnum
,该字段用整数1、2填充。 3,...在MyEnum#ordinal()
值的索引处。
这意味着当代码到达实际的开关时,
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
在上面的第一步中创建该值,则在两种情况下,ArrayIndexOutOfBoundsException
如果设置的ordinal
值大于编译器生成的数组,则将获得一个,否则,将获得其他切换条件值之一这无助于测试通缉default
案件。
现在,您可以获取此int[]
字段并对其进行修复,以包含NON_EXISTENT
枚举实例的序数的映射。但是正如我之前所说的,对于这个用例来说,测试用例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) {
}
}
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()
,但这将再次导致不同的开关分支或AIOOBE。
我刚刚显示的这段代码的作用是:
- 它遍历包含switch表达式的类内的匿名类
- 在那些搜索带有开关图的字段中
- 如果找不到该字段,则尝试下一个类
- 如果a
ClassNotFoundException
被抛出Class.forName
,则测试将失败,这是有意的,因为这意味着您使用遵循不同策略或命名模式的编译器对代码进行了编译,因此您需要添加更多的知识以涵盖用于打开的不同编译器策略。枚举值。因为如果找到带有该字段的类,则break
离开for循环,然后测试可以继续。当然,整个策略取决于匿名类从1开始编号且没有空格,但是我希望这是一个相当安全的假设。如果您不是在使用编译器,则需要相应地修改搜索算法。
- 如果找到了switch映射字段,则会创建一个相同大小的新int数组
- 新的数组充满了
Integer.MAX_VALUE
通常应该触发default
的情况下,只要你没有与2,147,483,647值的枚举
- 新的数组分配给switch映射字段
- for循环使用
break
- 现在可以完成实际测试,触发要评估的开关表达式
- 最后(
finally
如果不使用Spock,则在一个cleanup
块中;如果您使用Spock,则在一个块中),以确保这不会影响同一类上的其他测试,将原始切换图放回切换图字段中