12

I woud like to write a JUnit test to verify that the code below uses a BufferedInputStream:

public static final FilterFactory BZIP2_FACTORY = new FilterFactory() {
    public InputStream makeFilter(InputStream in) {        
        // a lot of other code removed for clarity 
        BufferedInputStream buffer = new BufferedInputStream(in);
        return new CBZip2InputStream(buffer);
    }
};

(FilterFactory is an interface.)

My test thus far looks like this:

@Test
public void testBZIP2_FactoryUsesBufferedInputStream() throws Throwable {
    InputStream in = mock(InputStream.class);
    BufferedInputStream buffer = mock(BufferedInputStream.class);
    CBZip2InputStream expected = mock(CBZip2InputStream.class);

    PowerMockito.spy(InputHelper.BZIP2_FACTORY);  // This line fails
    whenNew(BufferedInputStream.class).withArguments(in).thenReturn(buffer);
    whenNew(CBZip2InputStream.class).withArguments(buffer).thenReturn(expected);
    InputStream observed = InputHelper.BZIP2_FACTORY.makeFilter(in);

    assertEquals(expected, observed);
}

The call to PowerMockito.spy raises an exception with this message:

org.mockito.exceptions.base.MockitoException: 
Mockito cannot mock this class: class edu.gvsu.cis.kurmasz.io.InputHelper$1
Mockito can only mock visible & non-final classes.

What should I be using instead of PowerMocktio.spy to set up the calls to whenNew?

Eric Alberson
  • 1,116
  • 1
  • 11
  • 23
Zack
  • 6,232
  • 8
  • 38
  • 68

4 Answers4

14

The message is pretty obvious: You can't mock non-visible and final classes. Short answer : Create a named class of your anonymous one, and test this class instead!

Long answer, let's dig why !

An anonymous class is final

You instantiate an anonymous class of FilterFactory, when the compiler sees an anonymous class, it creates a final and package visible class. So the anonymous class is not mockable through standard mean i.e. through Mockito.

Mocking anonymous class : possible but BRITTLE if not HACKY

OK, now suppose you want to be able to mock this anonymous class through Powermock. Current compilers compile anonymous class with following scheme :

Declaring class + $ + <order of declaration starting with 1>

Mocking anonymous class possible but brittle (And I mean it) So supposing the anonymous class is the eleventh to be declared, it will appear as

InputHelper$11.class

So you could potentially prepare for test the anonymous class:

@RunWith(PowerMockRunner.class)
@PrepareForTest({InputHelper$11.class})
public class InputHelperTest {
    @Test
    public void anonymous_class_mocking works() throws Throwable {
        PowerMockito.spy(InputHelper.BZIP2_FACTORY);  // This line fails
    }
}

This code will compile, BUT will eventually be reported as an error with your IDE. The IDE probably doesn't know about InputHelper$11.class. IntelliJ who doesn't use compiled class to check the code report so.

Also the fact that the anonymous class naming actually depends on the order of the declaration is a problem, when someone adds another anonymous class before, the numbering could change. Anonymous classes are made to stay anonymous, what if the compiler guys decide one day to use letters or even random identifiers!

So mocking anonymous classes through Powermock is possible but brittle, don't ever do that in a real project!

EDITED NOTE : The Eclipse compiler has a different numbering scheme, it always uses a 3 digit number :

Declaring class + $ + <pad with 0> + <order of declaration starting with 1>

Also I don't think the JLS clearly specify how the compilers should name anonymous classes.

You don't reassign the spy to the static field

PowerMockito.spy(InputHelper.BZIP2_FACTORY);  // This line fails
whenNew(BufferedInputStream.class).withArguments(in).thenReturn(buffer);
whenNew(CBZip2InputStream.class).withArguments(buffer).thenReturn(expected);
InputStream observed = InputHelper.BZIP2_FACTORY.makeFilter(in);

PowerMockito.spy returns the spy, it doesn't change the value of InputHelper.BZIP2_FACTORY. So you would need to actually set via reflection this field. You can use the Whiteboxutility that Powermock provide.

Conclusion

