Как я могу модульно протестировать класс, который использует SimpleJdbcCall

У меня есть класс, который выглядит так:

public class MyClass {

    private final SimpleJdbcCall simpleJdbcCall;

    public MyClass(final DataSource dataSource) {
        this(new JdbcTemplate(dataSource));
    }

    public MyClass(final JdbcTemplate template) {
        simpleJdbcCall = new SimpleJdbcCall(template)
            .withoutProcedureColumnMetaDataAccess()
            .withCatalogName("MY_ORACLE_PACKAGE")
            .withFunctionName("GET_VALUE")
            .withReturnValue()
            .declareParameters(
                new SqlOutParameter("RESULT", Types.VARCHAR))
            .declareParameters(
                new SqlParameter("P_VAR1_NAME", Types.VARCHAR))
            .declareParameters(
                new SqlParameter("P_VAR2_NAME", Types.VARCHAR))
            .useInParameterNames("P_VAR1_NAME", "P_VAR2_NAME");
    }

    private String getValue(final String input) {
        final SqlParameterSource params = new MapSqlParameterSource()
            .addValue("P_VAR1_NAME", input, Types.VARCHAR)
            .addValue("P_VAR2_NAME", null, Types.VARCHAR);
        return simpleJdbcCall.executeFunction(String.class, params);
    }
}

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

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

Я хотел бы продолжать использовать этот API SimpleJdbcCall. Он пишет SQL за меня, поэтому мне не нужно смешивать SQL и Java, но я также очень хотел бы протестировать эту штуку, не написав 1000 строк кода. Может ли кто-нибудь увидеть хороший способ проверить это?


person Matt Malone    schedule 19.10.2015    source источник


Ответы (5)


Я тоже предпочитаю не вводить 15 разных SimpleJdbcCalls в свой репозиторий, поэтому я кусаю пулю и добавляю это в свой метод настройки теста:

DatabaseMetaData metaData = mock(DatabaseMetaData.class);
Connection con = mock(Connection.class);
when(con.getMetaData()).thenReturn(metaData);
DataSource ds = mock(DataSource.class);
when(ds.getConnection()).thenReturn(con);
jdbcTemplate = mock(JdbcTemplate.class);
when(jdbcTemplate.getDataSource()).thenReturn(ds);
person Jeff E    schedule 30.01.2019

Я бы определенно выбрал подход добавления конструктора, чтобы позволить вводить SimpleJdbcCall напрямую.

MyClass(SimpleJdbcCall simpleJdbcCall) {
  this.simpleJdbcCall = simpleJdbcCall;
}

(и, вероятно, вызвать этот конструктор из того, который в настоящее время вызывает new).

Это не "хакерство", это просто внедрение зависимостей. Я бы сказал, что создание тестируемого класса без необходимости проверки работы SimpleJdbcCall является определенным улучшением.

Вызов new в конструкторе усложняет тестирование, потому что это жесткая статическая связь с создаваемым классом.

Я нашел запись в блоге Мишко Хевери об этом тема очень интересная.

person Andy Turner    schedule 19.10.2015
comment
DI действительно предназначен для настраиваемых зависимостей и не подходит для замены объектов с состоянием, таких как SimpleJdbcCall. И такую ​​зависимость можно просто смоделировать, так что код уже легко тестируется. - person Rogério; 21.10.2015
comment
подобную зависимость можно просто высмеивать. Как? (Реальный вопрос) - person Andy Turner; 21.10.2015
comment
Обратите внимание, что вы можете сохранить конфигурацию SimpleJdbcCall внутри конструктора; просто позвольте тесту создать экземпляр снаружи, чтобы тест мог переопределить executeFunction(). - person Aaron Digulla; 21.10.2015
comment
Используя библиотеку mocking (в отличие от более простой библиотеки фиктивных объектов). Для Java у нас есть PowerMockito и JMockit. - person Rogério; 21.10.2015
comment
@Rogério, похоже, у вас есть правильный ответ, не могли бы вы написать его более подробно? - person Andy Turner; 21.10.2015
comment
Я добавил пример теста в свой ответ. - person Rogério; 22.10.2015

Добавьте дополнительный конструктор:

/*test*/ MyClass(final SimpleJdbcCall call) {
    simpleJdbcCall = call
        .withoutProcedureColumnMetaDataAccess()
        .withCatalogName("MY_ORACLE_PACKAGE")
        .withFunctionName("GET_VALUE")
        .withReturnValue()
        .declareParameters(
            new SqlOutParameter("RESULT", Types.VARCHAR))
        .declareParameters(
            new SqlParameter("P_VAR1_NAME", Types.VARCHAR))
        .declareParameters(
            new SqlParameter("P_VAR2_NAME", Types.VARCHAR))
        .useInParameterNames("P_VAR1_NAME", "P_VAR2_NAME");
}

