Как сохранить мои юнит-тесты СУХИМЫМИ, когда издевательство не работает? - PullRequest
6 голосов
/ 02 декабря 2011

Изменить:

Кажется, пытаясь найти решение своей проблемы, я размыл всю проблему. Поэтому я немного изменяю вопрос.

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

public class ProtocolMessage : IMessage
{
    public IHeader GetProtocolHeader(string name)
    {
        // Do some logic here including returning null 
        // and throw exception in some cases

        return header;
    }

    public string GetProtocolHeaderValue(string name)
    {
        IHeader header = GetProtocolHeader(name);

        // Do some logic here including returning null
        // and throw exception in some cases

        return value;
    }
}

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

Если GetProtocolHeaderValue будет зависеть от внешней зависимости, я смогу смоделировать ее и внедрить (я использую Moq + NUnit). Тогда мой модульный тест просто протестировал бы ожидание того, что была вызвана внешняя зависимость, и вернул ожидаемое значение. Внешняя зависимость была бы проверена его собственным модульным тестом, и я бы сделал, но как правильно поступить в этом примере, где метод не является внешней зависимостью?

Разъяснение проблемы:

Я считаю, что мой набор тестов для GetProtocolHeaderValue должен тестировать ситуацию, когда GetProtocolHeader возвращает заголовок, ноль или исключение. Итак, главный вопрос: стоит ли писать тесты, в которых действительно будет выполняться GetProtocolHeader (некоторые тесты будут продублированы, потому что они будут тестировать тот же код, что и тесты для самого GetProtocolHeader), или мне следует использовать метод имитации, описанный @adrift и @ Эрик Николсон, где я не буду запускать настоящий GetProtoclHeader, а просто сконфигурирую mock для возврата заголовка, нуля или исключения при вызове этого метода?

Ответы [ 7 ]

4 голосов
/ 02 декабря 2011

При звонке на GetProtocolHeaderValue вам действительно нужно знать, звонил он или нет GetProtocolHeader?

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

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

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

Пока вы создаете свои тесты и тестируете данные таким образом, чтобы гарантировать, что дублирующиеся заголовки не маскируют ошибки, все должно быть хорошо.

ПРАВКА для обновленного вопроса:

  1. Если GetProtocolHeader работает быстро, надежно и идемпотентно, то я все еще верю, что нет надобности его издеваться. Недостаток в любом из этих трех аспектов является (ИМО) основной причиной насмешек.

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

  2. Одной из ролей, выполняемых хорошими юнит-тестами, является документация .

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

  3. Издевательства могут скрывать потенциальные ошибки.

    Допустим, GetProtocolHeader выдает исключение, если имя пустое. Вы соответственно создаете макет и гарантируете, что GetProtocolHeaderValue обрабатывает это исключение соответствующим образом. Позже вы решаете, что GetProtocolHeader должен вернуть null для пустого имени. Если вы забудете обновить свой макет, GetProtocolHeaderValue("") теперь будет вести себя иначе в реальной жизни по сравнению с набором тестов.

  4. Насмешка может дать преимущество, если имитация менее многословна, чем установка, но сначала уделите должное внимание вышеуказанным пунктам.

    Хотя вы даете три различных ответа GetProtocolHeader (заголовок, ноль или исключение), которые GetProtocolHeaderValue необходимо проверить, я думаю, что первый из них, вероятно, будет "диапазоном заголовков". (Например, что он делает с присутствующим, но пустым заголовком? Как он обрабатывает начальные и конечные пробелы? Как насчет символов, не входящих в ASCII? Числа?). Если настройки для всех из них исключительно многословны, может быть, лучше подделать.

3 голосов
/ 02 декабря 2011

Я часто использую частичный макет (в Rhino) или его эквивалент (например, CallsBaseMethod в FakeItEasy), чтобы смоделировать реальный класс, который я тестирую. Затем вы можете сделать GetProtocolHeader виртуальным и высмеивать ваши звонки. Можно утверждать, что это нарушает принцип единственной ответственности, но это все еще явно очень сплоченный кодекс.

В качестве альтернативы вы можете создать метод, подобный

internal static string GetProtocolHeaderValue(string name, IHeader header )

и протестируйте эту обработку самостоятельно. Публичный метод GetProtocolHeaderValue не будет иметь / много тестов.

Редактировать: В этом конкретном случае я бы также рассмотрел возможность добавления GetValue () в качестве метода расширения для IHeader. Это было бы очень легко прочитать, и вы все равно могли бы сделать проверку нуля.

1 голос
/ 03 января 2012

Все дело в суждениях, чистые негодяи скажут, что не издеваются, насмешники будут все издеваться.

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

Проблема здесь заключается во втором методе GetProtocolHeaderValue, который не имеет возможности использовать существующий экземпляр IHeader

Я бы предложил GetValue (имя строки) в интерфейсе IHeader

1 голос
/ 02 декабря 2011

Почему бы просто не изменить GetProtocolHeaderValue (имя строки), чтобы он вызывал 2 метода, второй из которых принимает IHeader?Таким образом, вы можете протестировать все // сделать некоторую логическую часть в отдельном тесте с помощью метода RetrieveHeaderValue, не беспокоясь о Mocks.Примерно так:

public string GetProtocolHeaderValue(string name)
{
   IHeader header = GetProtocolHeader(name);
   return RetrieveHeaderValue(IHeader header);
}

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

Следуя той же мысли, эти методы могут быть извлечены в интерфейсе IHeaderParser, а методы GetProtocol будут принимать IHeaderParser, что будет тривиально для test / mock.

public string GetProtocolHeaderValue(string name, IHeaderParser parser)
{
    IHeader header = parser.GetProtocolHeader(name);
    return parser.HeaderValue(IHeader header);
}
1 голос
/ 02 декабря 2011

С Moq вы можете использовать CallBase, чтобы сделать эквивалент частичного макета в Rhino Mocks.

В вашем примере измените GetProtocolHeader на virtual, затем создайте макет ProtocolMessage, установив CallBase = true. Затем вы можете настроить метод GetProtocolHeader по своему желанию, и у вас будет вызываться функциональность базового класса GetProtocolHeaderValue.

См. Настройка Mock Behavior в разделе быстрого запуска moq для получения более подробной информации.

1 голос
/ 02 декабря 2011

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

Существуют две возможности:

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

В любом случае вы тестируете открытую функциональность, и в конце дня это все, что имеет значение. Если бы это была зависимость , тогда да, вы могли бы беспокоиться о том, был ли он вызван, но если это не так, безусловно, это деталь реализации и не имеет значения?

0 голосов
/ 06 декабря 2011

Попробуйте самое простое, что может сработать.

  • Если реальная реализация GetProtocolHeader () является быстрой и простой в управлении (например, для имитации случаев заголовка, нуля и исключений), просто используйте ее.
  • Если нет (т. Е. Либо реальная реализация требует много времени, либо вы можете легко смоделировать 3 случая), то посмотрите на изменение дизайна так, чтобы ограничения были ослаблены.

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

...