I am facing the below problem where, when indirectly updating the fields on a spy object, the spy does NOT see updates on primitive fields, whereas it sees on reference once.
As an example:
import org.junit.Test; import org.mockito.Mockito; import java.util.function.Consumer; import java.util.concurrent.atomic.AtomicBoolean; public class MyTest { class Actor { Consumer<Boolean> consumer; void init(Consumer<Boolean> consumer){ this.consumer = consumer; } void act(boolean flag){ this.consumer.apply(flag); } } class TestClass { boolean field = true; AtomicBoolean refField = new AtomicBoolean(true); TestClass(Actor actor){ actor.init( flag -> { System.out.println("Changing field to " + flag); field = flag; refField.set(flag); }); } void call(){ this.doSomething(field); } void callRef(){ this.doSomething(refField.get()); } void doSomething(boolean flag){ System.out.println("#doSomething(" + flag + ")"); } } @Test public void test(){ // given an actor and a spied TestClass Actor actor = new Actor(); TestClass spy = Mockito.spy(new TestClass(actor)); // when invoking the action with the primitive spy.call(); // then expect correct invocation Mockito.verify(spy, Mockito.times(1)).doSomething( true ); // when invoking the action with the ref field spy.callRef(); // then expect correct invocation Mockito.verify(spy, Mockito.times(2)).doSomething( true ); // when changing the flag to 'false' actor.act( false ); // and invoking the action with the refField spy.callRef(); // then expect correct invocation Mockito.verify(spy, Mockito.times(1)).doSomething(false); // when invoking the action with the primitive spy.call(); // then method is NOT invoked as expected !!!!!!! Mockito.verify(spy, Mockito.times(2)).doSomething(false); } }
The last verification, will fail, as the method is called with the first primitive value.
I was wondering why this is happenning? Is this an expected behaviour. Running the above test, will produce the below logging:
#doSomething(true) #doSomething(true) Changing flag to false #doSomething(false) #doSomething(true)
I would expect the last log statement to be invoked with false
.
Any insights on the above?
PS: Version=mockito-core:2.25.0
Advertisement
Answer
There are 2 instances of TestClass object in your system:
- original instance
- spy instance
The spy is created from original instance.
- primitive fields (like
boolean field
) are copied, each TestClass holds - references are copied, but underlying, referenced object is shared (
refField
)
Lets make following changes to observe that:
TestClass(Actor actor){ actor.init( flag -> { // print referenced TestClass instance System.out.println("from actor: " + TestClass.this); System.out.println("Changing field to " + flag); field = flag; refField.set(flag); }); } @Test public void test(){ // given an actor and a spied TestClass Actor actor = new Actor(); TestClass testClassOriginal = new TestClass(actor); TestClass spy = Mockito.spy(testClassOriginal); System.out.println("org" + testClassOriginal); System.out.println("spy" + spy); // when changing the flag to 'false' actor.act( false ); System.out.println("After change org: " + testClassOriginal.field); System.out.println("After change spy: " + spy.field); }
Which gives the following output:
org: mypackage.MyTest$TestClass@4218500f spy: mypackage.MyTest$TestClass$MockitoMock$2115616475@5c10f1c3 from actor: mypackage.MyTest$TestClass@4218500f Changing field to false After change org: false After change spy: true
You can clearly see:
- two instances of TestClass
- actor sees the original object, not the spy
- when actor makes the change to the primitive field, it changes the original
- primitive field in the spy remains unchanged
Possible solutions
You need to make the actor see the spy, not the original object. To achieve that, creation of TestClass must be decoupled from registering the consumer.
class TestClass { boolean field = true; AtomicBoolean refField = new AtomicBoolean(true); TestClass(){ } void consumeFlag(Boolean flag) { System.out.println("from actor: " + this); System.out.println("Changing field to " + flag); field = flag; refField.set(flag); } // ... } @Test public void test(){ // given an actor and a spied TestClass Actor actor = new Actor(); TestClass spy = Mockito.spy(new TestClass()); actor.init(spy::consumeFlag); // ... }