【问题标题】:How to test catch block in Java with Spock that has a simple log statement如何使用具有简单日志语句的 Spock 在 Java 中测试 catch 块
【发布时间】:2019-09-24 04:03:42
【问题描述】:

我有一个简单的 Java 方法,我想用 Spock 进行单元测试

private void executeDataLoad(String sql) {
        Statement snowflakeStatement=null;
        try {

            snowflakeStatement = getSnowflakeStatement();
            log.info("Importing data into Snowflake");
            int rowsUpdated = snowflakeStatement.executeUpdate(sql);
            log.info("Rows updated/inserted:  " + rowsUpdated);
        }
        catch (SQLException sqlEx) {
            log.error("Error importing data into Snowflake", sqlEx);
            throw new RuntimeException(sqlEx);
        }finally{
            try {
                if (snowflakeStatement != null)
                    snowflakeStatement.close();
            } catch (SQLException sqlEx) {
                log.error("Error closing the statement", sqlEx);
            }
        }
    }

我想在最后测试一下 catch 块。这是一个简单的 catch 块,它只记录一条语句。我看到的所有示例都只测试了在 catch 块中带有 throw 关键字的 catch 块。

如何测试以确保 catch 块被执行?

【问题讨论】:

  • 请了解MCVE 是什么,并尝试在您的问题中始终使用一个。没有围绕它的类并且缺少成员和方法的孤立方法不是 MCVE,因为我无法编译和运行它。你也没有显示任何测试代码,所以我看不到你尝试了什么。

标签: java unit-testing spock


【解决方案1】:

简单的答案是:您不直接测试私有方法。

相反,良好的测试实践是使用必要的参数和注入对象(通常是模拟对象)来测试公共方法,以便覆盖公共和私有方法中的所有执行路径。如果不能通过调用公有方法覆盖私有方法代码,则表明

  • 要么你的类不能很好地测试,你应该重构
  • 或(部分)您的私有方法代码无法访问,因此应删除
  • 或者两者兼而有之。

您的代码还存在实例化其自身依赖项的问题,在本例中为Statement 对象。如果您可以将其作为方法参数注入,而不是将其构造为局部变量的方法,您可以轻松地注入一个模拟、存根或间谍,并使该模拟对象按照您的意愿运行,以便在您的系统中测试不同的案例和执行路径方法。

附带说明,我假设您的记录器是private static final 对象。如果你想让它成为非最终的,你可以用一个模拟记录器替换它,甚至检查在测试期间是否调用了某些日志方法。但也许这对你来说不是那么重要,你不应该过度指定和测试太多。在我的示例中,我将使它成为非最终版本,以便向您展示什么是可能的,因为您似乎是测试自动化的初学者。

回到测试私有方法:由于大多数模拟框架(也是 Spock 的)基于子类化或通过动态代理实现原始类或接口,私有方法对其子类不可见,因此您也不能覆盖/存根一个私有方法。这也是为什么尝试在模拟对象上测试私有方法是一个坏主意的另一个(技术)原因。

让我们假设我们的测试类看起来像这样(请注意,我将这两个方法都设置为包保护以便能够模拟/存根它们):

package de.scrum_master.stackoverflow.q58072937;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.*;

public class SQLExecutor {
  private static /*final*/ Logger log = LoggerFactory.getLogger(SQLExecutor.class);

  /*private*/ void executeDataLoad(String sql) {
    Statement snowflakeStatement = null;
    try {
      snowflakeStatement = getSnowflakeStatement();
      log.info("Importing data into Snowflake");
      int rowsUpdated = snowflakeStatement.executeUpdate(sql);
      log.info("Rows updated/inserted:  " + rowsUpdated);
    } catch (SQLException sqlEx) {
      log.error("Error importing data into Snowflake", sqlEx);
      throw new RuntimeException(sqlEx);
    } finally {
      try {
        if (snowflakeStatement != null)
          snowflakeStatement.close();
      } catch (SQLException sqlEx) {
        log.error("Error closing the statement", sqlEx);
      }
    }
  }

