Модульное тестирование и дизайн кода - PullRequest
5 голосов
/ 02 апреля 2012

Во многих руководствах по TDD я вижу такой код:

public class MyClass
{
    public void DoSomething(string Data)
    {
      if (String.IsNullOrWhiteSpace(Data))
        throw new NullReferenceException();
    }
}


[Test]
public DoSomething_NullParameter_ThrowsException()
{
   var logic = MyClass();

   Assert.Throws<NullReferenceException>(() => logic.DoSomething(null));
}

Это все имеет смысл, но в какой-то момент вы попадете в класс, который фактически использует MyClass, и вы хотите проверить, что исключение обработано:

public class EntryPointClass
{
  public void DoIt(string Data)
  {
     var logicClass = new MyClass();

     try
     {
        logicClass.DoSomething(Data);
     }
     catch(NullReferenceException ex)
     {

     }
  }
}

[Test]
public DoIt_NullParameter_IsHandled()
{
  var logic = new EntryPointClass()

  try
  {
    logic.DoIt(null);
  }
  catch
  {
    Assert.Fail();
  }
}

Так почему бы не поставить на первое место try / catch в MyClass, а не выдавать исключение, и иметь тест, который проверяет на null в классе модульного теста MyClass, а не в классе модульного теста EntryPointClass?

Ответы [ 5 ]

3 голосов
/ 02 апреля 2012

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

public class EntryPointClass
{
  Logger _logfile;
  // ...
  public void DoIt(string Data)
  {
     var logicClass = new MyClass();

     try
     {
        logicClass.DoSomething(Data);
     }
     catch(NullReferenceException ex)
     {
         _logfile.WriteLine("null reference exception occured in method 'DoIt'");
     }
  }
}

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

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

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

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

 public class EntryPointClass 
 {
      // ....
      catch(NullReferenceException ex)
      {
          if(_logfile!=null)
              _logfile.WriteLine("null reference exception occured in method 'DoIt'");
      }    
      // ....
 }
1 голос
/ 02 апреля 2012

Вы на самом деле тестируете два совершенно разных условия:

  1. Проверить, что MyClass выдает исключение, когда аргумент недействителен
  2. Проверьте, как EntryPointClass, как пользователь MyClass, обрабатывает исключение, выброшенное MyClass
1 голос
/ 02 апреля 2012

Почему бы не поставить на первое место попытку / улов в MyClass ...

Исключения составляют отсоединение обнаружения ошибок и обработки ошибок.Если ошибка не может быть обработана (во всех случаях) локально в MyClass, тогда Вам следует выдать исключение.

Это позволяет обнаруживать локальные ошибки (MyClass) и обрабатывать ошибки любым, кто перехватит исключение.


Тест в EntryPointClass проверяет, является ли метод DoIt нулевым.Он имеет мало общего с использованием MyClass.

Другими словами: тест в EntryPointClass не имеет и (для проблем стабильности кода и инкапсуляции) не должен опираться на тот факт, что используется MyClass.

1 голос
/ 02 апреля 2012

Модульные тесты тестируют единицы работы

Напишите один тест, который тестирует исключение, затем напишите свой код в эту спецификацию и просмотрите тестовый проход (это ваш первый пример)

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

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

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

Пока вы тестируете правильную функцию вашего класса.

1 голос
/ 02 апреля 2012

Исключения указывают на исключительную ситуацию для вашего кода (см. Рекомендации MSDN по Обработка исключений ).Если аргумент null является исключительным для DoSomething, то его выдача имеет смысл, независимо от того, что и , как будет использовать этот класс позже.Это часть, которую стоит выделить из раздела Exception Throwing , который точно описывает это:

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...