Как проверить блок catch в Java с помощью Spock, который имеет простую инструкцию log - PullRequest
0 голосов
/ 24 сентября 2019

У меня есть простой 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 выполнен?

Ответы [ 2 ]

1 голос
/ 24 сентября 2019

Простой ответ: Вы не тестируете частные методы напрямую.

Вместо этого, хорошая практика тестирования - это тестирование открытых методов с необходимыми параметрами и внедренными объектами (часто фиктивными объектами).) для того, чтобы охватить все пути выполнения в ваших публичных и приватных методах.Если вы не можете покрыть код закрытого метода, вызывая открытые методы, это признак того, что

  • либо ваш класс плохо поддается тестированию, и вам следует провести рефакторинг
  • или (часть) вашего частногокод метода недоступен и поэтому должен быть удален
  • или, возможно, комбинацией обоих.

Ваш код также страдает от проблемы создания своих собственных зависимостей, в данном случае 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, который вы вводите, чтобы изменить его поведение.

Я мог бы сказать и объяснить больше, но этот ответ не может заменить учебное руководство.

0 голосов
/ 24 сентября 2019

Наконец, удалите пустую проверку в блоке try.Из-за этой нулевой проверки вы не можете получить никаких исключений.Просто попробуйте закрыть оператор, не проверяя его.

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