Модульное тестирование пустых методов?


170

Каков наилучший способ модульного тестирования метода, который ничего не возвращает? Конкретно в с #.

Что я действительно пытаюсь проверить, так это метод, который берет файл журнала и анализирует его для конкретных строк. Строки затем вставляются в базу данных. Ничего из того, что не было сделано раньше, но, будучи ОЧЕНЬ новым для TDD, я задаюсь вопросом, возможно ли это проверить или это то, что на самом деле не было проверено.


55
Пожалуйста, не используйте термин «TDD», если это не то, что вы делаете. Вы делаете модульное тестирование, а не TDD. Если бы вы занимались TDD, у вас никогда не возникало бы вопроса типа «как проверить метод». Сначала будет существовать тест, а затем возникает вопрос: «Как пройти этот тест?» Но если вы работали с TDD, ваш код был бы написан для теста (а не наоборот), и вы, по сути, в конечном итоге ответили на свой вопрос. Ваш код будет отформатирован по-разному в результате TDD, и эта проблема никогда не возникнет. Просто уточняю.
Суамере

Ответы:


151

Если метод ничего не возвращает, это либо одно из следующих

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

Обязательные методы - вы можете проверить, действительно ли задание было выполнено. Проверьте, действительно ли произошло изменение состояния. например

void DeductFromBalance( dAmount ) 

можно проверить, проверив, действительно ли баланс после отправки этого сообщения меньше первоначального значения по dAmount

Информационные методы - редко встречаются в качестве члена открытого интерфейса объекта ... и, следовательно, обычно не тестируются модулем. Однако, если необходимо, Вы можете проверить, выполняется ли обработка уведомления. например

void OnAccountDebit( dAmount )  // emails account holder with info

можно проверить, проверив, отправляется ли электронное письмо

Опубликуйте более подробную информацию о вашем фактическом методе, и люди смогут ответить лучше.
Обновление : ваш метод делает 2 вещи. Я бы на самом деле разделил его на два метода, которые теперь можно независимо протестировать.

string[] ExamineLogFileForX( string sFileName );
void InsertStringsIntoDatabase( string[] );

String [] можно легко проверить, предоставив первому методу фиктивный файл и ожидаемые строки. Второй вариант немного сложен. Вы можете использовать Mock (google или search stackflowflow на платформах mocking), чтобы имитировать БД, или поразить реальную БД и убедиться, что строки были вставлены в нужное место. Проверьте эту ветку на наличие хороших книг ... Я бы порекомендовал прагматическое модульное тестирование, если вы в затруднении.
В коде это будет использоваться как

InsertStringsIntoDatabase( ExamineLogFileForX( "c:\OMG.log" ) );

1
эй гишу, хороший ответ. Пример, который вы привели ... разве они не являются интеграционными тестами ...? и если да, то остается вопрос, как действительно проверить Void Methods .... может быть, это невозможно?
Энди

2
@andy - зависит от вашего определения «интеграционных тестов». Императивные методы обычно меняют состояние, поэтому вы можете проверить его с помощью модульного теста, который запрашивает состояние объекта. Информационные методы могут быть проверены модульным тестом, который включает ложного слушателя / сотрудника, чтобы гарантировать, что испытуемый выдает правильное уведомление. Я думаю, что оба могут быть разумно проверены с помощью модульных тестов.
Гишу

@ и базу данных можно смоделировать / разделить с помощью интерфейса доступа, что позволяет проверять действие по данным, передаваемым в фиктивный объект.
Питер Гейгер

62

Проверьте его побочные эффекты. Это включает:

  • Это бросает какие-либо исключения? (Если это так, убедитесь, что это так. Если это не так, попробуйте некоторые угловые случаи, которые могут быть, если вы не будете осторожны - нулевые аргументы являются наиболее очевидной вещью.)
  • Хорошо ли он играет со своими параметрами? (Если они изменчивы, мутирует ли он, когда не должен, и наоборот?)
  • Влияет ли это правильно на состояние объекта / типа, к которому вы его вызываете?

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


31

Как всегда: проверьте, что метод должен делать!

Должно ли это изменить глобальное состояние (ну, запах кода!) Где-нибудь?

Должен ли он позвонить в интерфейс?

Должно ли оно вызывать исключение при вызове с неправильными параметрами?

Должен ли он вызывать исключения при вызове с правильными параметрами?

Должно ли это ...?


11

Пустые возвращаемые типы / подпрограммы - старые новости. Я не делал возврат типа Void (если только я не был очень ленивым) в течение примерно 8 лет (со времени этого ответа, так что чуть раньше, чем этот вопрос был задан).

Вместо такого метода, как:

public void SendEmailToCustomer()

Создайте метод, который следует парадигме Microsoft int.TryParse ():

public bool TrySendEmailToCustomer()

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

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

public StateEnum TrySendEmailToCustomer()

Тем не менее, хотя Try-Paradigm в некоторой степени отвечает на этот вопрос о том, как тестировать возврат void, существуют и другие соображения. Например, во время / после цикла «TDD» вы выполняете «Рефакторинг» и замечаете, что вы делаете две вещи с помощью вашего метода ... тем самым нарушая «Принцип единой ответственности». Так что об этом нужно позаботиться в первую очередь. Во-вторых, вы могли идентифицировать зависимость ... вы касаетесь "постоянных" данных.

