2

I have run into a problem using PowerMock where I feel forced to create ugly code if I want to return a mocked item only once. As an example I have the following code:

mockMap = spy(new HashMap());
HashMap<String, String> normalMap = new HashMap<>();
HashMap<String, String> normalMap2 = new HashMap<>();
HashMap<String, String> normalMap3 = new HashMap<>();
whenNew(HashMap.class).withNoArguments()
    .thenReturn(normalMap)
    .thenReturn(mockMap)
    .thenReturn(normalMap2)
    .thenReturn(normalMap3);

This of course works, but it feels very clunky, especially as I need to create a new hashmap for every new call.

So my question is: is there a way to tell PowerMock that it should stop interfering after a set amount of calls?

edit: After reading the answers, I got the following:

final AtomicInteger count = new AtomicInteger(0);
whenNew(HashMap.class).withNoArguments().thenAnswer(invocation -> {
switch (count.incrementAndGet())
{
        case 1:
            return mockMap;
        default:
            return new HashMap<String, String>();
    }
});

However it gives me a StackOverFlowError using the following code:

describe("test", () -> {
    beforeEach(() -> {
        mockStatic(HashMap.class);
        final AtomicInteger count = new AtomicInteger(0);
        whenNew(HashMap.class).withNoArguments().thenAnswer(invocation ->
        {
            switch (count.incrementAndGet())
            {
                case 5:
                    return mockMap;
                default:
                    return new HashMap<String, String>();
            }
        });
    });
    it("Crashes on getting a new HashMap", () -> {
        HashMap map = new HashMap<>();
        map.put("test", "test");
        HashMap normal = new HashMap<String, String>();
        expect(normal.containsValue("test")).toBeTrue();
    });
});

Worth noting is that I don't have a mockStatic(HashMap.class) in my larger tests that get the same errors(which I get rid of if I remove the mockStatic call)

A solution that works(but feels like a workaround) is building on that by editing the default statement into:

default:
    normalMap.clear();
    return normalMap;
munHunger
  • 2,572
  • 5
  • 34
  • 63
  • My question is: why don't you have accessto that hashmap? Is it an *implementation detail* which your test should not know about at all? – Timothy Truckle May 11 '17 at 12:36
  • @TimothyTruckle The reason for it is that it is using the hashmap to build headers for an HTTP request. If I where to give access from the testcase then I think that my under test class would break and I don't see any good way around it. – munHunger May 11 '17 at 12:42
  • you can shorten the code like this `.thenReturn(normalMap, mockMap, normalMap2, normalMap3)`; since it takes varargs. – pvpkiran May 11 '17 at 12:44
  • if you only need mockMap on second call and just a empty hashmap on other calls you can do it like this `.thenReturn(normalMap, mockMap, normalMap)`. after second calls it keeps returning the last item i.e normalMap – pvpkiran May 11 '17 at 12:51
  • 1
    *"it is using the hashmap to build headers for an HTTP request."* - UnitTests verify *public observable behavior*. That means you verify the output and/or communication with dependencies. But a Hashmap usually is a datastructure, not a dependency. – Timothy Truckle May 11 '17 at 12:53
  • @pvpkiran I tried that however the problem that I got was that on the second normalMap it already had some entries(i.e I did not get a new HashMap) which broke other parts of the system – munHunger May 11 '17 at 13:01

2 Answers2

2

You could use thenReturn with multiple arguments like this:

whenNew(HashMap.class).withNoArguments()
    .thenReturn(normalMap, mockMap, normalMap2, normalMap3);

Or write your own Answer like this:

whenNew(HashMap.withNoArguments()).doAnswer(new Answer() {
    private int count = 0;

    public Object answer(InvocationOnMock invocation) {
        // implement your logic
        // if (count ==0) etc.
    }
});
R. Oosterholt
  • 7,720
  • 2
  • 53
  • 77
1

You can use Mockito's org.mockito.stubbing.Answer:

final AtomicInteger count = new AtomicInteger(0);
PowerMockito.whenNew(HashMap.class).withNoArguments()
    .thenAnswer(new Answer<HashMap<String, String>>() {

    @Override
    public HashMap<String, String> answer(InvocationOnMock invocation) throws Throwable {
        count.incrementAndGet();
        switch (count.get()) {
            case 1: // first call, return normalMap
                return normalMap;
            case 2: // second call, return mockMap
                return mockMap;
            case 3: // third call, return normalMap2
                return normalMap2;
            default: // forth call (and all calls after that), return normalMap3
                return normalMap3;
        }
    }
});