Это частный пакет, поэтому другие классы в том же пакете (= тесты) могут вызывать его. Таким образом, тест может создать экземпляр с переопределением executeFunction(). Вы можете возвращать поддельные результаты в методе или проверять состояние объекта.

Это означает, что ваш код по-прежнему настраивает объект; тест просто проходит «POJO», который заполняет тестируемый код.

Таким образом, вам не нужно писать много кода — реализация по умолчанию делает большую часть работы за вас.

В качестве альтернативы разрешите вызывать конструктор с интерфейсом SimpleJdbcCallOperations, что означает, что вам нужна мощная платформа для имитации или вы пишете много шаблонного кода.

Другие альтернативы: используйте фиктивный драйвер JDBC. Обычно их сложно настроить, они вызывают ложные сбои теста, когда тест терпит неудачу, вы часто не знаете, почему, ...

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

Вот почему я стараюсь по возможности избегать прохождения слоя JDBC туда и обратно. Предположим, что JDBC и база данных работают — этот код тестировали другие люди. Если вы сделаете это снова, вы просто потеряете время.

Связанный:

person Aaron Digulla    schedule 21.10.2015

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

В противном случае вы можете смоделировать класс SimpleJdbcCall с тестируемым кодом как есть, используя PowerMockito или JMockit.

Пример теста с JMockit:

@Mocked DataSource ds;
@Mocked SimpleJdbcCall dbCall;

@Test
public void verifyDbCall() {
    String value = new MyClass(ds).getValue("some input");

    // assert on value

    new Verifications() {{
        SqlParameterSource params;
        dbCall.executeFunction(String.class, params = withCapture());
        // JUnit asserts on `params`
    }};
}
person Rogério    schedule 21.10.2015
comment
Общий комментарий: Интеграционные тесты часто представляют собой запах кода (не знаю, что они делают). Если бы они знали, они могли бы написать простой, небольшой и эффективный модульный тест. Поскольку они этого не делают, они просто тестируют что-то и много, но, в конце концов, никто не может сказать, что тестируется, а что нет. - person Aaron Digulla; 22.10.2015
comment
Я должен заявить, что не знаком с JMockit: как dbCall в вашем примере связано с MyClass? - person Andy Turner; 22.10.2015
comment
@AaronDigulla Это абсурд. Никто не выступает за выполнение только модульных тестов, как вы предлагаете, поскольку они не очень хороши в поиске ошибок. Общий консенсус, AFAIK, состоит в том, чтобы иметь оба модульные тесты и какие-то интеграционные тесты. Хотя лично я предпочитаю иметь только интеграционные тесты, так как это очень хорошо сработало для меня в нескольких проектах. - person Rogério; 22.10.2015
comment
@AndyTurner Экземпляр dbCall, назначенный фиктивному полю, является представителем всех SimpleJdbcCall экземпляров, использованных во время теста; как таковой, его можно использовать при записи и/или проверке ожиданий, которые будут соответствовать вызовам в других экземплярах. - person Rogério; 22.10.2015
comment
@Rogério: У разных людей разный опыт. Я видел множество проектов, в которых производственные данные копировались в тестовые, и руководство считало, что этого достаточно. Поэтому я пытаюсь предупредить людей, что вам нужно понимать, что вы делаете; просто запустить много тестов на большом количестве данных недостаточно. - person Aaron Digulla; 22.10.2015

Я сделал это, используя http://www.jmock.org/

XML-конфигурация -

<bean id="simpleJDBCCall" class="org.springframework.jdbc.core.simple.SimpleJdbcCall">
    <property name="jdbcTemplate" ref="jdbcTemplate" />
</bean>

Java-файл -

@Autowired
private SimpleJdbcCall jdbcCall;

Класс испытаний -

simpleJDBCCall = mockingContext.mock(SimpleJdbcCall.class);
mockingContext.checking(new Expectations() {
        { 
            oneOf(simpleJDBCCall).withSchemaName("test");
            will(returnValue(simpleJDBCCall));
            oneOf(simpleJDBCCall).withCatalogName("test");
            will(returnValue(simpleJDBCCall));
            oneOf(simpleJDBCCall).withProcedureName(ProcedureNames.TEST);
            will(returnValue(simpleJDBCCall));
            oneOf(simpleJDBCCall).execute(5);
            will(returnValue(testMap));
        }
person Java_Alert    schedule 08.11.2016