Мысли вслух…

NUnit – unit-testing framework

Автор: Июл.30.2009 Категория: Development

Буквально сегодня бороздя просторы интернета в поисках нормальной русскоязычной документации (описания, мануалов) по аттрибутам фраемворка — NUnit, наткнулся на запись в блоге. Автор любезен и позволил скопировать информацию.

От себя хочу добавить. Материал достаточно полезный, просто описан и показывает много возможностей, если до этого работа с NUnit как таковая не осуществлялась.

Ну чтож, пожалуй начнем (ух и намучаюсь же я сейчас с оформлением :blink: )

NUnit – unit-testing framework

NUnit – это, пожалуй, старейший unit-testing framework для .NET, он появился в 2000-м году. Сейчас, после почти годичной разработки, готовится к выходу версия 2.5 (сегодня вышел RC1), в которой появилось множество новых возможностей, о которых я и хочу рассказать.

NUnit версии 2.5 – что нового?

Итак, что же появилось в NUnit версии 2.5?

  1. Новые Constraints.
  2. User-defined компараторы.
  3. Возможность задавать точность при сравнении чисел с плавающей запятой, даты и времени.
  4. Параметризованные тесты.
  5. Комбинаторные и Pairwise-тесты.
  6. Theories.
  7. Exception Browser (в GUI-клиенте).
  8. Параллельное выполнение тестов (pnunit).

Новые Constraints и модификаторы

Constraint-based модель

Наверное, перед тем как приступить к описанию новых constraints и модификаторов, есть смысл сказать несколько слов о том, что же такое constrains и модификаторы  :smile:

Традиционная модель проверок, корни которой лежат, пожалуй, в самом первом публичном unit testing framework, JUnit, подразумевает, что у нас есть некоторый класс (Assert), имеющий набор статических методов для проверки различных условий.

При таком подходе тестовые методы выглядят так:


[Test]
public void TraditionalTest()
{
string actualValue = "abc";
Assert.AreEqual("ABC", actualValue.ToUpper());
}

Первые версии NUnit использовали традиционную модель проверок, и она до сих пор сохранена для обеспечения обратной совместимости, но сейчас основная модель проверок основана на использовании отдельных объектов-constraints (реализующих интерфейс IConstraint), каждый из которых реализует ту или иную проверку:


[Test]
public void ConstraintBasedTest1()
{
string actualValue = "abc";
Assert.That(actualValue, new EqualConstraint("abc"));
}

Многие объекты-constraints имеют модификаторы – свойства или методы, позволяющие указать дополнительные параметры. Эти свойства и методы возвращают ссылку на сам объект, поэтому их можно использовать “в цепочке”:


[Test]
public void ConstraintBasedTest2()
{
string actualValue = "abc";
Assert.That(actualValue, new EqualConstraint("ABC").IgnoreCase);
}

Ну и наконец, NUnit включает специальные классы-“syntax helpers” – Is и Has, которые позволяют записывать условие практически на “человеческом” языке:


[Test]
public void ConstraintBasedTest3()
{
string actualValue = "abc";
Assert.That(actualValue, Is.EqualTo("ABC").IgnoreCase);
}

На мой взгляд, constraint-based модель имеет сразу три плюса:

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

Во-вторых, облегчается написание условий – метод AreEqual() имеет 24 варианта, и достаточно сложно выбрать нужный даже при использовании IntelliSense, а при использовании constraints и syntax helpers выбор нужного метода значительно облегчается.

И, наконец, constraint-based модель более правильна с методологической точки зрения — за каждый вид проверки отвечает отдельный класс, а значит, код получается более понятным.

Новые Constraints

Думаю, что теперь самое время ответить на вопрос, какие же новые constrains появились в NUnit 2.5. Эти constrains перечислены ниже:

  1. RangeConstraint
  2. Path Constraints (SamePathConstraint, SamePathOrUnderConstraint)
  3. Collection Constraints (UniqueItemsConstraint, CollectionOrderedConstraint)
  4. ThrowsConstraint
  5. DelayedConstraint

Конечно же, никаких откровений здесь нет, но работать с NUnit теперь стало проще!

Range Constraint

RangeConstraint позволяет проверить, что значение попадает в указанный диапазон:


Assert.That(2, Is.InRange(1, 3));

Как видно, это просто короткая замена для приведенной ниже конструкции:


Assert.That(2, Is.GreaterThanOrEqualTo(1).And.LessThanOrEqualTo(3));

Path Constraints

SamePathConstraint позволяет проверить, что пути совпадают:


Assert.That(@"c:\users\psg", Is.SamePath(@"C:\Users\psg"));
Assert.That(@"c:\users\..\users\psg", Is.SamePath(@"C:\Users\psg"));

SamePathOrUnderConstraint позволяет проверить, что actual-путь совпадает или находится “ниже” expected-пути:


Assert.That(@"c:\users\psg", Is.SamePathOrUnder(@"C:\Users\psg"));
Assert.That(@"c:\users\psg\Desktop", Is.SamePathOrUnder(@"C:\Users\psg"));

Тоже ничего особенного, просто удобное дополнение для сравнения путей.

Collection Constraints

Значительно больший интерес представляют constrains для сравнения коллекций: UniqueItemsConstraint и CollectionOrderedConstraint.

UniqueItemsConstraint, как это следует из названия, позволяет проверить, что коллекция содержит только уникальные элементы:


Assert.That(new int[] { 1, 2, 3 }, Is.Unique);

CollectionOrderedConstraint проверяет, что элементы коллекции следуют в определенном порядке. В приведенном ниже примере показаны варианты использования этого constraint.


class Number
{
public Number(int value)
{
Value = value;
}

public int Value { get; private set; }
}

[Test]
public void CollectionOrderedConstraintTest()
{
Assert.That(new int[] { 1, 2, 3 }, Is.Ordered);

Assert.That(new int[] { 3, 2, 1 }, Is.Ordered.Using((a, b) => -a.CompareTo(b)));

Number[] numbers1 = { new Number(1), new Number(2), new Number(3) };
Assert.That(numbers1, Is.Ordered.By("Value"));

Number[] numbers2 = { new Number(3), new Number(2), new Number(1) };
Assert.That(numbers2, Is.Ordered.By("Value").Using((a, b) => -a.CompareTo(b)));
}

Throws Constraint

Наконец-то в NUnit появился constraint для отслеживания исключений:


Assert.That(
() => new Worker().DoSomething(),
Throws.InstanceOf()
.And.Message.Contains("DoSomething()"));

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


[Test]
[ExpectedException(
ExpectedException = typeof(ApplicationException),
ExpectedMessage = "DoSomething()",
MatchType = MessageMatch.Contains)]
public void OldStyle()
{
new Worker().DoSomething();
}

Как видно, при использовании Throws Constraint приходится писать меньше кода, можно проверять несколько условий с исключениями в рамках одного теста и, наконец, появляется больше возможностей для проверки содержимого самого объекта исключения!

Delayed Constraint

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


[Test]
public void DelayedConstraintTest()
{
bool flag = false;

Thread thread = new Thread(() => { Thread.Sleep(1000); flag = true; });
thread.Start();

Assert.That(() => flag, Is.True.After(3000));
}

Т.е. DelayedConstraint может применяться для отслеживания без дополнительных телодвижений некоторых действий, которые выполняются асинхронно по отношению к тесту. Чтобы эта ситуация не казалась надуманной, приведу более жизненный пример:


class Command
{
public bool Processed { get; set; }
}

class CommandProcessor
{
public void EnqueueCommand(Command command)
{
ThreadPool.QueueUserWorkItem(
(state) => ((Command)state).Processed = true,
command);
}
}

[Test]
public void DelayedConstraintTest()
{
Command command = new Command();

CommandProcessor processor = new CommandProcessor();
processor.EnqueueCommand(command);

Assert.That(() => command.Processed, Is.True.After(1000, 100));
}

В этом примере класс CommandProcessor осуществляет асинхронную обработку переданных ему команд и по окончании обработки выставляет флаг Processed. Проверка того факта, что команда успешно обработана, производится с помощью DelayedConstraint, максимальное время ожидания составляет 1 секунду, опрос флага производится с интервалом 100 миллисекунд.

User-defined компараторы и сравнение коллекций

При сравнении элементов на равенство появилась возможность использовать компараторы, определенные пользователем:


[Test]
public void CustomComparerTest1()
{
Assert.That(
"a",
Is.EqualTo("A").Using(
(a, b) => String.Compare(
a, b, StringComparison.InvariantCultureIgnoreCase)));
}

И еще два варианта:


[Test]
public void CustomComparerTest2()
{
Assert.That(
"a",
Is.EqualTo("A").Using(
StringComparer.InvariantCultureIgnoreCase as
IComparer));
}

[Test]
public void CustomComparerTest3()
{
Assert.That(
"a",
Is.EqualTo("A").Using(
StringComparer.InvariantCultureIgnoreCase as
IEqualityComparer));
}

Такой подход может быть полезен в случаях, когда приходится проводить множество сравнений “сложных” объектов, для которых, тем не менее, нежелательно переопределять метод Equals(), но наибольший эффект достигается при сравнении коллекций:


[Test]
public void CustomComparerTest()
{
Assert.That(
new string[] { "a", "b", "c" },
Is.EqualTo(new string[] { "A", "B", "C" }).Using(
(a, b) => String.Compare(
a, b, StringComparison.InvariantCultureIgnoreCase)));
}

До версии 2.5 для реализации такого сравнения нужно было использовать обходные пути, например, использовать LINQ:


[Test]
public void LinqComparerTest()
{
Assert.That(
new string[] { "a", "b", "c" }.SequenceEqual(
new string[] { "A", "B", "C" },
StringComparer.InvariantCultureIgnoreCase));
}

Но при этом в случае ошибки вместо информативного сообщения:

Samples.CollectionSamples.CustomComparerTest:
Expected and actual are both Values differ at index [1]
String lengths are both 1. Strings differ at index 0.
Expected: «B»
But was: «x»
————^

мы получали маловразумительное:

Samples.CollectionSamples.LinqComparerTest:
Expected: True
But was: False

Сравнение чисел с плавающей запятой, даты и времени

Сравнение чисел с плавающей запятой

Как я уже говорил, в NUnit 2.5 появилась возможность указывать точность, с которой будет производиться сравнение чисел с плавающей запятой. Для этого в EqualConstraint было добавлено три модификатора: Within, Percent и Ulps.

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


Assert.That(1.5, Is.EqualTo(2).Within(1));

Но из-за ограничений на длину мантиссы абсолютное значение погрешности подходит далеко не всегда, например, для параметризованного теста, работающего в широком диапазоне значений, подобрать значение погрешности может оказаться просто невозможно – оно или будет слишком велико, чтобы проверки для “маленьких” значений имели смысл, или слишком мало, чтобы проверки успешно проходили для “больших” значений. Для компенсации этих неприятностей служат модификаторы Percent и Ulps.

Модификатор Percent позволяет задать погрешность в процентах:


Assert.That(1.5, Is.EqualTo(2).Within(50).Percent);

А модификатор Ulps позволяет задать точность в “units in last place”, т.е. фактически в единицах погрешности машинной арифметики:


Assert.That(Math.Sin(Math.PI / 2), Is.EqualTo(1).Within(1).Ulps);

Дополнительную информацию о тонкостях работы с числами с плавающей запятой можно получить, например, из статьи Сергей Холодилова “Плавающая запятая”, опубликованной на сайте RSDN.

Сравнение даты и времени

Кроме модификаторов, задающих точность при сравнении чисел с плавающей запятой в NUnit 2.5 появились модификаторы, позволяющие указать точность при сравнении даты и времени. Жизненный пример – тип DATETIME в SQL Server позволяет сохранять дату и время с точностью до 3-х миллисекунд, поэтому, если мы запишем значение даты и времени в БД SQL Server, а затем сразу же прочитаем это значение, то в большинстве случаев полученное от SQL Server значение не будет совпадать с исходным.

Это можно легко проверить:


[Test]
public void DateTimeTest()
{
for (int ms = 991; ms <= 999; ms++)
{
DateTime dt = new DateTime(2009, 04, 22, 20, 52, 41, ms);
DateTime sqldt = new SqlDateTime(dt).Value;
Assert.That(dt, Is.EqualTo(sqldt));
}
}

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

Раньше, чтобы этот тест заработал, приходилось вручную добавлять код для конвертации DateTime в SqlDateTime, теперь можно просто добавить модификатор Milliseconds:


[Test]
public void DateTimeTest()
{
for (int ms = 991; ms <= 999; ms++)
{
DateTime dt = new DateTime(2009, 04, 22, 20, 52, 41, ms);
DateTime sqldt = new SqlDateTime(dt).Value;
Assert.That(dt, Is.EqualTo(sqldt).Within(3).Milliseconds);
}
}

Кроме модификатора Milliseconds есть и другие: Days, Hours, Minutes, Seconds и Ticks.

Параметризованные тесты

Параметризованные тесты до версии 2.5

Пожалуй, одно из самых ожидаемых нововведений в NUnit 2.5 – это параметризованные тесты. Ранее параметризованные тесты были доступны только с помощью RowTest addin (автор Andreas Schlapsi), и выглядели они следующим образом:


[RowTest]
[Row(2, 3, 6)]
public void ParameterizedTest(int a, int b, int result)
{
Assert.That(a * b, Is.EqualTo(result));
}

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

Простые параметризованные тесты

В NUnit 2.5 появились “родные” параметризованные тесты и большое количество вариантов для задания аргументов этих тестов.

Во-первых, можно задавать параметры в атрибутах, так же, как это делалось при использовании RowTest:


[TestCase(2, 3, 6)]
public void ParameterizedTest(int a, int b, int result)
{
Assert.That(a * b, Is.EqualTo(result));
}

Можно упростить этот тест, используя вот такую забавную конструкцию:


[TestCase(2, 3, Result = 6)]
public int ParameterizedTest(int a, int b)
{
return a * b;
}

Можно указывать наборы тестовых значений в массиве – и теперь это могут быть параметры любого тип, например, тот же Decimal:


object[] TestData =
{
new object[] { 2m, 3m, 6m }
};

[Test]
[TestCaseSource("TestData")]
public void ParameterizedTest(decimal a, decimal b, decimal result)
{
Assert.That(a * b, Is.EqualTo(result));
}

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


object[] TestData =
{
new TestCaseData(6, 3).Returns(2),
new TestCaseData(6, 0).Throws(typeof(DivideByZeroException))
};

[Test]
[TestCaseSource("TestData")]
public int ParameterizedTest(int a, int b)
{
return a / b;
}

А еще можно определить свойство типа IEnumerable, и в этом случае мы не ограничены в выборе источника данных – их можно взять из файла или загрузить из БД:


IEnumerable TestData
{
get
{
yield return new TestCaseData(6, 3).Returns(2);
yield return new TestCaseData(6, 0).Throws(typeof(DivideByZeroException));
}
}

[Test]
[TestCaseSource("TestData")]
public int ParameterizedTest5(int a, int b)
{
return a / b;
}

Комбинаторные и Pairwise-тесты

Комбинаторные тесты

Кроме простых параметризованных тестов, в NUnit появилось еще два вида тестов – это комбинаторные и pairwise-тесты.

С комбинаторными тестами все просто – NUnit дает на вход тестового метода все возможные комбинации входных параметров:


[Test]
[Combinatorial]
public void CombinatorialTest(
[Values("a", "b")] string a,
[Values("+", "-")] string b,
[Values("x", "y")] string c)
{
Console.WriteLine(a + b + c);
}

Результат выполнения теста:

***** Samples.CombinatorialSamples.CombinatorialTest(«a»,»-«,»x»)
a-x
***** Samples.CombinatorialSamples.CombinatorialTest(«a»,»-«,»y»)
a-y
***** Samples.CombinatorialSamples.CombinatorialTest(«a»,»+»,»x»)
a+x
***** Samples.CombinatorialSamples.CombinatorialTest(«a»,»+»,»y»)
a+y
***** Samples.CombinatorialSamples.CombinatorialTest(«b»,»-«,»x»)
b-x
***** Samples.CombinatorialSamples.CombinatorialTest(«b»,»-«,»y»)
b-y
***** Samples.CombinatorialSamples.CombinatorialTest(«b»,»+»,»x»)
b+x
***** Samples.CombinatorialSamples.CombinatorialTest(«b»,»+»,»y»)
b+y

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

Pairwise-тесты

Второй тип тестов – это pairwise-тесты. В pairwise, или “all-pairs” тестах на вход тестового метода передаются все возможные комбинации пар параметров:


[Test]
[Pairwise]
public void PairwiseTest(
[Values("a", "b")] string a,
[Values("+", "-")] string b,
[Values("x", "y")] string c)
{
Console.WriteLine(a + b + c);
}

Вот что мы увидим при выполнении этого теста:

***** Samples.PairwiseSamples.PairwiseTest(«a»,»-«,»x»)
a-x
***** Samples.PairwiseSamples.PairwiseTest(«a»,»+»,»y»)
a+y
***** Samples.PairwiseSamples.PairwiseTest(«b»,»-«,»y»)
b-y
***** Samples.PairwiseSamples.PairwiseTest(«b»,»+»,»x»)
b+x
***** Samples.PairwiseSamples.PairwiseTest(«b»,»+»,»y»)
b+y

Для чего можно использовать pairwise-тесты? Конечно же, для сокращения затрат на выполнение тестов – даже на небольших примерах виден выигрыш pairwise-теста по сравнению с комбинаторным тестом – 5 комбинаций против 8!

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

Например, если у нас есть 10 параметров, и для каждого параметра есть 10 значений, то для комбинаторного теста количество комбинаций будет 1010, тогда как для all-pair теста количество комбинаций будет всего лишь 155!
Вообще говоря, это не совсем верно, правильнее будет сказать “не более 155”. Минимальное количество тестовых примеров, необходимое для покрытия всех возможных пар значений, можно установить только полным перебором, что требует огромного количества ресурсов. В NUnit использован алгоритм, который придумал и реализовал в своей утилите “jenny” Bob Jenkins. В этом алгоритме случайным образом генерируется несколько тестовых примеров, а затем из них выбирается один, обеспечивающий наилучшее покрытие – и так пока не будут “закрыты” все пары значений.

Атрибуты ValueSource, Range, Random и Sequential-тесты

Атрибут ValueSource

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


public string[] ArgDataA = { "a", "b" };
public string[] ArgDataB = { "+", "-" };
public string[] ArgDataC = { "x", "y" };

[Test]
public void ValueSourceTest(
[ValueSource("ArgDataA")] string a,
[ValueSource("ArgDataB")] string b,
[ValueSource("ArgDataC")] string c)
{
Console.WriteLine(a + b + c);
}

Атрибут Range

С помощью атрибута Range можно указать диапазон значений аргумента:


[Test]
public void RangeTest([Range(0, 1, 0.3)] double a)
{
Console.WriteLine(a);
}

Результат:

***** Samples.ParameterizedSamples.ParameterizedTest(0.0d)
0

***** Samples.ParameterizedSamples.ParameterizedTest(0.3d)
0.3

***** Samples.ParameterizedSamples.ParameterizedTest(0.6d)
0.6

***** Samples.ParameterizedSamples.ParameterizedTest(0.9d)
0.9

Атрибут Random

С помощью атрибута Random можно указать использовать в качестве аргумента случайные числа в заданном диапазоне:


[Test]
public void RandomTest([Random(1, 10, 3)] int a)
{
Console.WriteLine(a);
}

Результат:

***** Samples.ParameterizedSamples.RandomTest(1)
1

***** Samples.ParameterizedSamples.RandomTest(6)
6

***** Samples.ParameterizedSamples.RandomTest(7)
7

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

Sequential-тесты

И в завершение разговора о параметризованных тестах стоит упомянуть о sequential-тестах:


[Test]
[Sequential]
public void SequentialTest(
[Values("a", "b")] string a,
[Values("+", "-")] string b,
[Values("x", "y")] string c)
{
Console.WriteLine(a + b + c);
}

Результат:

***** Samples.ParameterizedSamples.SequentialTest(«a»,»+»,»x»)
a+x
***** Samples.ParameterizedSamples.SequentialTest(«b»,»-«,»y»)
b-y

Как видно, sequential-тест это просто еще одна форма записи для простых параметризованных тестов. Каких либо преимуществ перед использованием атрибута TestCase эта форма не имеет, но в каких-то случаях она может оказаться более предпочтительной.

Theories

Что такое “теории”?

Все, о чем я говорил выше, так или иначе укладывалось в рамки “расширений и улучшений”. Теперь я хочу рассказать о принципиально новой возможности, появившейся в NUnit 2.5, о “теориях”.

“Теории” – это специальный тип тестов, который используется для проверки некоторых общих предположений о поведении тестируемого кода и применяется в процессе разработки. Традиционные тесты основаны на примерах – для каждого набора входных параметров мы указываем, какой результат мы ожидаем получить. В случае же “теории” мы делаем предположение, что код будет работать корректно для любого набора аргументов, который удовлетворяет некоторым условиям.

Реализация “теорий” в NUnit очень похожа на параметризованные тесты – но “теории” используют дополнительные источники данных и позволяют вводить ограничения на входные данные.

Но все-таки, основное отличие “теорий” от традиционных тестов в их назначении – “теории” это не просто набор примеров, они предназначены для проверки каких-то общих предположений.

Источники данных для теорий

Основным источником данных для “теорий” являются поля, маркированные атрибутами Datapoint (для “одиночных” значений) и Datapoints (для наборов значений). При выполнении метода-“теории” NUnit собирает все подходящие по типу данные для каждого параметра этого метода и затем создает набор тестовых примеров (test cases), используя комбинаторный метод.

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

Ограничения на входные данные

Метод-“теория” сам проводит проверку входных данных. Для задания ограничений на входные данные используется конструкция Assume.That(…), которая работает так же, как и Assert.That(…), но не приводит к пометке “теории” как ошибочной.

Результат выполнения метода-“теории”

“Теория” считается подтвержденной, если хотя бы один тестовый пример удовлетворяет всем наложенным на входные данные ограничениям, и при выполнении метода-“теории” ни одна проверка (assertion) не провалилась.

Пример “теории”

В качестве примера использования “теории” я сделаю проверку следующего утверждения: “для любого количества миллисекунд отклонение значений DateTime и SqlDateTime не будет превышать 3-х миллисекунд”.


public class SqlDateTimeTests
{
[Datapoint]
public int zeroInt = 0;

[Datapoints]
public int[] negative = { -1, Int32.MinValue };

[Datapoints]
public int[] positive = { 1, Int32.MaxValue };

[Datapoints]
public int[] fromSqlBook = { 991, 992, 993, 994, 995, 996, 997, 998, 999 };

[Theory]
public void SqlDateTimeTheory(int milliseconds)
{
Assume.That(milliseconds >= 0 && milliseconds <= 999);

DateTime dt = new DateTime(2009, 4, 24, 23, 11, 51, milliseconds);
DateTime sqldt = new SqlDateTime(dt).Value;

Assert.That(sqldt, Is.EqualTo(dt).Within(3).Milliseconds);
}
}

Если запустить этот пример, то мы увидим, что эта теория верна  :smile:

Exception Browser

Exception Browser – это расширение NUnit GUI Runner, которое помогает найти место возникновения исключения при выполнении теста. До появления Exception Browser при возникновении исключения NUnit просто показывал Start Trace в том виде, в каком его можно получить, считав свойство StackTrace объекта-исключения:

at Samples.ExceptionBrowserTest.Worker.DoSomething() in
C:\psgdev\Projects\NUnit\Tests\Samples\ExceptionBrowserTest.cs:line 16
at Samples.ExceptionBrowserTest.Test() in
C:\psgdev\Projects\NUnit\Tests\Samples\ExceptionBrowserTest.cs:line 23

Понятно, что разбирать этот текст удовольствие ниже среднего. Теперь же эту информацию можно увидеть практически в том же виде, что и в Visual Studio:

NUnit GUI

Параллельное выполнение тестов

PNUnit (Parallel NUnit) – это расширение NUnit, позволяющее выполнять тесты параллельно, в том числе и на удаленных машинах. PNUnit разработал Pablo Santos Luaces и его команда в Codice Software для тестирования Plastic ™ SCM, в 2007 году эта разработка была опубликована.

Изначально PNUnit использовал несколько модифицированную версию NUnit, но с версии 2.5 PNUnit является частью NUnit, и все тесты, написанные для NUnit 2.5, PNUnit может выполнять без модификаций.

Простой пример

Теперь на нескольких примерах я попробую показать, как же использовать PNUnit.

Первый шаг – разработка теста. Я сделаю очень простой тест:


namespace Samples
{
public class SampleTests
{
[Test]
public void SimpleTest()
{
Assert.That("a", Is.EqualTo("A").IgnoreCase);
}
}
}

Второй шаг – настройка агента. Файл конфигурации агента (agent.conf) приведен ниже:


<AgentConfig>
<Port>8080</Port>
<PathToAssemblies>.</PathToAssemblies>
</AgentConfig>

Третий шаг – запуск агента:

start pnunit-agent.exe agent.conf

Четвертый шаг – подготовка конфигурации тестов. Файл конфигурации (Samples.conf) приведен ниже, думаю, что в дополнительных комментариях он не нуждается:


<TestGroup>
<ParallelTests>
<ParallelTest>
<Name>Sample</Name>
<Tests>
<TestConf>
<Name>SimpleTest</Name>
<Assembly>Samples.dll</Assembly>
<TestToRun>Samples.SampleTests.SimpleTest</TestToRun>
<Machine>localhost:8080</Machine>
<TestParams />
</TestConf>
</Tests>
</ParallelTest>
</ParallelTests>
</TestGroup>

И, наконец, пятый шаг – запуск теста:

pnunit-launcher.exe Samples.conf

Результат выполнения теста показан ниже:

INFO launcher — Test 1 of 1
INFO launcher — Starting Sample test SimpleTest on localhost:8080
INFO launcher — Result for TestGroup Sample, Test SimpleTest: PASS
INFO launcher — ==== Tests Results for Parallel TestGroup Sample ===
INFO launcher — (1) Name: SimpleTest
Result: SUCCESS Assert Count: 1 Time: 0.011
INFO launcher — Summary:
INFO launcher — Total: 1
Executed: 1
Failed: 0
Success: 1
% Success: 100
Biggest Execution Time: 0.011 s

INFO launcher — Launcher execution time: 2.476 seconds

Передача параметров

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

Тест:


namespace Samples
{
public class SampleTests
{
[Test]
public void ParameterizedTest()
{
string[] args = PNUnitServices.Get().GetTestParams();
Assert.That(args[0], Is.EqualTo("Sample").IgnoreCase);
}
}
}

Файл конфигурации:


<TestGroup>
<ParallelTests>
<ParallelTest>
<Name>Sample</Name>
<Tests>
<TestConf>
<Name>ParameterizedTest</Name>
<Assembly>Samples.dll</Assembly>
<TestToRun>Samples.SampleTests.ParameterizedTest</TestToRun>
<Machine>localhost:8080</Machine>
<TestParams>
<string>sample</string>
</TestParams>
</TestConf>
</Tests>
</ParallelTest>
</ParallelTests>
</TestGroup>

Результат выполнения теста:

INFO launcher — Test 1 of 1
INFO launcher — Starting Sample test ParameterizedTest on localhost:8080
INFO launcher — Result for TestGroup Sample, Test ParameterizedTest: PASS
INFO launcher — ==== Tests Results for Parallel TestGroup Sample ===
INFO launcher — (1) Name: ParameterizedTest
Result: SUCCESS Assert Count: 1 Time: 0.011
INFO launcher — Summary:
INFO launcher — Total: 1
Executed: 1
Failed: 0
Success: 1
% Success: 100
Biggest Execution Time: 0.011 s

INFO launcher — Launcher execution time: 2.541 seconds

Синхронизация тестов

PNUnit дает возможность синхронизировать работу нескольких тестов. Синхронизированные тесты уже не будут unit-тестами (см. “Эффективный модульный тест” в блоге Александра Бындю), но эта возможность будет полезной для функциональных тестов или для нагрузочных тестов – так что можно сказать, что PNUnit в значительной степени расширяет границы применимости NUnit.

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


namespace Samples
{
public class SampleTests
{
[Test]
public void ServerTest()
{
PNUnitServices.Get().InitBarriers();

for (int i = 10; i > 0; i--)
{
PNUnitServices.Get().WriteLine(
String.Format("Initializing: {0} seconds remaining", i));
Thread.Sleep(1000);
}

PNUnitServices.Get().EnterBarrier(
PNUnitServices.Get().GetTestStartBarrier());

PNUnitServices.Get().WriteLine("Server started!");
}

[Test]
public void ClientTest()
{
PNUnitServices.Get().InitBarriers();

PNUnitServices.Get().WriteLine("Waiting for server to start...");

PNUnitServices.Get().EnterBarrier(
PNUnitServices.Get().GetTestStartBarrier());

PNUnitServices.Get().WriteLine("Server started!");
}
}
}

Для параллельного запуска клиента и сервера потребуется два агента и специальная тестовая конфигурация.

Конфигурация агента #1 (agent1.conf):


<agentconfig>
<port>8081</port>
<pathtoassemblies>.</pathtoassemblies>
</agentconfig>

Запуск агента #1:

start pnunit-agent.exe agent1.conf

Конфигурация агента #2 (agent2.conf):


<agentconfig>
<port>8082</port>
<pathtoassemblies>.</pathtoassemblies>
</agentconfig>

Запуск агента #2:

start pnunit-agent.exe agent2.conf

Тестовая конфигурация (SyncSamples.conf):


<testgroup>
<paralleltests>
<paralleltest>
<name>Samples</name>
<tests>
<testconf>
<name>ServerTest</name>
<assembly>Samples.dll</assembly>
<testtorun>Samples.SampleTests.ServerTest</testtorun>
<machine>localhost:8081</machine>
<testparams />
</testconf>
<testconf>
<name>ClientTest</name>
<assembly>Samples.dll</assembly>
<testtorun>Samples.SampleTests.ClientTest</testtorun>
<machine>localhost:8082</machine>
<testparams />
</testconf>
</tests>
</paralleltest>
</paralleltests>
</testgroup>

Результат выполнения:

INFO launcher — Test 1 of 1
INFO launcher — Starting Samples test ServerTest on localhost:8081
INFO launcher — Starting Samples test ClientTest on localhost:8082
INFO launcher — Result for TestGroup Samples, Test ServerTest: PASS
INFO launcher — Result for TestGroup Samples, Test ClientTest: PASS
DEBUG launcher — Thread going to finish for TestGroup Samples
INFO launcher — ==== Tests Results for Parallel TestGroup Samples ===
INFO launcher — (1) Name: ServerTest
Result: SUCCESS Assert Count: 0 Time: 10.073
INFO launcher — (2) Name: ClientTest
Result: SUCCESS Assert Count: 0 Time: 9.114
INFO launcher — Summary:
INFO launcher — Total: 2
Executed: 2
Failed: 0
Success: 2
% Success: 100
Biggest Execution Time: 10.073 s

INFO launcher — Launcher execution time: 12.706 seconds

Результат выполнения для агента #1:

INFO PNUnit.Agent.PNUnitTestRunner — Running tests
Initializing: 10 seconds remaining

Initializing: 1 seconds remaining
>>>Test ServerTest entering barrier SERVERSTART
<<<Test ServerTest leaving barrier SERVERSTART
Server started!
INFO PNUnit.Agent.PNUnitTestRunner — Notifying the results

Результат выполнения для агента #2:

INFO PNUnit.Agent.PNUnitTestRunner — Running tests
Waiting for server to start…
>>>Test ClientTest entering barrier SERVERSTART
<<<Test ClientTest leaving barrier SERVERSTART
Server started!
INFO PNUnit.Agent.PNUnitTestRunner — Notifying the results

Для чего можно использовать PNUnit?

Спектр задач, для которых можно использовать PNUnit, достаточно широк, попробую обозначить самые основные:

  1. Распределение нагрузки. Если проект включает множество тестов, их выполнение может занимать длительное время. Используя PNUnit, можно разделить эти тесты на блоки и запускать их параллельно, что может существенно сократить время сборки проекта.
  2. Функциональные тесты, проверяющие взаимодействие различных модулей системы в “боевых условиях”.
  3. Нагрузочные тесты.

Ссылки

NUnit home page:
http://www.nunit.org/
http://www.nunit.com/

NUnit-Discuss
http://groups.google.com/group/nunit-discuss

Сергей Холодилов, Плавающая запятая
http://www.rsdn.ru/article/alg/float.xml

Bob Jenkins’ Jenny
http://burtleburtle.net/bob/math/jenny.html

Pairwise Testing
http://www.pairwise.org/

Codice Software
http://www.codicesoftware.com/

PNUnit @ Codice Software
http://www.codicesoftware.com/opdownloads2/oppnunit.aspx

:,

Leave a Reply

:bad: :beer: :biggrin: :blink: :blush: :bomb: :confused: :cool: :crazy: :cry: :dont_know: :eek: :evil: :dance: :heart: :idea: :joke: :kiss: :lol: :mad: :music: :rose: :sad: :smile: :surprised: :tongue: :yahoo: :wall: :wink:
 

Поиск

Список друзей