Если вы делаете доступ к данным в рассматриваемом методе, вам необходимо выполнить рефакторинг в n-уровневую или n-слойную архитектуру. Но мы можем предположить, что когда вы говорите «Строки затем вставляются в базу данных», вы на самом деле имеете в виду, что вызываете слой бизнес-логики или что-то в этом роде. Я, мы будем считать это.

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

public <Constructor/MethodName> (IBusinessDataEtc otherLayerOrTierObject, string[] stuffToInsert)

Теперь, когда вы можете принять интерфейс вашего объекта уровня бизнес / данных, вы можете смоделировать его во время модульных тестов и не иметь никаких зависимостей или боязни "случайного" интеграционного тестирования.

Таким образом, в вашем живом коде вы передаете IBusinessDataEtcобъект REAL . Но в вашем модульном тестировании вы передаете IBusinessDataEtcобъект MOCK . В этом макете вы можете включить неинтерфейсные свойства, такие как int XMethodWasCalledCountили что-то, чьи состояния обновляются при вызове методов интерфейса.

Таким образом, ваш модульный тест пройдет ваш метод (ы) в вопросе, выполнит любую логику и вызовет один или два или выбранный набор методов в вашем IBusinessDataEtcобъекте. Когда вы делаете свои утверждения в конце вашего модульного теста, у вас есть несколько вещей, чтобы проверить сейчас.

  1. Состояние подпрограммы, которая теперь является методом Try-Paradigm.
  2. Состояние вашего фиктивного IBusinessDataEtcобъекта.

Для получения дополнительной информации об идеях внедрения зависимостей на уровне конструкции ... как они относятся к модульному тестированию ... посмотрите шаблоны проектирования Builder. Он добавляет еще один интерфейс и класс для каждого текущего интерфейса / класса, который у вас есть, но они очень маленькие и обеспечивают ОГРОМНОЕ увеличение функциональности для лучшего модульного тестирования.


Первая часть этого замечательного ответа служит замечательным общим советом для всех начинающих / средних программистов.
pimbrouwers

не должно ли это быть что-то вроде public void sendEmailToCustomer() throws UndeliveredMailException?
А.Эмад

1
@ A.Emad Хороший вопрос, но нет. Управление потоком вашего кода с использованием исключений является давно известной плохой практикой. Хотя в voidметодах, особенно в объектно-ориентированных языках, это был единственный реальный вариант. Самая старая альтернатива от Microsoft - это парадигма Try-Paradigm, которую я обсуждаю, и парадигмы функционального стиля, такие как Monads / Maybes. Таким образом, Команды (в CQS) могут по-прежнему возвращать ценную информацию о состоянии вместо того, чтобы полагаться на бросок, который похож на GOTO(что, как мы знаем, плохо). броски (и переходы) медленны, трудны для отладки и не являются хорошей практикой.
Суамер

Спасибо за разъяснение этого. Это специфично для C # или вообще плохая практика - генерировать исключения даже в таких языках, как Java и C ++?
А.Эмад

9

Попробуй это:

[TestMethod]
public void TestSomething()
{
    try
    {
        YourMethodCall();
        Assert.IsTrue(true);
    }
    catch {
        Assert.IsTrue(false);
    }
}

1
Это не должно быть необходимым, но может быть сделано как таковое
Натан Алард

Добро пожаловать в StackOverflow! Пожалуйста, рассмотрите возможность добавления пояснения к вашему коду. Спасибо.
Аурасфера

2
Это ExpectedAttributeсделано для того, чтобы сделать этот тест более четким.
Мартин Ливержеж,

8

Вы даже можете попробовать это так:

[TestMethod]
public void ReadFiles()
{
    try
    {
        Read();
        return; // indicates success
    }
    catch (Exception ex)
    {
        Assert.Fail(ex.Message);
    }
}

1
Я полагаю, это самый простой способ.
Навин Пандит

5

это будет иметь некоторый эффект на объект .... запрос на результат эффекта. Если он не имеет видимого эффекта, его не стоит тестировать!


4

Предположительно метод что-то делает, а не просто возвращает?

Если предположить, что это так, то:

  1. Если он изменяет состояние объекта-владельца, вам следует проверить, что состояние изменилось правильно.
  2. Если он принимает какой-либо объект в качестве параметра и модифицирует этот объект, то следует проверить, что объект правильно изменен.
  3. Если он генерирует исключения в определенных случаях, проверьте, правильно ли выбрасываются эти исключения.
  4. Если его поведение варьируется в зависимости от состояния его собственного объекта или какого-либо другого объекта, задайте его состояние и проверьте, что метод имеет правильные значения через один из трех методов проверки, описанных выше).

Если вы дадите нам знать, что делает метод, я мог бы быть более конкретным.


3

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


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

2

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


Согласен - проверка поведения макетов, тестирующих метод, будет одним из способов.
Джефф Шумахер

0

Какой бы экземпляр вы ни использовали для вызова метода void, вы можете просто использовать,Verfiy

Например:

В моем случае _Logэто экземпляр и LogMessageтестируемый метод:

try
{
    this._log.Verify(x => x.LogMessage(Logger.WillisLogLevel.Info, Logger.WillisLogger.Usage, "Created the Student with name as"), "Failure");
}
Catch 
{
    Assert.IsFalse(ex is Moq.MockException);
}

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.