Mockito verify the last call on a mocked object
Asked Answered
B

4

9

I have a bit of logic that needs to be tested such as:

{
    ...
    A.add("1");
    ...
    A.add("what ever");
    ...
    A.add("2");
    A.delete("5");
    ...
}

I have already mocked A in my test and I can test the add method is called once on argument ("2") such as:

Mockito.verify(mockedA).add("2");

My question is how can I test if I can verify the last call on method add is add("2") instead of other arguments.

Since the test above can't catch if somebody by accident adds another call such as add("3") in the last. Please notice that we don't care about other method invocations on A again afterwards. We also don't care about the times of the method called, the sequence of the methods called. The key point here is if we can verify the last true argument on a certain method of a certain mockedObject.

If you ask why do you need such functionality, I'd say in real world we might need to handle some logic that set something and the last set wins, and in order to avoid someone by accident set some other thing unexpected and I'd like to use our UT to catch this. And in order not to make the test too complex and neat, so I only expect to verify the last call on a certain method of a object instead verify something like order/noMoreInteractions/AtMostTimes and so on.

Blessington answered 24/5, 2018 at 7:43 Comment(2)
See <#8504574> , maybe you will find it helpful.Aileneaileron
Thanks, this is very helpful.Blessington
B
7

Thanks @staszko032, inspired by the ArgumentCaptor, instead of getAllValues and verify the sequence, we can use getValue of captor since captor's getValue always get the last true argument. We can do it like this:

    ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
    Mockito.verify(mockedA, Mockito.atLeastOnce()).add(captor.capture());
    Assert.assertEquals("2", captor.getValue());
Blessington answered 25/5, 2018 at 2:24 Comment(0)
C
7

About the order of the invocations

By default, Mockito.verify() doesn't matter of the invocation order.
To take it into consideration, wrap the mock in an InOrder instance and perform the invocation verification on this instance.

About the no more interations

If the mock is no more invoked after the methods that you want to verify, you could use Mockito.verifyNoMoreInteractions(Object... mocks) that checks if any of given mocks has any unverified interaction such as :

InOrder inOrder = Mockito.inOrder(mockedA);
inOrder.verify(mockedA).add("1");
inOrder.verify(mockedA).add("2");
Mockito.verifyNoMoreInteractions(mockedA);

If the mock may still be invoked after the methods that you want to verify, you could add after your verifies an invocation to verify(T mock, VerificationMode mode) by passing a VerificationMode that checks that at most 2 invocations were performed.

InOrder inOrder = Mockito.inOrder(mockedA);
inOrder.verify(mockedA).add("1");
inOrder.verify(mockedA).add("2");
Mockito.verify(mockedA, Mockito.atMost(2)).add(Mockito.anyString());

A warning about your think and this way of mocking

Since the test above can't catch if somebody by accident adds another call such as add("3") in the last.

Mockito provides a powerful and broad toolkit to work with mocks. Some features such as verify and more particularly verify that no more interaction was detected about a mock or a specific method of the mock make your test more complex to read and to maintain.
As well as, currently you want to check that the invocations on a mock were performed in a specific order. But you generally want to use these checks only as required that is according to the business/logic scenarios, not technical invocations.
For example supposing that in the tested method you have a case where for business reasons the mocked method is invoked 3 times and another case where the mocked method is invoked 2 times. It could make sense to check that it is only invoked 2 times and not more in the case with two expected invocations.
But in a general way, you should be cautious that your unit test doesn't overuse mocking verify that could look like as a assertion on the description of the flow and not a assertion on the behavior/logic.

Cohobate answered 24/5, 2018 at 7:46 Comment(8)
This doesn't work, since the logic can call other methods such as A.delete() on A, the key point here is verify the last true arguement on a certain METHOD of a object.Blessington
@Blessington OK. I upated to distinguish two cases.Cohobate
Thanks. while this is still not what we expect. What if the add method called more than twice? The key point is can we verify the last call on a certain method of a object?Blessington
@Blessington Ok. I understand your point .I updated but I wonder really if adding so complexity in the test makes sense. I add a last point about my think about that.Cohobate
As I want to avoid complexity so I'd like to have such a functionality. At Most Times can work now, but it can't catch if someone move the first call to the last. Your UT pass, but your service breaks. Or you can argue that we can use inOrder to test the sequence. But I'd say what if someone remove the first one and the method is still not breaking. But your UT breaks. Overall, this is not what we expect.Blessington
If someone changes the test code in a incorrect way, what do you want to do against that ? A tests make sense only if it is correct. That's all. I have really the feeling that you overthink.Cohobate
I'm sorry you have that feeling and I don't want to argue if it's overthink or not. We want to test the real point/main point, and if possible, moving further to catch bugs by your test is a plus. This makes your service more robust and also makes the refactoring guy more confident to do work.Blessington
@GhostCat Hello my dear ! Thanks for these good words ! As usual :) I hope you are fine.Cohobate
B
7

