Когда я впервые увидел MbUnit в одном из проектов, то расстроился: "Ну вот, и в этой фирме тоже зоопарк фреймворков. Мало людям MSTest и NUnit, зачем-то нашли ещё один." Но потом MbUnit стал мне нравится. Документация, правда, неполная. Синтаксис стандартный, но с некоторыми дополнениями. Например, атрибут Rollback2 или сравнение коллекций. Вообще много чего там есть, но моя любимая фича - это Row Test.
Мне нужно протестировать всего-навсего три функции. Но, чтобы быть уверенным в результате, на вход нужно подавать сотни различных комбинаций параметров. Row Test - как раз то, что нужно. Я не пишу сотни Test-функций, а просто использую атрибуты, вот так:
[Test]
[Row("Barking", "Waterloo", RateType.Peak, 3.00, 4.00, 2.70 )]
[Row("Barking", "Waterloo", RateType.OffPeak, 3.00, 4.00, 2.50 )]
[Row("Bank", "Waterloo", RateType.OffPeak, 2.50, 2.00, 1.00 )]
[Row("Bank", "Waterloo", RateType.Peak, 2.50, 2.00, 1.50 )]
public void JourneyPrice(string origin, string destination, RateType rateType, decimal weeklyTravelCardPrice, decimal cashFare, decimal oysterPaygFare)
{
var journey = new Journey(origin, destination, rateType);
Assert.AreEqual(journey.WeeklyTravelCardPrice, weeklyTravelCardPrice);
Assert.AreEqual(journey.CashFare, cashFare);
Assert.AreEqual(journey.OysterPaygFare, oysterPaygFare);
}
Этот пример я придумал для блога. Возможно, он слегка притянут за уши. Я пытаюсь протестировать класс Journey, который вычисляет стоимость проезда между разными станциями лондонского метро. Почему я тестирую конкретные станции, а не зоны? Потому что именно это и должны делать программисты TfL. Во-первых, некоторые станции находятся на границах зон; во-вторых, иногда бывает, что во время поездки пассажир может пересаживаться на разных станциях, и однозначно неизвестно, как именно он доехал, и в какие зоны заезжал. Всё это надо учесть при вычислении цены.
С помощью атрибутов Row я передаю и входные параметры (станции отправления и прибытия, тип тарифа), и ожидаемые результаты (цена недельного проездного, который бы покрыл поездку, цена одноразового билета и цена с Oyster Pay As You Go). Не придирайтесь к цифрам, я их не проверял :) И ещё здесь daily cap не учитывается.
Отлично - мне не пришлось писать десяток почти одинаковых методов со странными названиями вроде JourneyPrice_from_Barking_to_Waterloo_OffPeak(). Но возникает другая проблема: допустим, у меня есть несколько сотен тестов, а класс Journey работает достаточно медленно. У меня нет никакой возможности выбрать, какую группу тестов запускать. Да и не только в этом дело. Даже если все тесты прогоняются быстро, в test runner не сразу понятно, что именно провалилось, они же все относятся к одному методу и классу.
Чтобы решить эту проблему, я создал абстрактный класс JourneyTestBase. В нем находятся атрибуты [TextFixture], [SetUp], [TearDown] и [Test]. В [SetUp] я один раз создаю экземпляр Journey (потому что он долго инициализируется). Кроме того, у меня есть свойство
public string Origin { get; set; }
и реализация теста JourneyPrice()
[Test]
public virtual JourneyPrice(string destination, RateType rateType, decimal weeklyTravelCardPrice, decimal cashFare, decimal oysterPaygFare)
{
var journey = new Journey(this.Origin, destination, rateType);
// здесь может быть ещё какой-то длинный код
Assert.AreEqual(journey.WeeklyTravelCardPrice, weeklyTravelCardPrice);
Assert.AreEqual(journey.CashFare, cashFare);
Assert.AreEqual(journey.OysterPaygFare, oysterPaygFare);
}
Обратите внимание, что origin теперь не передается как аргумент, вместо этого я использую this.Origin.
Наследую десяток классов, вроде такого:
public class Barking : JourneyTestBase
{
public Barking()
{
this.Origin = "Barking";
}
[Row("Waterloo", RateType.Peak, 3.00, 4.00, 2.70 )]
[Row("Waterloo", RateType.OffPeak, 3.00, 4.00, 2.50 )]
public void JourneyPrice(string destination, RateType rateType, decimal weeklyTravelCardPrice, decimal cashFare, decimal oysterPaygFare)
{
base.JourneyPrice(destination, rateType, weeklyTravelCardPrice, cashFare, oysterPaygFare);
}
}
Я разбил все тесты по станциям отправления. Конечно, в зависимости от задачи, можно это сделать по какому-то другому признаку. Например, по типу оплаты (проездной, одноразовый билет, Ойстер), по тарифу (час пик, выходные, студент, пенсионер) или по комбинации станции отправления и прибытия (тогда бы класс назывался Barking_Waterloo ).
В дочерних классах нет никакой логики, только входные данные и ожидаемые результаты. Мне пришлось написать конструктор, но в принципе можно было бы извратиться и извлекать название станции прямо из имени класса. Ещё пришлось перекрыть функцию JourneyPrice и вызвать JourneyPrice базового класса, но реально я это не набирал руками. Просто написал override, и Visual Studio всё сгенерировала за меня.
Теперь, когда я запускаю всё это, то сразу вижу: Barking красный, Waterloo зеленый... Кстати, именно для удобства запуска в test runner (в моем случае Gallio) я не стал давать классам длинные имена, вроде BarkingOriginTestFixture - их тяжело читать.
Маленькое неудобство: базовый класс тоже показывается в Gallio, хотя он и абстрактный. Но это не вызывает ошибку, он всегда показывается зеленым.
Вы скажите, а зачем я вообще связался с наследованием? Ведь можно было сделать один класс с вспомогательным методом JourneyPrice(), и кучу тестовых методов вроде Barking() и Waterloo(). Да, это так, но представьте, что кроме цены, наш класс Journey умеет выдавать ещё что-нибудь. Например, минимальную и среднюю продолжительность поездки, количество пересадок, расстояние, список достопримечательностей по пути... Наследование помогает сгруппировать тесты. И в test runner ты их видишь так:
Barking: Price, Duration, Distance
Waterloo: Price, Duration, Distance
Я не эксперт в тестировании - вполне может быть, что мой подход не самый красивый. Но мне понравилось. Первый день работать не мог - всё время хотелось запустить свои 800 тестов и просто наблюдать как Gallio по ним бежит, постепенно окрашивая всё в зеленый цвет...
P.S. В версии 3 поменялся синтаксис, он стал более аккуратным и простым. Например, вместо атрибута [RowTest] теперь используется обычный [Test]. Так же, как и раньше, данные для такого теста передаются через атрибут [Row]. Сравнение коллекций и прочие навороты теперь делаются через обыкновенный класс Assert, например: Assert.AreElementsEqualIgnoringOrder(....) Если у Вас уже есть много тестов, написанных с использованием старого синтаксиса, то надо подключить MbUnit.Compatibility.dll
1 комментарий:
Row Tests уже добавили в NUnit 2.5
Отправить комментарий