Юнит-тесты и логика проверки - PullRequest
11 голосов
/ 08 января 2009

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

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

    // create and return user
}

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

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

Какие-нибудь замечательные модели или предложения в этом случае?

Ответы [ 6 ]

11 голосов
/ 08 января 2009

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

Это очень помогает при написании поддерживаемого набора юнит-тестов.

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


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

Возможно, вас заинтересует то, что написал Дж.П.Будху о своем стиле дизайна, ориентированного на поведение. http://blog.developwithpassion.com/2008/12/22/how-im-currently-writing-my-bdd-style-tests-part-2/

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

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

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

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

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

2 голосов
/ 08 января 2009

Какова ответственность вашего класса бизнес-логики и делает ли он что-то помимо проверки? Я думаю, что у меня возникнет соблазн переместить подпрограммы проверки в собственный класс (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
}

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

2 голосов
/ 08 января 2009

Вам обязательно нужно проверить валидацию методов.

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

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

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

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

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

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

2 голосов
/ 08 января 2009
  • Пусть юнит-тесты (множественное число) с методами Validate подтверждают их правильное функционирование.
  • Пусть модульные тесты (во множественном числе) метода CreateUser подтверждают его правильное функционирование.

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

0 голосов
/ 08 января 2009

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

[Test]
public void Test()
{
  ShouldPass("rinat.abdullin@lokad.com", "pwd", "http://ws.lokad.com/TimeSerieS2.asmx");
  ShouldPass("some@nowhere.net", "pwd", "http://127.0.0.1/TimeSerieS2.asmx");
  ShouldPass("rinat.abdullin@lokad.com", "pwd", "http://sandbox-ws.lokad.com/TimeSerieS2.asmx");

  ShouldFail("invalid", "pwd", "http://ws.lokad.com/TimeSerieS.asmx");
  ShouldFail("rinat.abdullin@lokad.com", "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-адрес соединения), то правило и тест обновляются.

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

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

0 голосов
/ 08 января 2009

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

...