2

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?

GhostCat
  • 137,827
  • 25
  • 176
  • 248
Stormcloud
  • 2,065
  • 2
  • 21
  • 41

3 Answers3

4
  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

Artur Zagretdinov
  • 2,034
  • 13
  • 22
  • I think you should post here more often. There are many interesting bizarre questions around PowerMock around ;-) – GhostCat Aug 07 '17 at 11:28
  • @GhostCat I know. And I try, but I'm focusing more on developing a new version of PowerMock that will help resolve several old issues like code coverage. – Artur Zagretdinov Aug 07 '17 at 12:32
  • Glad to hear about that. And just to prevent misconceptions: I tend to advise people to not use PowerMock. Not because it is a bad framework - but because I have made the experience that too many people do not think about their design; and then instead of improving the design, they take that big PowerMock hammer to "solve" symptoms caused by an inflexible design. – GhostCat Aug 07 '17 at 12:37
  • @Arthur Zagretdinov. This looks like the solution I needs. Would there be any chance of providing a working sample solution? I'm asking because I *think* I've followed your advice, but the method I've mocked out is still getting called! – Stormcloud Aug 08 '17 at 11:02
  • 1
    I've added full example. – Artur Zagretdinov Aug 08 '17 at 19:29
  • Ah, so `fullyQualifiedNames` needs to specify the implicit enum element classes? This is the part I was missing when I said the test in the question should work as is. Perhaps PowerMock could automatically figure this out? (JMockit does it, but I don't remember how anymore.) – Rogério Aug 10 '17 at 15:00
  • The other bit I missed out was `MyEnum one = mock(MyEnum.ONE.getClass());` I had been using `MyEnum one = mock(MyEnum.class);` It was only when I saw the expanded example I realised my mistake – Stormcloud Aug 11 '17 at 08:21
3

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).

GhostCat
  • 137,827
  • 25
  • 176
  • 248
  • PowerMock is not generate a byte code. It does not use proxy as EasyMock or Mockito. It modifies byte-code. And for example in enum case PowerMock does two things: removed final modifier and inject a instruction at the beginning of each methods. – Artur Zagretdinov Aug 06 '17 at 20:56
  • @ArthurZagretdinov I appreciate the correction. But I am wondering. When you do `mockStatic(Foo.class)` - my assumption is that PowerMock **first** checks the method signatures of Foo - to then *generate* something based on that. Maybe that is just nit-picking here. You see: in order to *modify* something, you have to A) take something away B) replace it with something new (which was generated for that purpose). – GhostCat Aug 07 '17 at 11:27
  • Then `mockStatic` is called then a new mock for object Foo.class is created with using one of two mocking frameworks and registered in a repository. The code is inserted at the beginning of static methods checks is mock for this class exist or not. If not, just continues normal execution. If exist, then calls mocking framework engine to get stub response. – Artur Zagretdinov Aug 07 '17 at 12:37
0

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.

Rogério
  • 16,171
  • 2
  • 50
  • 63