使用argumentCaptor进行测试

时间:2018-03-25 15:16:39

标签: java unit-testing junit mocking mockito

我使用ArgumentCaptor捕获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());
     }
   });
 }

以下是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 , ?);
      }

我可以在调用update时成功捕获PreparedStatementCreator调用。我使用assertNotNull来测试并查看pscArgcaptor是否是某种东西。我不知道如何进入此对象以验证预准备语句中的参数,例如ps.setString(1, item.getId()ps.setString(1, item.getName(),以确保准备好的语句是正确的。是否可以不使用PreparedStatementCreator中的任何getter?

1 个答案:

答案 0 :(得分:0)

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.