模拟 Java 枚举以添加值来测试失败情况
我有一个 enum 开关或多或少像这样:
public static enum MyEnum {A, B}
public int foo(MyEnum value) {
switch(value) {
case(A): return calculateSomething();
case(B): return calculateSomethingElse();
}
throw new IllegalArgumentException("Do not know how to handle " + value);
}
我希望测试涵盖所有行,但由于代码预计会处理所有可能性,因此我无法提供值switch 中没有相应的 case 语句。
扩展枚举来添加额外的值是不可能的,并且仅仅模拟 equals 方法返回 false 也不起作用,因为生成的字节码在幕后使用跳转表来转到正确的情况...所以我想也许可以通过 PowerMock 之类的东西来实现一些黑魔法。
谢谢!
编辑:
由于我拥有枚举,我认为我可以向值添加一个方法,从而完全避免切换问题;但我留下这个问题,因为它仍然很有趣。
I have an enum switch more or less like this:
public static enum MyEnum {A, B}
public int foo(MyEnum value) {
switch(value) {
case(A): return calculateSomething();
case(B): return calculateSomethingElse();
}
throw new IllegalArgumentException("Do not know how to handle " + value);
}
and I'd like to have all the lines covered by the tests, but as the code is expected to deal with all possibilities, I cannot supply a value without its corresponding case statement in the switch.
Extending the enum to add an extra value is not possible, and just mocking the equals method to return false
won't work either because the bytecode generated uses a jump table behind the curtains to go to the proper case... So I've thought that maybe some black magic could be achieved with PowerMock or something.
Thanks!
edit:
As I own the enumeration, I've thought that I could just add a method to the values and thus avoid the switch issue completely; but I'm leaving the question as it's still interesting.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(11)
这是一个完整的例子。
该代码几乎就像您的原始代码(只是简化了更好的测试验证):
这是具有完整代码覆盖率的单元测试,该测试适用于 Powermock (1.4.10)、Mockito (1.8.5) 和 JUnit (4.8.2) :
结果:
Here is a complete example.
The code is almost like your original (just simplified better test validation):
And here is the unit test with full code coverage, the test works with Powermock (1.4.10), Mockito (1.8.5) and JUnit (4.8.2):
Result:
如果您可以使用 Maven 作为构建系统,则可以使用更简单的方法。只需在测试类路径中使用附加常量定义相同的枚举即可。
假设您在源目录 (src/main/java) 下声明了枚举,如下所示:
现在,您在测试源目录 (src/test/java) 中声明完全相同的枚举,如下所示:
测试会看到 testclass 路径“重载”枚举,您可以使用“C”枚举常量测试您的代码。然后你应该看到你的 IllegalArgumentException 。
在 windows 下使用 maven 3.5.2、AdoptOpenJDK 11.0.3 和 IntelliJ IDEA 2019.3.1 进行测试
If you can use Maven as your build system, you can use a much simpler approach. Just define the same enum with an additional constant in your test classpath.
Let's say you have your enum declared under the sources directory (src/main/java) like this:
Now you declare the exact same enum in the test sources directory (src/test/java) like this:
The tests see the testclass path with the "overloaded" enum and you can test your code with the "C" enum constant. You should see your IllegalArgumentException then.
Tested under windows with maven 3.5.2, AdoptOpenJDK 11.0.3 and IntelliJ IDEA 2019.3.1
这是我的 Mockito 版本的 @Jonny Heggheim 的解决方案。它已经使用 Mockito 3.9.0 和 Java 11 进行了测试:
使用此方法时需要注意几点:
setup()
方法中运行代码至关重要。包含模拟枚举的 switch 语句的 JVM 类加载器。如果您想知道原因,我建议您阅读@Vampire 的答案中引用的文章。@BeforeClass
注解的静态方法中。MyEnum
保持扩展状态,直到您对MockedStatic
调用close()
。setUp()
和tearDown()
代码提取到单个测试用例中测试用例时,我强烈建议使用 Robolectric 运行程序或任何其他测试运行程序运行测试,这样可以保证每个测试用例在新启动的 JVM 中运行。通过这种方式,您可以确保包含枚举 switch 语句的所有类都由类加载器为每个测试用例新加载。Here is my Mockito only version of the solution of @Jonny Heggheim. It has been tested with Mockito 3.9.0 and Java 11:
A few words of caution when using this:
setup()
Method before any class is loaded by the JVM classloader that contains a switch statement for the mocked Enum. I recommend you read the article quoted in the answer of @Vampire if you want to know why.@BeforeClass
.tearDown()
method if can happen that the tests in the test class succeed but tests in other test classes fail when run afterwards in the same test run. This is becauseMyEnum
stays extended until you callclose()
onMockedStatic
.setUp()
andtearDown()
code into single test cases, I highly recommend running the tests with the Robolectric runner or any other test runner, that guarantees that each test case runs in a freshly started JVM. This way you can make sure, that all classes that contain switch statements for the Enum are newly loaded by the class loader for every test case.仅仅创建一个假枚举值是不够的,您最终还需要操作编译器创建的整数数组。
实际上,要创建一个假枚举值,您甚至不需要任何模拟框架。您可以使用 Objenesis 创建枚举类的新实例(是的,这有效),然后使用普通的旧 Java 反射来设置私有字段
name
和ordinal
和你已经有了新的枚举实例。使用 Spock 框架进行测试,这看起来像:
如果您还希望 MyEnum.values() 方法返回新的枚举,那么您现在可以使用 JMockit 来模拟 value( ) 像这样调用
,或者您可以再次使用普通的旧反射来操作
$VALUES
字段,例如:只要您不处理
switch
表达式,但对于一些if
或类似的情况,仅第一部分或第一和第二部分可能对您来说就足够了。但是,如果您正在处理
switch
表达式,例如希望default
情况的 100% 覆盖率,该情况会引发异常,以防枚举像您的示例中那样扩展,事情会变得稍微复杂一点,同时又简单一点。有点复杂,因为您需要进行一些认真的反射来操作编译器在编译器生成的合成匿名内部类中生成的合成字段,因此您在做什么并不明显,并且您必须遵守实际的实现编译器的错误,因此在任何 Java 版本中,即使您对同一 Java 版本使用不同的编译器,这也可能随时中断。 Java 6 和 Java 8 之间实际上已经不同了。
更简单一点,因为你可以忘记这个答案的前两部分,因为你根本不需要创建一个新的枚举实例,你只需要操作一个
int[]
,您无论如何都需要操作它来进行您想要的测试。我最近在 https://www.javaspecialists.eu/archive/ 找到了一篇关于此的非常好的文章Issue161.html。
那里的大部分信息仍然有效,只是现在包含 switch 映射的内部类不再是命名内部类,而是匿名类,因此您不能再使用
getDeclaredClasses
,而是需要使用不同的方法如下所示。基本上总结一下,字节码级别的开关不适用于枚举,而仅适用于整数。因此,编译器所做的是,它创建一个匿名内部类(以前是根据本文撰写的命名内部类,这是 Java 6 与 Java 8),该内部类包含一个静态最终
int[]
字段称为 $SwitchMap$net$kautler$MyEnum,在MyEnum#ordinal()
值的索引处填充整数 1、2、3、...。这意味着当代码到达实际开关时,它会执行
如果现在
myEnumVariable
将具有在上述第一步中创建的值NON_EXISTENT
,您将获得一个ArrayIndexOutOfBoundsException如果您将ordinal设置为大于编译器生成的数组的某个值,或者如果不是,您将获得其他switch-case值之一,在这两种情况下这都无济于事测试所需的默认情况。
您现在可以获取此
int[]
字段并对其进行修复,以包含NON_EXISTENT
枚举实例的 orinal 的映射。但正如我之前所说,对于这个用例,测试默认情况,您根本不需要前两个步骤。相反,您可以简单地将任何现有枚举实例提供给正在测试的代码,并简单地操作映射int[]
,以便触发default
情况。因此,这个测试用例所需要的实际上就是这个,同样是用 Spock (Groovy) 代码编写的,但您也可以轻松地将其改编为 Java:
在这种情况下,您根本不需要任何模拟框架。实际上,无论如何它对您都没有帮助,因为我知道没有任何模拟框架允许您模拟数组访问。您可以使用 JMockit 或任何模拟框架来模拟ordinal() 的返回值,但这又会导致不同的切换分支或AIOOBE。
我刚刚显示的这段代码的作用是:
ClassNotFoundException
由Class.forName
抛出,测试失败,这是有意的,因为这意味着您使用遵循不同策略或命名模式的编译器编译代码,因此您需要添加更多智能来涵盖用于切换枚举值的不同编译器策略。因为如果找到具有该字段的类,break
就会离开 for 循环,测试可以继续。当然,整个策略取决于匿名类从 1 开始编号并且没有间隙,但我希望这是一个非常安全的假设。如果您使用的编译器并非如此,则需要相应地调整搜索算法。Integer.MAX_VALUE
,通常应该触发default
情况,只要由于您没有包含 2,147,483,647 个值的枚举,因此break
保留 for 循环finally
块中;如果您使用 Spock,则在cleanup
块中)以确保这不会影响同一类上的其他测试,将原来的switch map放回switch map字段Just creating a fake enum value will not be enough, you eventually also need to manipulate an integer array that is created by the compiler.
Actually to create a fake enum value, you don't even need any mocking framework. You can just use Objenesis to create a new instance of the enum class (yes, this works) and then use plain old Java reflection to set the private fields
name
andordinal
and you already have your new enum instance.Using Spock framework for testing, this would look something like:
If you also want the
MyEnum.values()
method to return the new enum, you now can either use JMockit to mock thevalues()
call likeor you can again use plain old reflection to manipulate the
$VALUES
field like:As long as you don't deal with a
switch
expression, but with someif
s or similar, either just the first part or the first and second part might be enough for you.If you however are dealing with a
switch
expression, e. g. wanting 100% coverage for thedefault
case that throws an exception in case the enum gets extended like in your example, things get a bit more complicated and at the same time a bit more easy.A bit more complicated because you need to do some serious reflection to manipulate a synthetic field that the compiler generates in a synthetic anonymous innner class that the compiler generates, so it is not really obvious what you are doing and you are bound to the actual implementation of the compiler, so this could break anytime in any Java version or even if you use different compilers for the same Java version. It is actually already different between Java 6 and Java 8.
A bit more easy, because you can forget the first two parts of this answer, because you don't need to create a new enum instance at all, you just need to manipulate an
int[]
, that you need to manipulate anyway to make the test you want.I recently found a very good article regarding this at https://www.javaspecialists.eu/archive/Issue161.html.
Most of the information there is still valid, except that now the inner class containing the switch map is no longer a named inner class, but an anonymous class, so you cannot use
getDeclaredClasses
anymore but need to use a different approach shown below.Basically summarized, switch on bytecode level does not work with enums, but only with integers. So what the compiler does is, it creates an anonymous inner class (previously a named inner class as per the article writing, this is Java 6 vs. Java 8) that holds one static final
int[]
field called$SwitchMap$net$kautler$MyEnum
that is filled with integers 1, 2, 3, ... at the indices ofMyEnum#ordinal()
values.This means when the code comes to the actual switch, it does
If now
myEnumVariable
would have the valueNON_EXISTENT
created in the first step above, you would either get anArrayIndexOutOfBoundsException
if you setordinal
to some value greater than the array the compiler generated, or you would get one of the other switch-case values if not, in both cases this would not help to test the wanteddefault
case.You could now get this
int[]
field and fix it up to contain a mapping for the orinal of yourNON_EXISTENT
enum instance. But as I said earlier, for exactly this use-case, testing thedefault
case, you don't need the first two steps at all. Instead you can simple give any of the existing enum instances to the code under test and simply manipulate the mappingint[]
, so that thedefault
case is triggered.So all that is necessary for this test case is actually this, again written in Spock (Groovy) code, but you can easily adapt it to Java too:
In this case you don't need any mocking framework at all. Actually it would not help you anyway, as no mocking framework I'm aware of allows you to mock an array access. You could use JMockit or any mocking framework to mock the return value of
ordinal()
, but that would again simply result in a different switch-branch or an AIOOBE.What this code I just shown does is:
ClassNotFoundException
is thrown byClass.forName
, the test fails, which is intended, because that means that you compiled the code with a compiler that follows a different strategy or naming pattern, so you need to add some more intelligence to cover different compiler strategies for switching on enum values. Because if the class with the field is found, thebreak
leaves the for-loop and the test can continue. This whole strategy of course depends on anonymous classes being numbered starting from 1 and without gaps, but I hope this is a pretty safe assumption. If you are dealing with a compiler where this is not the case, the searching algorithm needs to be adapted accordingly.Integer.MAX_VALUE
which usually should trigger thedefault
case as long as you don't have an enum with 2,147,483,647 valuesbreak
finally
block if you are not using Spock, in acleanup
block if you are using Spock) to make sure this does not affect other tests on the same class, the original switch map is put back into the switch map field我不会使用一些激进的字节码操作来使测试能够命中 foo 中的最后一行,而是将其删除并依赖静态代码分析。例如,IntelliJ IDEA 具有“缺少 case 的 Enum
switch
语句”代码检查,如果foo
方法缺少case<,则会对
foo
方法产生警告。 /代码>。Rather than using some radical bytecode manipulation to enable a test to hit the last line in
foo
, I would remove it and rely on static code analysis instead. For example, IntelliJ IDEA has the "Enumswitch
statement that misses case" code inspection, which would produce a warning for thefoo
method if it lacked acase
.正如您在编辑中指出的,您可以在枚举本身中添加功能。然而,这可能不是最好的选择,因为它可能违反“一个责任”原则。实现此目的的另一种方法是创建一个静态映射,其中包含枚举值作为键,功能作为值。这样,您可以通过循环所有值来轻松测试对于任何枚举值是否具有有效的行为。在这个例子中可能有点牵强,但这是我经常使用的一种技术,用于将资源 ID 映射到枚举值。
As you indicated in your edit, you can add the functionaliy in the enum itself. However, this might not be the best option, since it can violate the "One Responsibility" principle. Another way to achieve this is to create a static map which contains enum values as key and the functionality as value. This way, you can easily test if for any enum value you have a valid behavior by looping over all the values. It might be a bit far fetched on this example, but this is a technique I use often to map resource ids to enum values.
jMock(至少从我使用的 2.5.1 版本开始)可以开箱即用地执行此操作。您需要将 Mockery 设置为使用 ClassImposterizer。
jMock (at least as of version 2.5.1 that I'm using) can do this out of the box. You will need to set your Mockery to use ClassImposterizer.
首先,Mockito 可以创建模拟数据,可以是整数长等
它无法创建正确的枚举,因为枚举具有特定数量的序数名称
值等,所以如果我有一个枚举
,那么我在枚举 HttpMethod 中总共有 5 个序数,但mockito 不知道它。Mockito 一直创建模拟数据及其 null,你最终将传递一个 null 值。
因此,这里提出了一个解决方案,您可以随机化序数并获得一个正确的枚举,该枚举可以传递给其他测试
输出:
First of all Mockito can create mock data which can be integer long etc
It cannot create right enum as enum has specific number of ordinal name
value etc so if i have an enum
so i have total 5 ordinal in enum HttpMethod but mockito does not know it .Mockito creates mock data and its null all the time and you will end up in passing a null value .
So here is proposed solution that you randomize the ordinal and get a right enum which can be passed for other test
Output :
我认为到达 IllegalArgumentException 的最简单方法是将 null 传递给 foo 方法,您将看到“不知道如何处理 null”
I think that the simplest way to reach the IllegalArgumentException is to pass null to the foo method and you will read "Do not know how to handle null"
我向我的枚举添加了一个 Unknown 选项,这是我在测试期间传入的。并非在所有情况下都理想,但很简单。
I added an Unknown option to my enum, which I pass in during the test. Not ideal in every case, but simple.
我会将默认情况与枚举情况之一放在一起:
I would put the default case with one of enum cases: