В настоящее время я пишу несколько модульных тестов для класса бизнес-логики, который включает процедуры проверки. Например:
public User CreateUser(string username, string password, UserDetails details)
{
ValidateUserDetails(details);
ValidateUsername(username);
ValidatePassword(password);
// create and return user
}
Должен ли мой тестовый набор содержать тесты для всех возможных ошибок валидации, которые могут возникнуть в методах Validate *, или лучше оставить это для отдельного набора тестов? Или, может быть, логику валидации нужно как-то реорганизовать?
Я считаю, что если я решу протестировать все ошибки валидации, которые могут возникнуть в CreateUser, тестовое приспособление станет довольно раздутым. И большинство методов проверки используются более чем из одного места ...
Какие-нибудь отличные шаблоны или предложения в этом случае?





Каждый тест должен завершиться неудачно только по одной причине, и только один тест должен быть неудачным по этой причине.
Это очень помогает при написании поддерживаемого набора модульных тестов.
Я бы написал по паре тестов для 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 много писал об этом в своем блоге (см. Ссылку выше), или вы также можете послушать эпизод dot net rocks со Скоттом Беллваром, в котором он рассказывает об аналогичном способе группировки и именования. тесты http://www.dotnetrocks.com/default.aspx?showNum=406
Надеюсь, это именно то, что вы ищете.
Если CreateUser просто требуется для вызова методов проверки, но не требуется для принятия решений о проверке, то тесты для CreateUser должны подтвердить это требование.
Вам обязательно нужно протестировать методы Проверка.
Нет необходимости тестировать другие методы для всех возможных комбинаций аргументов, чтобы убедиться, что проверка выполняется.
Похоже, вы смешиваете валидацию и дизайн по контракту.
Проверка обычно выполняется для удобного уведомления пользователя о том, что он введен неверно. Это очень связано с бизнес-логикой (недостаточно надежный пароль, неправильный формат электронной почты и т. д.).
Дизайн по контракту гарантирует, что ваш код может выполняться без генерации исключений позже (даже без них вы получите исключение, но гораздо позже и, вероятно, более неясное).
Что касается уровня приложения, который должен содержать логику проверки, вероятно, лучшим является уровень обслуживания (по Фаулеру), который определяет границы приложения и является хорошим местом для очистки ввода приложения. И внутри этих границ не должно быть никакой логики проверки, только Design By Contract для более раннего обнаружения ошибок.
Итак, наконец, напишите тесты логики проверки, если вы хотите дружественным образом уведомить пользователя о том, что он ошибся. В противном случае используйте Design By Contract и продолжайте генерировать исключения.
Я бы добавил несколько тестов для каждого метода ValidateXXX. Затем в CreateUser создайте 3 тестовых примера для проверки того, что происходит, когда каждый из ValidateUserDetails, ValidateUsername и ValidatePassword терпит неудачу, а другой - успешно.
Я использую Общая библиотека Lokad для определения правил проверки бизнеса. Вот как я тестирую угловые случаи (образец из открытого исходного кода):
[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-адрес подключения), тогда правило и тест обновляются.
Подробнее об этом шаблоне можно найти в эта статья. Все с открытым исходным кодом, поэтому не стесняйтесь использовать повторно или задавать вопросы.
PS: обратите внимание, что примитивные правила, используемые в этом образце составного правила (например, StringIs.ValidEmail или StringIs.Limited), тщательно протестированы сами по себе и, следовательно, не нужны чрезмерные модульные тесты.
За что отвечает ваш класс бизнес-логики и выполняет ли он что-то помимо проверки? Я думаю, что у меня возникнет соблазн переместить процедуры проверки в отдельный класс (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
}
Затем вы можете предоставить отдельные модульные тесты чисто для проверки, и ваши тесты для класса бизнес-логики могут сосредоточиться на том, когда проверка проходит и когда проверка не проходит, а также на всех других ваших тестах.