Должен ли я имитировать вызов локального метода внутри тестируемого метода?

Мой вопрос о концепции модульного тестирования:

class A {
   public void m1() {
      // code
      m2();
      //code
   }

   public void m2() {
      //some code
   }

}

Согласно рекомендациям, как мне протестировать метод m1? Модуль — это класс или модуль — это метод?

Мое мнение: я должен тестировать m2 отдельно и не тестировать интеграцию m1 и m2.

Использовать мой взгляд достаточно сложно - я должен использовать сложные фреймворки для тестирования и использовать очень современные вещи.

Согласно моему пониманию модульного тестирования, тесты должны быть простыми! Если ваш код хорош, вы можете протестировать его без сложных вещей. Но вызов m2() внутри m1() является нормальным кодом.

Пожалуйста, разъясните мое недоразумение.

Обновить:

насмешливый пример (псевдокод):

//prepare
A testClass = mock(A.class);
when(testClass.m2()).doNothing();
when(testClass.m1()).callRealMethod();
//execute:
testClass.m1();
//verify
check testClass state after method m1 invocation.

Вот как я тестирую издевательский класс. Это нормально?


person gstackoverflow    schedule 14.05.2014    source источник


Ответы (4)


Во-первых, при модульном тестировании проверьте все общедоступные методы. В вашем примере m1 и m2 общедоступны, поэтому у вас будут тесты для обоих.

Есть несколько причин, по которым вы можете заглушить или имитировать m2:

  1. Если при тестировании m1 у вас возникнут проблемы, потому что m1 вызывает m2, заглушку или имитацию m2. Некоторые проблемы, с которыми вы можете столкнуться:

    • m2 might call external services
    • m2 может быть просто медленным
    • может быть сложно вызвать m1 с параметрами, удовлетворяющими m2 (ваш m2 не имеет параметров, но я говорю в общем)
  2. Иногда, когда вы тестируете метод, который вызывает другой метод, а также тестируете другой метод, вы обнаруживаете дублирование между тестами двух методов — некоторые тесты вызывающего метода на самом деле проверяют вызываемый метод. Справьтесь с этим, заглушив или имитируя вызываемый метод из вызывающего метода, проверяя вызывающий метод ровно настолько, чтобы доказать, что вызываемый метод вызывается, и тщательно проверяя вызываемый метод.

  3. Если вы используете TDD, вы можете написать m1 перед тем, как написать m2. Затем вы должны заглушить или имитировать m2, чтобы ваши тесты m1 прошли, а затем перейти к тестированию и написать m2.

Но если у вас нет причин оскорблять или издеваться над m2, не делайте этого. Для метода является обычным и разумным вызывать другие методы способами, которые не требуют заглушки или имитации. Вызываемые методы могут быть короткими и простыми, или вызывающий метод может быть просто разбит на несколько вспомогательных методов для удобства чтения. Это верно даже в том случае, если вызываемый метод находится в другом классе (поскольку он используется более чем одним вызывающим методом); если он полностью протестирован тестами методов, которые его вызывают, и если между тестами нет дублирования, нет необходимости заглушать или имитировать.

Приведенный выше пример имитации m2 без запуска m1 — это совершенно нормальная вещь, которую нужно сделать, и она выполняет свою работу, но это уродливо, поскольку Mockito берет на себя все методы класса. Вы можете сделать это лучше с помощью шпиона Mockito, обсуждаемого здесь: В чем разница между насмешкой и шпионажем при использовании Мокито?.

A a = spy(new A());
doNothing().when(spy).m2();
a.m1();
# check state of a
person Dave Schweisguth    schedule 03.06.2014
comment
Чтобы издеваться над методом m2 - я должен издеваться над объектом. Следовательно, я вынужден проверить вызов метода издевательства над объектом (я о методе m1). Это нормально? - person gstackoverflow; 05.06.2014

Вы все равно должны протестировать m1().

Вы можете использовать макеты метода m2(),

person user3636269    schedule 14.05.2014
comment
В этом случае я должен издеваться над A и после теста. - person gstackoverflow; 14.05.2014

Термин модульный тест происходит от слова "единица работы". Отдельная единица работы часто, но не всегда, представляет собой один метод. На практике модульное тестирование часто относится к тестированию одного класса (сравните SRP) и противопоставляет интеграционные и приемочные тесты. . Фреймворк JUnit происходит от модульного тестирования, но в настоящее время используется для всех видов и уровней тестов.