Too much trouble to just test with mocks that the anonymous filter uses a BufferedInputStream.

Alternative

I would rather write the following code:

An input helper that will use the named class, I don't use the interface name to make clear to the user what is the intent of this filter!

public class InputHelper {
    public static final BufferedBZIP2FilterFactory BZIP2_FACTORY = new BufferedBZIP2FilterFactory();
}

And now the filter itself :

public class BufferedBZIP2FilterFactory {
    public InputStream makeFilter(InputStream in) {
        BufferedInputStream buffer = new BufferedInputStream(in);
        return new CBZip2InputStream(buffer);
    }
}

Now you can write a test like this :

@RunWith(PowerMockRunner.class)
public class BufferedBZIP2FilterFactoryTest {

    @Test
    @PrepareForTest({BufferedBZIP2FilterFactory.class})
    public void wraps_InputStream_in_BufferedInputStream() throws Exception {
        whenNew(CBZip2InputStream.class).withArguments(isA(BufferedInputStream.class))
                .thenReturn(Mockito.mock(CBZip2InputStream.class));

        new BufferedBZIP2FilterFactory().makeFilter(anInputStream());

        verifyNew(CBZip2InputStream.class).withArguments(isA(BufferedInputStream.class));
    }

    private ByteArrayInputStream anInputStream() {
        return new ByteArrayInputStream(new byte[10]);
    }
}

But could eventually avoid powermock stuff for this test scenario if you force the CBZip2InputStream to only accept BufferedInputStream. Usually using Powermock means something is wrong with the design. In my opinion Powermock is great for legacy softwares, but can blind developers when designing new code; as they are missing the point of OOP's good part, I would even say they are designing legacy code.

Hope that helps !

bric3
  • 40,072
  • 9
  • 91
  • 111
  • +1 for the $1 inner class :) I thought about it, but I did not dare to try it. On the other hand, for short classes I would prefer using anonymous class and the $1 class. Its just ugly to name a class only for testing. – Gábor Lipták Mar 06 '12 at 21:52
  • Yeah I agree, but in this case the anonymous class is pretty simple and shall not need such heavy testing setup. In Zack's case it seems he's dealing with more code in the inner class. In the end it depends of the criticality of what is being tested, if it is deemed important then the code itself shall be conspicuous about it. – bric3 Mar 07 '12 at 11:18
  • Thanks to you I realize, that I have miss some important knowledge. I use utils (or helper) class to reduce code for creating and testing abstract class and of course i want spy them. That was mistake, so now I create simple extending class in test sources and spy this instead and its ok – Perlos Dec 04 '12 at 06:10
10

Old post, but you don't need to create a named class - use wildcards instead as mentioned in this post powermock mocking constructor via whennew() does not work with anonymous class

@PrepareForTest(fullyQualifiedNames = "com.yourpackage.containing.anonclass.*")

Community
  • 1
  • 1
Matt Byrne
  • 4,908
  • 3
  • 35
  • 52
  • This is the correct answer. But if you are PowerMocking an individual class it might look like `@PrepareForTest(fullyQualifiedNames = "com.yourpackage.YourClass*")` – gladed Sep 23 '16 at 20:25
1

You need to run the test using the PowerMockito runner, and you need to tell the framework which class(es) should have custom behaviour. Add the following class annotations on your test class:

@RunWith(PowerMockRunner.class)
@PrepareForTest({ BufferedInputStream.class })
LaurentG
  • 11,128
  • 9
  • 51
  • 66
  • The annotations aren't the problem. If I change InputHelper.BZIP2_FACTORY from an anonymous inner class to a named inner class, then it works. – Zack Nov 06 '11 at 13:29
0

I just came around the same problem. So according to the documentation of constructor mocking you need to prepare the class, which will create the evil class(es). In your case the evil classes are BufferedInputStream and CBZip2InputStream, and the creator of them is an anonymous class, which cannot be defined in PrepareForTest annotation. So I had to do the same as you did (hmm, just saw your comment), I moved the anonymous class to named class.

Gábor Lipták
  • 9,646
  • 2
  • 59
  • 113