【问题标题】:Testing with argumentCaptor使用 argumentCaptor 进行测试
【发布时间】:2018-03-25 15:16:39
【问题描述】:

我正在使用 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?

【问题讨论】:

  • 坦率地说,这是你不应该嘲笑的事情。调用您的实际代码,并检查预期的数据是否已在数据库中插入/更新。
  • 我会在 JB Nizet 评论中添加:通过使用内存数据库。
  • @davidxxx 除非您真的关心您的代码是否适用于您的实际生产数据库。
  • @JB Nizet 单元测试和集成测试是互补的。生产数据库应该用于集成测试而不是本地测试,但如果您希望构建缓慢或单元测试之间的潜在副作用。
  • @TimothyTruckle 你在争论这种测试的最佳名称。我真的不在乎那个。我关心编写有意义的测试,并让我相信我的代码能完成它应该做的事情。

标签: java unit-testing junit mocking mockito


【解决方案1】:

这里的问题是您的 被测代码 实例化了它的依赖项(这里是 PreparedStatementCreator 的实例)本身。

你应该注入它的一个实例。在这种情况下,您可以注入 PreparedStatementCreator 的模拟并捕获传递给该模拟的参数。


我对 Junit 和 Mocking 还很陌生。

这不是关于嘲笑,而是关于单一职责/关注点分离。它提高了代码的可重用性。可测试性是可重用代码的标志。

注入模拟是什么意思。可以举个例子吗?

这里的问题是接口PreparedStatementCreator 没有提供合适的接口与Item 类一起作为参数使用。因此引入一个工厂类是很有用的:

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;
         }
       })
    }
}

您可以将该类的实例作为构造函数参数传递给您的测试代码:

您的测试代码可能如下所示:

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

那么被测方法会变成:

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

而且您将对被测代码进行单独的测试。

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());
   }
}

结论

当您在测试生产代码时遇到困难时,很可能不是以违反 SRP/SoC 原则的可重用方式编写的。

另一方面,显示的测试是愚蠢的,因为没有真正的逻辑可以验证,因为生产代码“太简单而不会失败”,并且测试基本上重复了代码所做的事情。通常这样的测试并没有真正的用处,因为它们与实现紧密耦合,并在实现发生变化时中断。

【讨论】:

  • 我对 Junit 和 Mocking 还很陌生。注入模拟是什么意思。你能给我举个例子吗?
猜你喜欢
  • 2023-03-10
  • 1970-01-01
  • 2022-01-12
  • 2012-08-31
  • 1970-01-01
  • 2023-02-06
  • 1970-01-01
  • 2013-02-05
相关资源
最近更新 更多