Тест: заглушка против реальной реализации - PullRequest
2 голосов
/ 14 марта 2012

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

Предположим, у нас есть этот (псевдо) код:

public class A {
  public int getInt() {
    if (..) {
      return 2;
    }
    else {
      throw new AException();
    }
  }
}

public class B {
  public void doSomething() {
    A a = new A();
    try {
      a.getInt();
    }
    catch(AException e) {
      throw new BException(e);
    }
  }
}


public class UnitTestB {
  @Test
  public void throwsBExceptionWhenFailsToReadInt() {
     // Stub A to throw AException() when getInt is called
     // verify that we get a BException on doSomething()
  }
}

Теперь предположим, что в какой-то момент позже, когда мы напишем еще сотни тестов, мы поймем, что A на самом деле не должен генерировать AException, а вместо этого AOtherException. Мы исправляем это:

public class A {
  public int getInt() {
    if (..) {
      return 2;
    }
    else {
      throw new AOtherException();
    }
  }
}

Теперь мы изменили реализацию A на исключение AOtherException и запустили все наши тесты. Они проходят. Что не так хорошо, так это то, что модульное тестирование на B проходит, но не так. Если мы соберем вместе A и B в производство на этом этапе, B распространит AOtherException, потому что его реализация думает, что A выдает AException.

Если бы вместо этого мы использовали реальную реализацию A для нашего теста throwsBExceptionWhenFailsToReadInt, то он бы потерпел неудачу после изменения A, поскольку B больше не генерировал бы BException.

Это просто пугающая мысль, что если бы у нас была тысяча тестов, структурированных как в приведенном выше примере, и мы изменили одну крошечную вещь, то все модульные тесты по-прежнему работали бы, даже если поведение многих модулей было бы неправильным! Возможно, я что-то упускаю, и я надеюсь, что некоторые из вас, умные люди, могли бы объяснить мне, что это такое.

Ответы [ 5 ]

2 голосов
/ 14 марта 2012

Когда вы говорите

Теперь мы изменили реализацию A на исключение AOtherException и запустили все наши тесты. Они проходят.

Я думаю, что это неправильно. Вы, очевидно, не реализовали свой модульный тест, но класс B не будет перехватывать AException и, следовательно, не генерировать BException, потому что AException теперь является AOtherException. Может быть, я что-то упускаю, но не провалится ли ваш модульный тест, утверждая, что BException выбрасывается в этот момент? Вам нужно будет обновить код класса, чтобы он соответствующим образом обрабатывал тип исключения AOtherException.

0 голосов
/ 06 февраля 2013

Древняя нить, я знаю, но я подумал, что добавлю, что JUnit имеет действительно удобную функцию для обработки исключений. Вместо того, чтобы делать try / catch в своем тесте, скажите JUnit, что вы ожидаете, что класс сгенерирует определенное исключение.

@Test(expected=AOtherException)
public void ensureCorrectExceptionForA {
    A a = new A();
    a.getInt();
}

Расширяя это до вашего класса B, вы можете опустить некоторые из try / catch и позволить инфраструктуре обнаружить правильное использование исключений.

0 голосов
/ 21 марта 2012

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

Тем не менее, этот тип проблемы должен быть частью испытаний - вы не можете отказаться от преимуществ только для этого углового случая.

См. Также: TDD, как обрабатывать изменения в макетируемом объекте - на форумах testdrivendevelopment проводилось аналогичное обсуждение (см. Выше) Цитируя Стива Фримена (известность GOOS и сторонник тестов на основе взаимодействия)

Все это правда. На практике в сочетании с продуманным комбинация тестов более высокого уровня, я не видел, чтобы это было большим проблема. Сначала обычно есть что-то большее.

0 голосов
/ 16 марта 2012

Это нормально, что тест, который вы написали с использованием заглушек, не дает сбоя, поскольку он предназначен для проверки того, что объект B хорошо взаимодействует с A и может обработать ответ от getInt () , предполагая, что getInt () генерирует AException . не предназначено для проверки, действительно ли getInt () действительно генерирует AException в любой точке.

Вы можете назвать такой тест, который вы написали, «тестом на совместную работу».

Теперь вам необходимо выполнить дополнительный тест, который проверяет, будет ли getInt () когда-либо генерировать AException (или AOtherException, если на то пошло) в первую очередь. Это «контрактный тест».

J B Rainsberger представляет отличную презентацию о методике испытаний по контракту и совместной работе.

С этим приемом вы, как правило, решаете проблему «ложного зеленого теста»:

  1. Определите, что getInt () теперь должна генерировать AOtherException, а не AException

  2. Написать тестовый контракт, подтверждающий, что getInt () генерирует AOtherException при определенных обстоятельствах

  3. Введите соответствующий производственный код для прохождения теста

  4. Понимаете, вам нужны тесты совместной работы для этого контрактного теста: может ли каждый сотрудник, использующий getInt (), обработать исключение AOtherException, которое мы собираемся выдать?

  5. Реализовать эти тесты совместной работы (допустим, вы еще не заметили, что тестирование совместной работы для AException уже проверяется на этом этапе).

  6. Напишите производственный код, который соответствует тестам, и поймет, что B уже ожидает AException при вызове getInt () , но не AOtherException.

  7. Обратитесь к существующему тесту совместной работы, содержащему помеченную букву A, генерирующую AException, и осознайте, что она устарела, и вам необходимо удалить ее.

Это если вы начнете использовать эту технику только сейчас, но при условии, что вы приняли ее с самого начала, реальной проблемы не возникнет, поскольку вы, естественно, измените тестовый метод getInt (), чтобы сделать его ожидайте AOtherException и меняйте соответствующие тесты совместной работы сразу после этого (золотое правило заключается в том, что контрактный тест всегда идет с тестом совместной работы, поэтому со временем он становится легким делом).

Если бы мы вместо этого использовали реальную реализацию A для нашего throwsBExceptionWhenFailsToReadInt test, то это не удалось бы после изменения A, потому что B больше не будет генерировать исключение BEx.

Конечно, но это был бы совсем другой вид теста - интеграционный тест, на самом деле. Интеграционный тест проверяет обе стороны монеты: правильно ли обрабатывает объект B ответ R от объекта A, и отвечает ли когда-либо объект A таким образом? Это нормально только для такого теста, когда реализация A, использованная в тесте, начинает отвечать R 'вместо R.

0 голосов
/ 15 марта 2012

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

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

Единственное решение, которое я могу придумать для этого конкретного примера, - это использовать#define в заголовочном файле, чтобы определить тип исключения.Это может привести к путанице, если вам нужно передать параметры в конструктор исключения.

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

...