Единичные тесты и логика проверки

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

public User CreateUser(string username, string password, UserDetails details)
{
    ValidateUserDetails(details);
    ValidateUsername(username);
    ValidatePassword(password);

    // create and return user
}

Должно ли мое тестовое устройство содержать тесты на все возможные ошибки проверки, которые могут возникать в методах Validate *, или лучше оставить это для отдельного набора тестов? Или, возможно, логика проверки должна быть каким-то образом реорганизована?

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

Любые большие шаблоны или предложения в этом случае?

Ответ 1

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

Это очень помогает при написании поддерживаемого набора модульных тестов.

Я бы написал пару тестов для ValidateUserDetails, ValidateUsername и ValidateUserPassword. Затем вам нужно только проверить, что CreateUser вызывает эти функции.


Перечитайте свой вопрос; Кажется, я немного что-то понял.

Вам может быть интересно, что написал J.P Boodhoo в своем стиле поведения, ориентированного на поведение. http://blog.developwithpassion.com/2008/12/22/how-im-currently-writing-my-bdd-style-tests-part-2/

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

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

У меня не было большого опыта с этим.

Если вам интересно читать больше, JP Boodhoo опубликовал много об этом в своем блоге (см. ссылку выше), или вы также можете послушать эпизод с сетчатыми точками с Scott Bellware, где он говорит о подобном способе тесты группировки и именования http://www.dotnetrocks.com/default.aspx?showNum=406

Надеюсь, это больше того, что вы ищете.

Ответ 2

  • Пусть Unit Tests (множественное число) против методов Validate подтверждают их правильное функционирование.
  • Пусть Unit Tests (множественное число) против метода CreateUser подтверждают его правильное функционирование.

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

Ответ 3

Вам определенно нужно проверить методы проверки.

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

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

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

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

Что касается уровня приложения, который должен содержать логику проверки, возможно, лучшим является уровень сервиса (по Fowler), который определяет границы приложения и является хорошим место для дезинфекции заявки. И не должно быть никакой логики проверки внутри этих границ, только Design By Contract для обнаружения ошибок раньше.

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

Ответ 4

В чем ответственность вашего бизнес-логического класса и делает ли он что-то помимо проверки? Я думаю, что у меня возникнет соблазн переместить подпрограммы проверки в собственный класс (UserValidator) или несколько классов (UserDetailsValidator + UserCredentialsValidator) в зависимости от вашего контекста, а затем предоставить макеты для тестов. Таким образом, ваш класс теперь будет выглядеть примерно так:

public User CreateUser(string username, string password, UserDetails details)
{
    if (Validator.isValid(details, username, password)) {
       // what happens when not valid
    }

    // create and return user
}

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

Ответ 5

Я бы добавил кучу тестов для каждого метода ValidateXXX. Затем в CreateUser создайте 3 тестовых примера для проверки того, что происходит, когда каждый из ValidateUserDetails, ValidateUsername и ValidatePassword терпит неудачу, а другой успешно.

Ответ 6

Я использую Lokad Shared Library для определения правил проверки бизнеса. Вот как я тестирую угловые случаи (образец из открытого источника):

[Test]
public void Test()
{
  ShouldPass("[email protected]", "pwd", "http://ws.lokad.com/TimeSerieS2.asmx");
  ShouldPass("[email protected]", "pwd", "http://127.0.0.1/TimeSerieS2.asmx");
  ShouldPass("[email protected]", "pwd", "http://sandbox-ws.lokad.com/TimeSerieS2.asmx");

  ShouldFail("invalid", "pwd", "http://ws.lokad.com/TimeSerieS.asmx");
  ShouldFail("[email protected]", "pwd", "http://identity-theift.com/TimeSerieS2.asmx");
}

static void ShouldFail(string username, string pwd, string url)
{
  try
  {
    ShouldPass(username, pwd, url);
    Assert.Fail("Expected {0}", typeof (RuleException).Name);
  }
  catch (RuleException)
  {
  }
}

static void ShouldPass(string username, string pwd, string url)
{
  var connection = new ServiceConnection(username, pwd, new Uri(url));
  Enforce.That(connection, ApiRules.ValidConnection);
}

Если правило ValidConnection определено как:

public static void ValidConnection(ServiceConnection connection, IScope scope)
{
  scope.Validate(connection.Username, "UserName", StringIs.Limited(6, 256), StringIs.ValidEmail);
  scope.Validate(connection.Password, "Password", StringIs.Limited(1, 256));
  scope.Validate(connection.Endpoint, "Endpoint", Endpoint);
}

static void Endpoint(Uri obj, IScope scope)
{
  var local = obj.LocalPath.ToLowerInvariant();
  if (local == "/timeseries.asmx")
  {
    scope.Error("Please, use TimeSeries2.asmx");
  }
  else if (local != "/timeseries2.asmx")
  {
    scope.Error("Unsupported local address '{0}'", local);
  }

  if (!obj.IsLoopback)
  {
    var host = obj.Host.ToLowerInvariant();
    if ((host != "ws.lokad.com") && (host != "sandbox-ws.lokad.com"))
      scope.Error("Unknown host '{0}'", host);
  }

Если обнаружен какой-то неудачный случай (т.е. добавлен новый действительный URL соединения), тогда правило и тест будут обновлены.

Подробнее об этом шаблоне можно найти в в этой статье. Все есть Open Source, поэтому не стесняйтесь повторять или задавать вопросы.

PS: обратите внимание, что примитивные правила, используемые в этом примерном составном правиле (т.е. StringIs.ValidEmail или StringIs.Limited), тщательно протестированы самостоятельно и, следовательно, не требуют чрезмерных модульных тестов.