Thanks @staszko032, inspired by the ArgumentCaptor, instead of getAllValues and verify the sequence, we can use getValue of captor since captor's getValue always get the last true argument. We can do it like this:

    ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
    Mockito.verify(mockedA, Mockito.atLeastOnce()).add(captor.capture());
    Assert.assertEquals("2", captor.getValue());
Blessington answered 25/5, 2018 at 2:24 Comment(0)
M
1

I've ended up implementing a new VerificationMode to do this. Here's the code in case it's useful to anyone.

class MostRecently implements VerificationMode {

    @Override
    public void verify(VerificationData data) {
        MatchableInvocation target = data.getTarget();
        List<Invocation> invocations = data.getAllInvocations();
        if (invocations.isEmpty()) {
            throw wantedButNotInvoked(target);
        }
        List<Invocation> targets = findInvocations(invocations, target);
        if (targets.isEmpty()) {
            throw wantedButNotInvoked(target);
        }
        Invocation lastInvocation = invocations.get(invocations.size() - 1);
        if (target.matches(lastInvocation)) {
            return;
        }
        ListIterator<Invocation> iterator = invocations.listIterator(invocations.size());
        Invocation previous = iterator.previous();
        Invocation undesired = previous;
        while (!target.matches(previous)) {
            undesired = previous;
            previous = iterator.previous();
        }
        Invocation lastGoodInvocation = previous;

        throw new MockitoAssertionError(join(
                "Wanted most recent on '" + lastGoodInvocation.getMock() + "' to be " + lastGoodInvocation.getMethod(),
                "No more interactions wanted after " + lastGoodInvocation.getLocation(),
                "but found this interaction on mock '"
                        + MockUtil.getMockName(undesired.getMock()) + "':",
                undesired.getLocation(),
                allLocations(invocations)));
    }

    static String allLocations(List<Invocation> invocations) {
        StringBuilder sb = new StringBuilder("***\nFor your reference, here is the list of all invocations.\n");

        int counter = 0;
        for (Invocation i : invocations) {
            sb.append(++counter).append(". ");
            sb.append(i.getLocation()).append("\n");
        }

        return sb.toString();
    }
}
Mur answered 30/11, 2022 at 10:43 Comment(0)
H
0

What this looks like to me is that you are mocking a data class. In my experience it's better to leave (stateful) data classes and mock (stateless) services. This way, you can verify that the method under test produces the correct data, rather than just verifying a series of invocations. Along with testdata builders (making it easy to instantiate your data classes with some default state, using builder pattern for instance), it becomes real easy to write tests.

If you do need to mock, the only way to test what you want is to use InOrder, and verify each of the invocations on the mock, and end with verifyNoMoreInteractions.

Huambo answered 24/5, 2018 at 10:6 Comment(5)
I have thought of using real object instead of mocked one to test the last state we have. This is not a bad idea. When the class is trivial, this is perfect. While if the class delegates that to another class, and this class also delegates to other classes, being complex, having many dependencies, it's not easy to do this. And also it's more like testing the A logic instead of testing the logic of using A.Blessington
Your test will "expand" a bit, that is correct. But usually, data classes are quite simple. Using the builder pattern can mitigate some of the complexity as well, I have used it with success in the past. If your data class is a Person, and you just want the default person, you do new PersonBuilder().build();. Then you get a person named "Test Testman" or something like that. If you want to override some values for your specific test, you do something like new PersonBuilder().firstName("Donald").lastName("Duck").address(new AddressBuilder().city("Duckburg").build()).build();Huambo
It's a bit of work creating these builders, but once you have them in place writing tests using them becomes really pleasant. Much easier to verify state than to verify a number of method invocations.Huambo
Consider such case: what if your class delegates that to another class, and this class also delegates to other classes, being complex, having many dependencies?Blessington
I tried to show how a graph of classes would be handled with the Person-Address in the example. You would need a builder for each class in the tree (but they can perhaps be created as they are needed, not all at once). Of course, if your data class graph consists of hundreds of classes, then it might not be the best approach.Huambo

© 2022 - 2024 — McMap. All rights reserved.