模拟Java枚举以添加一个值以测试失败情况

|| 我有一个或多或少像这样的枚举开关:
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);
}
并且我希望测试涵盖所有行,但是由于期望代码能够处理所有可能性,因此如果没有在开关中使用相应的case语句,则无法提供值。 扩展枚举以添加额外的值是不可能的,并且仅模拟equals方法以返回ѭ1也不可行,因为生成的字节码使用幕后的跳转表进行适当处理...所以我我曾想过,用PowerMock或其他方法可以实现一些黑魔法。 谢谢! 编辑: 当我拥有枚举时,我以为我可以在值上添加一个方法,从而完全避免切换问题。但我还是要提这个问题,因为它仍然很有趣。     
已邀请:
这是一个完整的例子。 该代码几乎类似于您的原始代码(只是简化了更好的测试验证):
public enum MyEnum {A, B}

public class Bar {

    public int foo(MyEnum value) {
        switch (value) {
            case A: return 1;
            case B: return 2;
        }
        throw new IllegalArgumentException(\"Do not know how to handle \" + value);
    }
}
这是具有完整代码覆盖率的单元测试,该测试适用于Powermock(1.4.10),Mockito(1.8.5)和JUnit(4.8.2):
@RunWith(PowerMockRunner.class)
public class BarTest {

    private Bar bar;

    @Before
    public void createBar() {
        bar = new Bar();
    }

    @Test(expected = IllegalArgumentException.class)
    @PrepareForTest(MyEnum.class)
    public void unknownValueShouldThrowException() throws Exception {
        MyEnum C = PowerMockito.mock(MyEnum.class);
        Whitebox.setInternalState(C, \"name\", \"C\");
        Whitebox.setInternalState(C, \"ordinal\", 2);

        PowerMockito.mockStatic(MyEnum.class);
        PowerMockito.when(MyEnum.values()).thenReturn(new MyEnum[]{MyEnum.A, MyEnum.B, C});

        bar.foo(C);
    }

    @Test
    public void AShouldReturn1() {
        assertEquals(1, bar.foo(MyEnum.A));
    }

    @Test
    public void BShouldReturn2() {
        assertEquals(2, bar.foo(MyEnum.B));
    }
}
结果:
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.628 sec
    
与其使用一些彻底的字节码操作来使测试能够到达
foo
中的最后一行,我将其删除并依赖于静态代码分析。例如,IntelliJ IDEA具有\“ Enum
switch
语句缺少大小写\”代码检查,如果缺少
case
,则将对
foo
方法产生警告。     
正如您在编辑中指出的那样,您可以在枚举本身中添加功能。但是,这可能不是最佳选择,因为它可能违反“一个职责”原则。实现此目的的另一种方法是创建一个包含枚举值作为键和功能作为值的静态映射。这样,您可以通过遍历所有值来轻松测试任何枚举值是否具有有效行为。在这个例子中可能有点牵强,但这是我经常使用的一种将资源ID映射到枚举值的技术。     
jMock(至少在我正在使用的2.5.1版本中)可以立即使用。您将需要将Mockery设置为使用ClassImposterizer。
Mockery mockery = new Mockery();
mockery.setImposterizer(ClassImposterizer.INSTANCE);
MyEnum unexpectedValue = mockery.mock(MyEnum.class);
    
仅创建伪枚举值是不够的,您最终还需要处理由编译器创建的整数数组。 实际上,要创建一个虚假的枚举值,您甚至不需要任何模拟框架。您可以只使用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[]
    }
}
或者您可以再次使用普通的旧反射来操作the16ѭ字段,例如:
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
表达式,但使用某些
if
或类似表达式,则仅第一部分或第一部分和第二部分就足够了。 但是,如果要处理
switch
表达式,则e。 G。想要对21例进行100%的覆盖,在枚举像您的示例那样扩展的情况下引发异常,事情会变得更加复杂,同时也更加容易。 稍微复杂一点,因为您需要认真思考一下以操作编译器在编译器生成的合成匿名innerner类中生成的合成字段,因此您所做的工作并不十分明显,并且您必须绑定到实际实现中编译器,因此这可能会在任何Java版本中随时中断,即使您对同一Java版本使用不同的编译器也是如此。实际上,Java 6和Java 8之间已经有所不同。 稍微容易一点,因为您可以忘记此答案的前两个部分,因为根本不需要创建新的枚举实例,只需要操纵
int[]
,就必须操纵ѭ22测试你想要的。 我最近在https://www.javaspecialists.eu/archive/Issue161.html上找到了一篇很好的文章。 那里的大多数信息仍然有效,只是现在包含开关映射的内部类不再是命名的内部类,而是匿名类,因此您不能再使用
getDeclaredClasses
,而需要使用下面显示的其他方法。 基本上总结起来,在字节码级别启用开关不适用于枚举,而仅适用于整数。因此,编译器要做的是,它创建一个匿名内部类(以前按本文撰写,是一个命名内部类,这是Java 6 vs. Java 8),其中包含一个静态最终
int[]
字段,称为
$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
将具有在第一步中创建的value29ѭ值,或者将
ordinal
设置为大于编译器生成的数组的某个值,则将得到get30ѭ,否则将得到其他开关情况值之一,在两种情况下,这都无助于测试通缉的“ 21”案。 现在,您可以获取此
int[]
字段并进行修复,以包含
NON_EXISTENT
枚举实例的序数的映射。但是正如我之前说的,对于这个用例,测试“ 21”例,您根本不需要前两个步骤。相反,您可以简单地将任何现有的枚举实例提供给被测代码,并只需操作映射“ 22”,就可以触发“ 21”的情况。 因此,对于这个测试用例,所有必要的实际上就是这个,再次用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()
的返回值,但这将再次导致不同的开关分支或AIOOBE。 我刚刚显示的这段代码的作用是: 它遍历包含switch表达式的类内的匿名类 在那些搜索带有开关图的字段中 如果未找到该字段,则尝试下一个类 如果
Class.forName
抛出
ClassNotFoundException
,则测试将失败,这是有意的,因为这意味着您使用遵循不同策略或命名模式的编译器编译了代码,因此您需要添加更多的知识以涵盖用于以下方面的不同编译器策略:开启枚举值。因为如果找到带有该字段的类,则ѭ42会离开for循环,并且测试可以继续。当然,整个策略取决于匿名类从1开始编号且没有空格,但是我希望这是一个非常安全的假设。如果您不是在使用编译器,则需要相应地修改搜索算法。 如果找到switch映射字段,则会创建一个相同大小的新int数组 新数组填充为
Integer.MAX_VALUE
,只要您没有包含2,147,483,647值的枚举,通常应触发
default
情况 新数组将分配给switch映射字段 使用loop42保留for循环 现在可以完成实际测试,触发要评估的开关表达式 最后(如果不使用Spock,则在
finally
块中;如果使用Spock则在
cleanup
块中)以确保这不会影响同一类的其他测试,将原始开关图放回开关图字段中     
首先,Mockito可以创建模拟数据,该数据可以是整数长等 它无法创建正确的枚举,因为枚举具有特定数量的序数名称 值等,如果我有一个枚举
public enum HttpMethod {
      GET, POST, PUT, DELETE, HEAD, PATCH;
}
所以我在枚举HttpMethod中总共有5个序数,但是mockito不知道它.Mockito始终创建模拟数据及其null,您最终将传递null值。 因此,这里提出了一种解决方案,您可以将序数随机化并获得正确的枚举,该枚举可以传递给其他测试
import static org.mockito.Mockito.mock;

import java.util.Random;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Matchers;
import org.mockito.internal.util.reflection.Whitebox;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import com.amazonaws.HttpMethod;




//@Test(expected = {\"LoadableBuilderTestGroup\"})
//@RunWith(PowerMockRunner.class)
public class testjava {
   // private static final Class HttpMethod.getClass() = null;
    private HttpMethod mockEnumerable;

