如何使用具有简单日志语句的 Spock 在 Java 中测试 catch 块



我有一个简单的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 块中具有 throw 关键字的 catch 块。

如何测试以确保执行捕获块?

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

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

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

您的代码还存在实例化其自己的依赖项(在本例中为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,只需对注入的Statement使用MockStub即可修改其行为。

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

最后删除空签入尝试块。由于此空检查,您不会获得任何异常。只是尝试关闭语句而不检查它。

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

最新更新