  /*private*/ Statement getSnowflakeStatement() {
     return new Statement() {
       @Override public ResultSet executeQuery(String sql) throws SQLException { return null; }
       @Override public int executeUpdate(String sql) throws SQLException { return 0; }
       @Override public void close() throws SQLException {}
       @Override public int getMaxFieldSize() throws SQLException { return 0; }
       @Override public void setMaxFieldSize(int max) throws SQLException {}
       @Override public int getMaxRows() throws SQLException { return 0; }
       @Override public void setMaxRows(int max) throws SQLException {}
       @Override public void setEscapeProcessing(boolean enable) throws SQLException {}
       @Override public int getQueryTimeout() throws SQLException { return 0; }
       @Override public void setQueryTimeout(int seconds) throws SQLException {}
       @Override public void cancel() throws SQLException {}
       @Override public SQLWarning getWarnings() throws SQLException { return null; }
       @Override public void clearWarnings() throws SQLException {}
       @Override public void setCursorName(String name) throws SQLException {}
       @Override public boolean execute(String sql) throws SQLException { return false; }
       @Override public ResultSet getResultSet() throws SQLException { return null; }
       @Override public int getUpdateCount() throws SQLException { return 0; }
       @Override public boolean getMoreResults() throws SQLException { return false; }
       @Override public void setFetchDirection(int direction) throws SQLException {}
       @Override public int getFetchDirection() throws SQLException { return 0; }
       @Override public void setFetchSize(int rows) throws SQLException {}
       @Override public int getFetchSize() throws SQLException { return 0; }
       @Override public int getResultSetConcurrency() throws SQLException { return 0; }
       @Override public int getResultSetType() throws SQLException { return 0; }
       @Override public void addBatch(String sql) throws SQLException {}
       @Override public void clearBatch() throws SQLException {}
       @Override public int[] executeBatch() throws SQLException { return new int[0]; }
       @Override public Connection getConnection() throws SQLException { return null; }
       @Override public boolean getMoreResults(int current) throws SQLException { return false; }
       @Override public ResultSet getGeneratedKeys() throws SQLException { return null; }
       @Override public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { return 0; }
       @Override public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { return 0; }
       @Override public int executeUpdate(String sql, String[] columnNames) throws SQLException { return 0; }
       @Override public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { return false; }
       @Override public boolean execute(String sql, int[] columnIndexes) throws SQLException { return false; }
       @Override public boolean execute(String sql, String[] columnNames) throws SQLException { return false; }
       @Override public int getResultSetHoldability() throws SQLException { return 0; }
       @Override public boolean isClosed() throws SQLException { return false; }
       @Override public void setPoolable(boolean poolable) throws SQLException {}
       @Override public boolean isPoolable() throws SQLException { return false; }
       @Override public void closeOnCompletion() throws SQLException {}
       @Override public boolean isCloseOnCompletion() throws SQLException { return false; }
       @Override public <T> T unwrap(Class<T> iface) throws SQLException { return null; }
       @Override public boolean isWrapperFor(Class<?> iface) throws SQLException { return false; }
     };
  }
}

然后你可以像这样写一个 Spock 测试:

package de.scrum_master.stackoverflow.q58072937

import org.slf4j.Logger
import spock.lang.Specification

import java.sql.SQLException

class SQLExecutorTest extends Specification {
  def test() {
    given:
    def logger = Mock(Logger)
    def originalLogger = SQLExecutor.log
    SQLExecutor.log = logger
    SQLExecutor sqlExecutor = Spy() {
      getSnowflakeStatement() >> {
        throw new SQLException("uh-oh")
      }
    }

    when:
    sqlExecutor.executeDataLoad("dummy")

    then:
    def exception = thrown RuntimeException
    exception.cause instanceof SQLException
    exception.cause.message == "uh-oh"
    0 * logger.info(*_)
    1 * logger.error(*_)

    cleanup:
    SQLExecutor.log = originalLogger
  }
}

如上所述,对记录器的整个交互测试是可选的,不是回答您的问题所必需的。我这样做只是为了说明什么是可能的。

我也不喜欢我自己的解决方案,因为你需要

  • 为您的被测类使用间谍对象并
  • 了解executeDataLoad(String) 的内部实现,即它调用getSnowflakeStatement() 以便能够存根后一种方法并使其抛出您想要抛出的异常,以覆盖异常处理程序的执行路径。

还请注意,声明 exception.cause.message == "uh-oh" 并不是真正必要的,因为它只是测试模拟。我只是把它放在那里是为了向你展示嘲笑的东西是如何工作的。


现在让我们假设我们重构您的类以使 Statement 可注入:

  /*private*/ void executeDataLoad(String sql, Statement snowflakeStatement) {
    try {
      if (snowflakeStatement == null)
        snowflakeStatement = getSnowflakeStatement();
      log.info("Importing data into Snowflake");
      // (...)

然后您可以将 getSnowflakeStatement() 设为私有(前提是您可以通过另一种公共方法覆盖它)并像这样修改您的测试(删除记录器交互测试以便专注于我正在更改的内容):

package de.scrum_master.stackoverflow.q58072937

import spock.lang.Specification

import java.sql.SQLException
import java.sql.Statement

class SQLExecutorTest extends Specification {
  def test() {
    given:
    def sqlExecutor = new SQLExecutor()
    def statement = Mock(Statement) {
      executeUpdate(_) >> {
        throw new SQLException("uh-oh")
      }
    }

    when:
    sqlExecutor.executeDataLoad("dummy", statement)

    then:
    def exception = thrown RuntimeException
    exception.cause instanceof SQLException
  }
}

看到区别了吗?您不再需要在您的被测类上使用Spy,只需将MockStub 用于您注入的Statement 即可修改其行为。

我可以说和解释更多,但这个答案不能代替测试教程。

【讨论】:

    【解决方案2】:

    finally 删除 try 块中的 null 检查。因为这个空检查,你不能得到任何异常。尝试关闭语句而不检查它。

    private void executeDataLoad(String sql) {
        Statement snowflakeStatement=null;
        try {
    
            snowflakeStatement = getSnowflakeStatement();
            log.info("Importing data into Snowflake");
            int rowsUpdated = snowflakeStatement.executeUpdate(sql);
            log.info("Rows updated/inserted:  " + rowsUpdated);
        }
        catch (SQLException sqlEx) {
            log.error("Error importing data into Snowflake", sqlEx);
            throw new RuntimeException(sqlEx);
        }finally{
            try {
                snowflakeStatement.close();
            } catch (SQLException sqlEx) {
                log.error("Error closing the statement", sqlEx);
            }
        }
    }
    

    【讨论】:

    • 仍然可以从close()获取SQLException。更重要的是,现在可以得到NullPointerException,以防getSnowflakeStatement()SQLException 失败,结果snowflakeStatement 为空。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-16
    • 1970-01-01
    相关资源
    最近更新 更多