    @Test
    public void setUpallpossible_value_of_enum () {
        for ( int i=0 ;i<10;i++){
            String name;
            mockEnumerable=    Matchers.any(HttpMethod.class);
            if(mockEnumerable!= null){
                System.out.println(mockEnumerable.ordinal());
                System.out.println(mockEnumerable.name());

                System.out.println(mockEnumerable.name()+\"mocking suceess\");
            }
            else {
                //Randomize all possible  value of  enum 
                Random rand = new Random();
                int ordinal = rand.nextInt(HttpMethod.values().length); 
                // 0-9. mockEnumerable=
                mockEnumerable= HttpMethod.values()[ordinal];
                System.out.println(mockEnumerable.ordinal());
                System.out.println(mockEnumerable.name());
            }
        }
    }







    @Test
    public void setUpallpossible_value_of_enumwithintany () {
        for ( int i=0 ;i<10;i++){
            String name;
            mockEnumerable=    Matchers.any(HttpMethod.class);
            if(mockEnumerable!= null){
                System.out.println(mockEnumerable.ordinal());
                System.out.println(mockEnumerable.name());

                System.out.println(mockEnumerable.name()+\"mocking suceess\");
            } else {
               int ordinal;
               //Randomize all possible  value of  enum 
               Random rand = new Random();
               int imatch =  Matchers.anyInt();
               if(  imatch>HttpMethod.values().length)
                 ordinal = 0    ;
               else
                ordinal = rand.nextInt(HttpMethod.values().length);

               // 0-9.  mockEnumerable=
               mockEnumerable= HttpMethod.values()[ordinal];
               System.out.println(mockEnumerable.ordinal());
               System.out.println(mockEnumerable.name());       
            }
       }  
    }
}
输出:
0
GET
0
GET
5
PATCH
5
PATCH
4
HEAD
5
PATCH
3
DELETE
0
GET
4
HEAD
2
PUT
    
我认为达​​到IllegalArgumentException的最简单方法是将null传递给foo方法,您将读到“不知道如何处理null”。     
我将默认情况与枚举情况之一:
  public static enum MyEnum {A, B}

  public int foo(MyEnum value) {
    if (value == null) throw new IllegalArgumentException(\"Do not know how to handle \" + value);

    switch(value) {
        case(A):
           return calculateSomething();
        case(B):
        default:
           return calculateSomethingElse();
    }
  }
    

要回复问题请先登录注册