Простой ответ: Вы не тестируете частные методы напрямую.
Вместо этого, хорошая практика тестирования - это тестирование открытых методов с необходимыми параметрами и внедренными объектами (часто фиктивными объектами).) для того, чтобы охватить все пути выполнения в ваших публичных и приватных методах.Если вы не можете покрыть код закрытого метода, вызывая открытые методы, это признак того, что
- либо ваш класс плохо поддается тестированию, и вам следует провести рефакторинг
- или (часть) вашего частногокод метода недоступен и поэтому должен быть удален
- или, возможно, комбинацией обоих.
Ваш код также страдает от проблемы создания своих собственных зависимостей, в данном случае Statement
объект.Если бы вы могли внедрить его как параметр метода вместо метода, создающего его как локальную переменную, вы могли бы легко внедрить имитатор, заглушку или шпион и заставить этот фиктивный объект вести себя так, как вам хочется, для тестирования различных случаев и путей выполнения в вашемmethod.
В качестве примечания, я предполагаю, что ваш логгер является объектом private static final
.Если вы сделаете его не окончательным, вы можете заменить его на фиктивный регистратор и даже проверить, не вызывались ли определенные методы журнала во время теста.Но, возможно, это не так важно для вас, вам не следует переусердствовать и слишком много тестировать.В моем примере я просто сделаю его не финальным, чтобы показать вам, что возможно, поскольку вы, кажется, новичок в автоматизации тестирования.
Возвращаясь к тестированию частных методов: как и большинство фальшивых фреймворков (также Спока)основаны на создании подклассов или реализации исходных классов или интерфейсов через динамические прокси, и частные методы не видны их подклассам, вы также не можете переписать / заглушить поведение частного метода.Это еще одна (техническая) причина, по которой попытка тестирования закрытых методов на фиктивных объектах является плохой идеей.
Предположим, что наш тестируемый класс выглядит следующим образом (обратите внимание, что я сделал оба метода защищенными пакетами, поэтомучтобы можно было их издеваться / заглушки):
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; }
};
}
}
Тогда вы можете написать тест Спока, например:
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
в тестируемом классе, и вы можете просто использовать Mock
или Stub
для Statement
, который вы вводите, чтобы изменить его поведение.
Я мог бы сказать и объяснить больше, но этот ответ не может заменить учебное руководство.