Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
Mocking methods with arguments
Let's say I have this class I want to mock
class A {
public void add(T arg) {
B b = A.getB();
U val = somefunc(arg);
V ret = b.add(val);
}
}
I have a spy on A and a mock on B, but when A::foo calls B::bar, I want it to be mocked. But I want some stuff to be done inside it. Something like this:
class BMock {
List<U> ulist = new ArrayList<>();
R add(U u) {
if(ulist.contains(u)) throw exception;
}
return new R(x);
}
But I'm having a bit of a problem how to do this. I'm looking for something on this form:
A a = spy(A.class);
B b = mock(B.class);
a.b = b;
BMock bm = new BMock();
doAnswer(bm.add(arg)).when(b).add(any());
^ |
| |
------------------
What I want to do is to send the any argument over to bm.add
In the real code, B::add will contact a remote server to add an item. If it gets called twice with the same item, the server will return an error. A::add will analyze this return code and act accordingly. And this is what I'm trying to simulate. How should I do it?
EDIT
I got some suggestions in comment section. But I might add that I cannot change B because it's an external library, and the class B both misses default constructor and is final.
3 answers
Based on your answer, I guess you just wanted to have a list of unique arguments that were passed to B::add
. In that case, you could use a Set
instead of a List
:
// "Type" is whatever type B:add receives as argument
Set<Type> calledArgs = new LinkedHashSet<>();
@Test
public void test() {
doAnswer(i -> {
Type arg = i.getArgument(0);
calledArgs.add(arg);
return null;
}).when(b).add(any());
}
A Set
doesn't allow duplicated elements, so you can just add them and the Set
will only add elements that aren't already in the set. I've used a LinkedHashSet
to keep the elements in the order they were inserted (but if you don't care about that, you could use a HashSet
instead).
If you really want a list, though, just create one after the test:
// creates a List with all the Set's elements
List<Type> list = new ArrayList<>(calledArgs);
It's not clear what type B::add
receives as argument (in your answer you create a List<U>
, but then you add a T
instance to that list), but anyway, just change Type
to whatever type the method expects.
Another solution is to use an org.mockito.ArgumentCaptor
to capture the arguments passed to B::add
:
// setup "a" and "b" (the same way you're already doing)
B b = mock(B.class);
A a = spy(A.class);
a.b = b;
...
@Test
public void test() {
// call a.add() many times, passing whatever type it expects
int n = // ... number of times a.add() will be called
for (int i = 0; i < n; i++) {
a.add(whateverArgsItExpects);
}
// after the calls to a.add(), check the arguments that were passed to b.add()
ArgumentCaptor<Type> captor = ArgumentCaptor.forClass(Type.class);
verify(b, times(n)).add(captor.capture()); // <-- HERE: the ArgumentCaptor will capture all args passed to B::add
// get all the values passed to b.add()
List<Type> calledArgs = captor.getAllValues();
// creates a Set with the values
Set<Type> calledArgsNoRepeat = new LinkedHashSet<>(calledArgs);
// check whatever you need in the List/Set
}
I'm assuming that Type
is the object's class you're passing to B::add
.
I'm using Mockito.verify
(assuming you're using import static org.mockito.Mockito.*
, as your code suggests) to check if B::add
is being called as many times as A::add
(assuming that every call to A::add
will also call B::add
, but you can adjust the number to your test cases). And the ArgumentCaptor
will capture the arguments passed to B::add
in all those calls.
Then I get the list of all the arguments that were passed to B::add
(by using captor.getAllValues()
) and create a Set
with it, which eliminates the duplicates. Again, I used a LinkedHashSet
to preserve the order, but a HashSet
can also be used if you don't care about the order.
Obviously, this assumes that
Type
correctly implementsequals
andhashCode
methods. If it doesn't, the set won't be able to check for duplicates (as your code usesList::contains
, I'm assuming that those methods are correctly implemented).
I managed to do it with lambda. Something like this:
Set<U> ulist = new LinkedHashSet<>();
@Test
void test() {
doAnswer(i -> {
U arg = i.getArgument(0);
if(ulist.contains(arg)) {
return 0;
}
ulist.add(arg);
return 1;
}).when(b).add(any());
0 comment threads
You mean, aside from giving the team that made B
a stern talking to? ;-)
You could introduce another layer of indirection, and mock that. For instance like this:
interface MockFriendlyB {
public void add(T arg);
}
class UnmockedB implements MockFriendlyB {
B b = ...;
public void add(T arg) {
b.add(arg);
}
}
class MockedB implements MockFriendlyB {
public void add(T arg) {
// do whatever you like
}
}
and then change A
to use a MockFriendlyB
rather than B
, and give A
an UnmockedB
in normal usage, but a MockedB
when testing.
1 comment thread