Note that I had to use java.util.concurrent.atomic.AtomicInteger and declare it as a final variable for 2 reasons:

Note: in this solution, you must also change all normalMap's to be final


Actually, if all your normalMap's are the same, you can just do:

PowerMockito.whenNew(HashMap.class).withNoArguments()
    .thenAnswer(new Answer<HashMap<String, String>>() {

    @Override
    public HashMap<String, String> answer(InvocationOnMock invocation) throws Throwable {
        count.incrementAndGet();
        if (count.get() == 2) { // second call
            return mockMap;
        }
        return normalMap; // don't forget to make normalMap "final"
        // or if you prefer: return new HashMap<String, String>();
    }
});

PS: as stated in this answer, instead of using AtomicInteger, you can also create an int counter inside the anonymous class (it's up to you to choose, as both work):

PowerMockito.whenNew(HashMap.class).withNoArguments()
    .thenAnswer(new Answer<HashMap<String, String>>() {

    private int count = 0;

    @Override
    public HashMap<String, String> answer(InvocationOnMock invocation) throws Throwable {
        count++;
        if (count == 2) { // second call
            return mockMap;
        }

        return normalMap; // don't forget to make normalMap "final"
        // or if you prefer: return new HashMap<String, String>();
    }
});

And this also works with the switch solution as well.

Community
  • 1
  • 1
  • I really like that I can logically choose when to return the mock using the thenAnswer function, however I am still getting stuck on it not returning a new object on each normal object. That results in a large switch anyways so I need to write more code than before. So, I would need it to return a empty HashMap on every normal object in order for this to work – munHunger May 11 '17 at 13:16
  • What do you mean by *"not returning a new object on each normal object"*? –  May 11 '17 at 13:20
  • ok, say that I don't want the first two objects to be mocked. The code that is under test will edit the first HashMap returned by adding entries. The second time the code under test creates a HashMap, it will get a HashMap containing those entries. This is if I follow your last example. If I where to edit it to not return normalMap and instead new HashMap(), I get a stackoverflow exception. one solution that I have found is clearing the normalMap before returning it, but it seems like a workaround – munHunger May 11 '17 at 13:26
  • 1
    Could you edit your question and post your test code? Just to check why the stackoverflow exception occurs (my test if very simple and I've got no errors) –  May 11 '17 at 13:29
  • I have made some edits, however I cannot post the test code. – munHunger May 11 '17 at 13:45
  • Well, I'm still not getting any errors (I did a test adding some entries to the hashmaps as you said, but no error occurs). The stackoverflow might be related to something else you're doing with these maps or in some other part of the code. But I can't figure out what it is.. Would you mind posting the stacktrace/error messages? –  May 11 '17 at 13:55
  • I managed to reproduce it with a much smaller test(se my example). However in that example I must use mockStatic to get the overflow exception, which I don't otherwise use. – munHunger May 11 '17 at 14:00
  • I don't think you need to `mockStatic` a `HashMap`, as you're not using any static method of it. Are you using [spectrum](https://github.com/greghaskins/spectrum)? –  May 11 '17 at 14:16
  • I know I don't need it and I don't use it in my real test, however it was atleast a way of reproducing the error without pasting in 50 files. No I am using [oleaster-runner](https://github.com/mscharhag/oleaster/tree/master/oleaster-runner) instead of spectrum – munHunger May 11 '17 at 14:33
  • I'm getting trouble to run your code. How do you combine powermock and oleaster runners together? Sorry, I don't use oleaster very often –  May 11 '17 at 14:43
  • No need to apologize, I am just glad that you are trying to help :) if you want to use oleaster and powermock you need to provide the following annotations on the test class `@RunWith(PowerMockRunner.class) @PowerMockRunnerDelegate(OleasterRunner.class)` – munHunger May 11 '17 at 14:50
  • 1
    I tried that and it didn't work. Maybe I'm using an incompatible version of powermock, I don't know. I'll take a look and see what I can find.. –  May 11 '17 at 15:00
  • 1
    I could reproduce the error (by changing my powermock's version), but not sure how to fix it. Looking at the stacktrace, I can't know if the problem is in powermock, mockito, oleaster, or the combination of all. Have you tried using old-plain unit tests (without oleaster)? I'd also suggest opening an issue in oleaster github. –  May 11 '17 at 15:50