Dixon Ivey
Dixon Ivey

Reputation: 15

Testing with argumentCaptor

Im using ArgumentCaptor to capture the internal call of PreparedStatementCreator.

public void update(Item item) {
   String sql = "SET NAME = ? where ID = ?";
   jdbcTemplate.update(new PreparedStatementCreator() {
     @Override
     public PreparedStatement createPreparedStatement(Connection connection) {
       final PreparedStatement ps = connection.prepareStatement(sql);
       ps.setString(1, item.getName());
       ps.setInt(1, item.getId());
     }
   });
 }

Below is the test with ArgumentCaptor.

      @Test
      public void testUpdate() {
        Item item= new Item("1","testName");
        ArgumentCaptor<PreparedStatementCreator> pscArgCaptor = ArgumentCaptor.forClass(PreparedStatementCreator.class);
        insert(item);
        verify(mockJdbc, times(1)).update(pscArgCaptor .capture());
        assertNotNull(pscArgCaptor.getValue(); 
        assertEquals(pscArgCaptor , ?);
      }

I can successful capture the PreparedStatementCreator call when I call update. I used assertNotNull to test and see if the pscArgcaptor was something. I dont know how to get inside this object to verify my parameters in the prepared statement such as ps.setString(1, item.getId() and ps.setString(1, item.getName() to ensure that the prepared statement is the correct one. Is it possible to do without any getters from PreparedStatementCreator?

Upvotes: 1

Views: 1223

Answers (1)

Timothy Truckle
Timothy Truckle

Reputation: 15622

The problem here is that your code under test instantiates its dependencies (here the instance of PreparedStatementCreator) itself.

Instead you should inject an instance of it. In that case you could inject a mock of PreparedStatementCreator and capture the parameters passed to that mock.


I'm fairly new to Junit and Mocking.

This is not so much about mocking but about Single Responsibility/Separation of Concerns. It improves reusability of your code. Testability is a sign of reusable code.

What do you mean by inject a mock. Could you provide me an example?

the problem here is that the interface PreparedStatementCreator does not provide a suitable interface to be used with the Item class as a parameter. Therefore it is usefull to introduce a factory class:

public class ItemPreparedStatementCreatorFactory{
   public PreparedStatementCreator createFor(Item item){
     return new PreparedStatementCreator() {
        @Override
         public PreparedStatement createPreparedStatement(Connection connection) {
           final PreparedStatement ps = connection.prepareStatement( "SET NAME = ? where ID = ?");
           ps.setString(1, item.getName());
           ps.setInt(1, item.getId());
           return ps;
         }
       })
    }
}

You would pass an instance of that class as constructor parameter to your code under test:

Your code under test could look like this:

public class YourDaoClass { 
  private final JdbcTemplate jdbcTemplate;
  private final ItemPreparedStatementCreatorFactory preparedStatementCreatorFactory;
  public YourDaoClass(ItemPreparedStatementCreatorFactory preparedStatementCreatorFactory, JdbcTemplate jdbcTemplate){
    this.preparedStatementCreatorFactory = preparedStatementCreatorFactory;
    this.jdbcTemplate = jdbcTemplate;
  }

Then the method under test would change to:

public void update(Item item) {
  jdbcTemplate.update(preparedStatementCreatorFactory.createFor(item));
}

And you would have separate tests for your code under test.

public class YourDaoClassTest{
    @Rule
    public MockitoRule rule = MockitoJUnit.rule();
    @Mock
    private JdbcTemplate jdbcTemplate;
    @Mock
    private ItemPreparedStatementCreatorFactory preparedStatementCreatorFactory;
    @Mock
    private PreparedStatementCreator preparedStatementCreator;

    YourDaoClass yourDaoClass;


    @Before
    public void setup(){
      // I prefer direct object creation over @InjectMocks since the latter does not raise compile errors on missing constructor arguments...
      yourDaoClass = new YourDaoClass(preparedStatementCreatorFactory,jdbcTemplate); 
    }  


    @Test
    public void passesItemToStatementFactory(){
       Item item = new Item();
       doReturn(preparedStatementCreator)
            .when(preparedStatementCreatorFactory)
            .createFor(item);

       yourDaoClass-update(item);

       InOrder inOrder= inOrder(preparedStatementCreatorFactory,jdbcTemplate);
       inOrder.verify(preparedStatementCreatorFactory).createFor(item);
       inOrder.verify(jdbcTemplate).update(preparedStatementCreator);
    }
}

public class ItemPreparedStatementCreatorFactoryTest{
    @Rule
    public MockitoRule rule = MockitoJUnit.rule();
    @Mock
    private PreparedStatement preparedStatement;
    @Mock
    private Connection connection;

    @Before
    public void setup(){
      // maybe exchange anyString() with an ArgumentCaptor
       doReturn(preparedStatement).when(connection).prepareStatement(anyString());
    }

    @Test
    public void passesNameAndIdToPreparedStatement(){
       Item item = new Item();
       item.setName("an valid name");
       item.setID(ANY_VALID_ID);

       ItemPreparedStatementCreatorFactory itemPreparedStatementCreatorFactory =
           new ItemPreparedStatementCreatorFactory();
       PreparedStatement createdPreparedStatement = itemPreparedStatementCreatorFactory.createFor(item);

       verify(createdPreparedStatement).setString(1, item.getName());
       verify(createdPreparedStatement).setInt(1, item.getId());
   }
}

conclusion

When ever you have difficulties to test your production code it most likely is not written in an reusable way violating the SRP/SoC principles.

On the other hand the tests shown are dumb because there is no real logic to verify because the production code is "too simple to fail" and the tests basically repeat what the code does. Usually such tests are not really useful since they are to tightly coupled with the implementation and break when the implementation changes.

Upvotes: 1

Related Questions