I have a load method and a save method in a DAO and a domain class. In my application I load a domain object, update attribute values and save the object again. I wrote this test for the app:
import ...
@RunWith(MockitoJUnitRunner.class)
public class MyAppTest
{
@Mock
private MyDao myDao;
@InjectMocks
private MyApp myApp;
@Test
public void testMyMethod(){
// given
MyDomainClass myObj = new MyDomainClass();
when(myDao.load(anyInt())).thenReturn(myObj);
// when
myApp.myMethod();
// then
ArgumentCaptor<MyDomainClass> argumentCaptor = ArgumentCaptor.forClass(MyDomainClass.class);
verify(myDao).save(argumentCaptor.capture());
assertThat(argumentCaptor.getValue().myInt, is(1));
}
}
The test passes although the logic contains a bug:
public class MyApp
{
private MyDao myDao = new MyDao();
public void myMethod(){
MyDomainClass myObj = myDao.load(0);
myDao.save(myObj);
System.out.println("Saved myObj with myInt=" + myObj.myInt); // myInt=0
myObj.myInt = 1;
}
}
My question is if anyone else experienced this, and what (junit) test strategy would expose the bug.
EDIT: In response to the first suggested solution, I replaced the argument captor like so:
...
@Test
public void testMyMethod(){
...
MyDomainClass expected = new MyDomainClass();
expected.myInt = 1;
verify(myDao).save(eq(expected));
}
and the test still passes. The reason is the same: by the time verify is invoked, the value of myInt is 1 in both instances. I'm trying to verify that save was invoked with myInt of myObj set to 0, i.e. this test should fail.
My question was also if there is a general thing, e.g. a different test strategy that I should consider. Are ArgumentCaptors a bad choice in some cases? I know I could write an integration test instead, but imo this should be testable in a unit test.
By the time this is invoked ...
assertThat(argumentCaptor.getValue().myInt, is(1));
... the value of myInt is 1 since it is set to that value inside myApp.myMethod(); which is invoked before the verification.
Using an Answer
If you want to assert against the state of MyDomainClass as it was when myDao.save() was invoked then you'll need to capture that state via an answer in the 'when' stage.
Since Mockito records a reference to the MyDomainClass instance passed to myDao.save() (rather than a copy of it) you could use an Answer to record your own copy and then assert against that copy.
For example:
@Test
public void testMyMethod(){
// given
MyDomainClass myObj = new MyDomainClass();
when(myDao.load(anyInt())).thenReturn(myObj);
AtomicReference<MyDomainClass> actualSavedInstance = new AtomicReference<>();
doAnswer(invocation -> {
MyDomainClass supplied = (MyDomainClass) invocation.getArguments()[0];
// copy/clone state from supplied - which represents the instance passed to save - into a separate
// instance which can be used for an assertion
MyDomainClass actual = new MyDomainClass();
actual.myInt = supplied.myInt;
actualSavedInstance.set(actual);
return null;
}).when(myDao).save(myObj);
// when
myApp.myMethod();
assertThat(actualSavedInstance.get().myInt, is(0));
}
An Alternative
An alternative to this might be to delegate the 'post update increment' within MyDao.save() to a separate actor and then mock that actor and verify it.
For example, change MyApp as follows:
public void myMethod(){
MyDomainClass myObj = myDao.load(0);
myDao.save(myObj);
System.out.println("Saved myObj with myInt=" + myObj.myInt); // myInt=0
// the handler will do this: myObj.myInt = 1;
postSaveHandler.handle(myObj);
}
And then implement the test like so:
@RunWith(MockitoJUnitRunner.class)
public class MyAppTest
{
@Mock
private MyDao myDao;
@Mock
private PostSaveHandler postSaveHandler;
@InjectMocks
private MyApp myApp;
@Test
public void testMyMethod(){
// given
MyDomainClass myObj = new MyDomainClass();
when(myDao.load(anyInt())).thenReturn(myObj);
// when
myApp.myMethod();
ArgumentCaptor<MyDomainClass> argumentCaptor = ArgumentCaptor.forClass(MyDomainClass.class);
verify(myDao).save(argumentCaptor.capture());
assertThat(argumentCaptor.getValue().myInt, is(0));
verify(postSaveHandler).handle(myObj);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With