Как правило, вы должны тестировать все общедоступные методы. Если хотите, включите отрицательные тесты, которые передают вашим методам неверные данные. Обычно они выявляют слабые места кода. Если два общедоступных метода перекрываются - как в вашем случае - я не знаю о жестком принципе использования все время. Вы могли

  • A) Используйте Mockito или аналогичный mock m2() во время доступа из m1().
  • B) Протестируйте m1() и m2() обычным способом.

A) дает вам тест, более чувствительный к коду m1, но более дорогой. Выберите этот вариант, если код m1 важен как отдельная единица работы. B) это более быстрый результат и, на мой взгляд, лучший вариант, так как в случае ошибки m1 вы сразу увидите, есть ли ошибка и в m2. Возможно, оставьте небольшой приятный комментарий для других программистов, объясняющий зависимость.

person LastFreeNickname    schedule 14.05.2014
comment
В этом случае я должен издеваться над классом A, сказать doNothing для m2, а затем сказать call realMethod для m1. Обычно я тестировал только реальный объект с внедренными фиктивными объектами. Нормально ли тестировать mock ? - person gstackoverflow; 14.05.2014
comment
Чаще всего издеваются над целыми классами, а не над методами. Как я уже сказал, вы должны решить, стоит ли в этом особом случае стоит пройти специальный тест только для m1. - person LastFreeNickname; 15.05.2014
comment
Я о методе тестирования для m1 - person gstackoverflow; 15.05.2014
comment
Я не уверен, что понял ваш последний вопрос. Протестируйте каждый общедоступный метод. То есть m1 и m2. В m1 вы можете либо смоделировать m2, либо оставить его как есть. Подробности я объяснил в последнем абзаце. - person LastFreeNickname; 15.05.2014

Представьте на мгновение, что m1 не зависит от m2.

Теперь учтите, что m1 не вызывал m2, а дублировал код m2 внутри себя.

(В любом из приведенных выше сценариев вы бы протестировали и m1, и m2, верно?)

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

Я хочу сказать, что зависимость m1 от m2 является частной деталью реализации. Обычно вы хотите скрыть детали реализации от своих тестов, чтобы они не стали хрупкими. Следовательно, вы должны писать свои тесты так, как если бы они не знали об этой зависимости.

Изменить: добавлен код для демонстрации.

Представьте, что мы написали следующий код и тесты (извините за любые синтаксические ошибки, у меня нет под рукой компилятора):

interface Foo {
    public void doStuff(int value);
}

interface Bar {
    public void doOtherStuff();
}

class MyClass {
    private Foo foo;
    private Bar bar;

    public MyClass(Foo foo, Bar bar) {
        this.foo = foo;
        this.bar = bar;
    }

    public void m1() {
        foo.doStuff(42);
        foo.doOtherStuff();
    }

    public void m2() {
        foo.doStuff(42);
    }
}

@Test
public void m1ShouldDoALotOfStuff() throws Exception {
    Foo foo = PowerMockito.mock(Foo.class);
    Bar bar = PowerMockito.mock(Bar.class);
    MyClass sut = new MyClass(foo, bar);

    sut.m1();

    verify(foo).doStuff(42);
    verify(bar).doOtherStuff();
}

@Test
public void m2ShouldJustDoStuff() throws Exception {
    Foo foo = PowerMockito.mock(Foo.class);
    Bar bar = PowerMockito.mock(Bar.class);
    MyClass sut = new MyClass(foo, bar);

    sut.m2();

    verify(foo).doStuff(42);
}

В приведенном выше коде у нас есть зеленые тесты для m1 и m2. Теперь мы замечаем, что в m1 и m2 есть дублирующийся код: foo.doStuff(42);. Итак, мы рефакторим m1, чтобы избавиться от дублирования:

    public void m1() {
        m2();
        foo.doOtherStuff();
    }

Наши тесты все еще зеленые после внесения этого изменения, потому что мы не изменили поведение m1; мы только изменили детали того, как он выполняет это поведение. Чтобы быть надежными, наши тесты должны проверять, что мы ожидаем от ТУС, не проверяя, как он это делает.

person Lilshieste    schedule 14.05.2014
comment
Я обновил свой ответ, включив в него некоторый код, чтобы продемонстрировать, о чем я говорю. (Обратите внимание, что я имитирую только внешние зависимости, а не внутренние.) - person Lilshieste; 14.05.2014