powermockito: How to mock abstract method in enum

2019-01-28 17:35发布

Consider the following (simplified) enumeration:

MyEnum {
    ONE public int myMethod() {
        // Some complex stuff
        return 1;
    },

    TWO public int myMethod() {
        // Some complex stuff
        return 2;
    };

    public abstract int myMethod();
}

This is used in a function like:

void consumer() {
    for (MyEnum n : MyEnum.values()) {
       n.myMethod();
    }
}

I'd now like to write a unit test for consumer that mocks out the calls to myMethod() in each of the enumeration instances. I've tried the following:

@RunWith(PowerMockRunner.class)
@PrepareForTest(MyEnum.class)
public class MyTestClass {
    @Test
    public void test() throws Exception {
        mockStatic(MyEnum.class);

        when(MyEnum.ONE.myMethod()).thenReturn(10);
        when(MyEnum.TWO.myMethod()).thenReturn(20);

        // Now call consumer()
}

But the real implementations of ONE.myMethod() and TWO.myMethod() are being called.

What have I done wrong?

3条回答
太酷不给撩
2楼-- · 2019-01-28 18:03
  1. Each constant in enum it's a static final nested class. So to mock it you have to pointe nested class in PrepareForTest.
  2. MyEnum.values() returns pre-initialised array, so it should be also mock in your case.
  3. Each Enum constant it is just public final static field.

All together:

@RunWith(PowerMockRunner.class)
@PrepareForTest(
value = MyEnum.class,
fullyQualifiedNames = {
                          "com.stackoverflow.q45414070.MyEnum$1",
                          "com.stackoverflow.q45414070.MyEnum$2"
})

public class MyTestClass {

  @Test
  public void should_return_sum_of_stubs() throws Exception {

    final MyEnum one = mock(MyEnum.ONE.getClass());
    final MyEnum two = mock(MyEnum.TWO.getClass());

    mockStatic(MyEnum.class);
    when(MyEnum.values()).thenReturn(new MyEnum[]{one, two});

    when(one.myMethod()).thenReturn(10);
    when(two.myMethod()).thenReturn(20);

    assertThat(new Consumer().consumer())
        .isEqualTo(30);
  }

  @Test
  public void should_return_stubs() {

    final MyEnum one = mock(MyEnum.ONE.getClass());

    when(one.myMethod()).thenReturn(10);

    Whitebox.setInternalState(MyEnum.class, "ONE", one);

    assertThat(MyEnum.ONE.myMethod()).isEqualTo(10);
  }

}

Full example

查看更多
Summer. ? 凉城
3楼-- · 2019-01-28 18:14

That is the crux with using enums for more than "compile time constants" - enum classes are final by default (you can't extend MyEnum). So dealing with them within unit test can be hard.

@PrepareForTest means that PowerMock will generate byte code for the annotated class. But you can't have it both ways: either the class is generated (then it doesn't contain ONE, TWO, ...) or it is "real" - and then you can't override behavior.

So your options are:

  • mock the whole class, and then see if you can somhow get values() to return a list of mocked enum class objects ( see here for the first part )
  • step back and improve your design. Example: you could create an interface that denotes myMethod() and have your enum implement that. And then you don't use values() directly - instead you introduce some kind of factory that simply returns a List<TheNewInterface> - and then the factory can return a list of mocked objects for your unit test.

I strongly recommend option 2 - as that will also improve the quality of your code base (by cutting the tight coupling to the enum class and its constants that your code currently deals with).

查看更多
Fickle 薄情
4楼-- · 2019-01-28 18:22

From what I know about PowerMock, your test should work as is. Maybe you could open an issue in the PowerMock github project?

Anyway, here is a self-contained test that does work, but using another library, JMockit:

public final class MockingAnEnumTest {
    public enum MyEnum {
        ONE { @Override public int myMethod() { return 1; } },
        TWO { @Override public int myMethod() { return 2; } };
        public abstract int myMethod();
    }

    int consumer() {
        int res = 0;

        for (MyEnum n : MyEnum.values()) {
            int i = n.myMethod();
            res += i;
        }

        return res;
    }

    @Test
    public void mocksAbstractMethodOnEnumElements() {
       new Expectations(MyEnum.class) {{
           MyEnum.ONE.myMethod(); result = 10;
           MyEnum.TWO.myMethod(); result = 20;
       }};

       int res = consumer();

       assertEquals(30, res);
   }
}

As you can see, the test is quite short and simple. However, I would recommend to not mock an enum unless you have a clear need to do so. Don't mock it just because it can be done.

查看更多
登录 后发表回答