Страницы

понедельник, 13 февраля 2012 г.

Эффективное тестирование. Инструменты.

  Каждый мастер должен иметь в своём арсенале надёжные и удобные инструменты. И для тестирования .NET приложений инструментов много MSTest, NUnit, MbUnit, XUnit и даже свой DSL для написания тестов – T#. Список не полный, но думаю понять объёмы популярности тестирования достаточно. Я не собираюсь устраивать сравнения между ними, слишком много для этого надо написать. Но для начала я бы рекомендовал NUnit. Он наиболее популярен в .NET мире, и наиболее прост для начинающего (опытные смогут выбрать сами Улыбка).  Просто скачайте установщик с официального сайта, подключите к проекту ссылку на nunit.framework.dll. Всё готово для первого  теста. Допустим, мы делаем класс, реализующий функционал работы с версиями. У класса  есть четыре целочисленных свойства Major, Minor, Build и Revision. Конструктор класса должен принимать разделённую точками строку, как это принято в AssemblyVersionAttribute. Тогда тест может выглядеть вот так:
[TestFixture]
public class VersionFixture
{
    [Test]
    public void Constructor_parses_string_properly()
    {
        var version = new Version("1.2.330.400");

        //Обратите внимание, все сообщения консоли будут выведены в GUI NUnit
        //Часто таким способом полезно "логгировать" прохождене теста
        Console.WriteLine("Major={0} Minor={1} Build={2} Revision = {3}", version.Major, version.Minor, version.Build, version.Revision);

        Assert.AreEqual(1, version.Major);
        Assert.AreEqual(2, version.Minor);
        Assert.AreEqual(330, version.Build);
        Assert.AreEqual(400, version.Revision);
    }
}

  Скомпилируйте проект, запустите NUnit,  откройте скомпилированную dll с тестами, и жмите кнопку Run. В итоге должны увидеть нечто подобное:

image



  Ошибочка, тест не прошёл. NUnit показывает, на какой строке произошла ошибка. Видимо я ошибся в “парсере” строки. Ладно, поправим код и попробуем запустить еще раз:

image

  Так гораздо лучше! Я нашёл ошибку в процессе подготовки кода примеров для этого сообщения. Улыбка

  Тесты выше можно переписать с помощью нового  FluentAPI NUnit:
[TestFixture]
public class VersionFixture
{
    [Test]
    public void Constructor_parses_string_properly_with_fluent_API()
    {
        var version = new Version("1.2.330.400");

        //Обратите внимание, все сообщения консоли будут выведены в GUI NUnit
        //Часто таким способом полезно "логгировать" прохождене теста
        Console.WriteLine("Major={0} Minor={1} Build={2} Revision = {3}", version.Major, version.Minor, version.Build, version.Revision);

        Assert.That(version.Major, Is.EqualTo(1));
        Assert.That(version.Minor, Is.EqualTo(2));
        Assert.That(version.Build, Is.EqualTo(330));
        Assert.That(version.Revision, Is.EqualTo(400));
    }
}
  Список ниже не полный, но содержит наиболее часто используемые атрибуты:

  • TestFixture – Используется NUnit’ом для поиска классов тестов в сборке.
  • TestFixtureSetUp – метод выполняется один раз перед всеми тестами.
  • SetUp – метод выполняется перед каждым тестом.
  • Test – обозначает метод-тест.
  • TearDown - метод выполняется после каждого теста.
  • TestFixtureTearDown - метод выполняется один раз после всех тестами.
  • Ignore – не выполнять тест. Можно помечать методы класс.
  • Explicit – тест выполнится только при явном запуске из GUI. Можно помечать методы и класс.

  NUnit создаёт один экземпляр класса, отмеченного атрибутом TestFixture, и использует его для всех тестов в этом классе. Поэтому важно перед каждым тестом инициализировать и очищать состояние класса. Это можно сделать с помощью атрибутов Setup/Teardown TestFixtureSetUp/TestFixtureTearDown.

  Иногда тест долго не удаётся сделать “зелёным”. Тогда можно временно исключить его из набора тестов с помощью атрибута Ignore. Почему просто не закомментировать? Потому что с атрибутом NUnit выдаст информативное сообщение, какие тесты временно отключены. Это хорошее напоминание – такие тесты надо как можно скорее приводить в рабочее состояние, и убирать атрибут Ignore. Explicit делает то же самое. Его применяют для демонстрации явных намерений “этот тест выполнятся, только если именно его и хотят запустить”.

  Отличное описание атрибутов и Assert-методов есть в статье про NUnit 2.5. Настойчиво рекомендую ее изучить, и периодически заглядывать. Знание возможных вариантов будет весьма полезных для выбора наиболее подходящего к конкретному случаю, и позволит существенно упростить код тестов.

  Не сводите все тесты к вызову Assert.True()/Assert.False(), например так: 
[TestFixture]
public class VersionFixture
{
    [Test]
    public void Constructor_parses_string_properly_in_the_wrong_way()
    {
        var version = new Version("1.2.330.400");

        //Обратите внимание, все сообщения консоли будут выведены в GUI NUnit
        //Часто таким способом полезно "логгировать" прохождене теста
        Console.WriteLine("Major={0} Minor={1} Build={2} Revision = {3}", version.Major, version.Minor, version.Build, version.Revision);
        
        //Так делать очень плохо, т.к.. в итоге сообщение об ошибке будет невыразительным
        Assert.IsTrue(version.Major == 1);
        Assert.IsTrue(version.Minor == 2);
        Assert.IsTrue(version.Build == 330);
        Assert.IsTrue(version.Revision == 400);
    }
}

  В этом случае будет ничего не говорящее о сути происходящего сообщение:

Expected: True
But was: False

  Используйте подходящий случаю Assert-метод (или его FluentAPI аналог). Не ленитесь, напишите несколько строк в сообщение об ошибке в методе Assert. В случае сломанного теста это сообщение сильно облегчает понимание проблемы.

  Еще один штрих – все мы любим работать в Visual Studio, и хотелось бы запускать тесты прямо из IDE. Решений много, могу лишь упомянуть Resharper, TestDriven.Net, и еще бесплатный Visual NUnit 2010. Совсем простой вариант – настроить запуск NUnit как External Tool. (Кстати, таким образом можно еще и отлаживать код тестов).

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

понедельник, 6 февраля 2012 г.

Эффективное тестирование. Мотивация.

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

  Увы, огромное число команд до сих пор не используют автотесты в своей работе, и даже не готовы рискнуть. Многие предложения были отвергнуты именно по этой причини. Печальный факт. Это первая причина, по которой я решил написать эту серию блогов – возможно кто-то рискнёт ( и обязательно выиграет! ). Ниже причины, по которым я пишу, и не собираюсь отказываться от тестов:

  • Нам это надо. Нам – это программистам. Это наиболее важная причина. Мы всё равно тестируем: тыкаем кнопки в приложении, пишем консоль протестировать чужую библиотеку, или создаём тестовую форму проверки своего алгоритма.
  • Сокращается время обратной связи. Работоспособность кода можно проверить в считанные секунды. Это намного быстрее, чем ручное тестирование. А свежий код легче править, пока каждая строчка живёт в голове.
  • Улучшает архитектуру. Если сложно написать автоматизированный тест – значит грабли закрались в рабочий код. Это как сигнал тревоги – код в опасности.
  • Защищает от будущих ошибок. Конечно, только в том случае тесты регулярно прогоняются на рабочем коде. Сложно даже вспомнить, сколько раз это спасало меня от ошибок в коде.

  Чтобы быть честным до конца, стоит всё-таки признать: на начальном этапе вхождения в автоматизированное тестирование время разработки увеличивается в 3-4 раза. При интенсивном использование этот период длится 2-3 месяца, и потом время сокращается до 1.5-2 раз. Так стоит ли автоматизированное тестирование таких затрат? Стоит. Это вложение в надёжность и устойчивость программы. Тесты можно сравнить с фундаментом. Без него построить дом гораздо быстрее и дешевле. Но тогда не стоит удивляться последствиям.

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

  В последнее время я настолько привык писать тесты, что работа без них меня пугает. Да, именно так. Я боюсь писать непроверенный код. Как убедиться в работоспособности? Как рефакторить без вреда? Как быстро я могу проверить? Эти вопросы постоянно крутятся в голове при виде не покрытого тестами кода.

  На этом хватит введения. Более подробно можно прочитать во многих книжках, но я бы рекомендовал две:  Test Driven Development: By Example и The Art Of Unit Testing. Причём именно в такой последовательности. Кент Бек даёт хорошую идеологию модульного тестирования, а Рой Ошеров практическую базу для тестирования .net, и систематизирует основные ошибки при написании тестов.

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

суббота, 4 февраля 2012 г.

HNibernate. Enhima. SQLite in-memory в две строчки. Где моя сессия?

  После прошлого сообщения мы знаем, как экспортировать схему для тестирования SQLite im-memory. Ок. Но как работать с сессиями? Давайте посмотрим на интерфейс SQLiteInMemorySchemaExport.

/// <summary>
/// Returns session associated with current SQLite connection
/// </summary>
public ISession CurrentSession { get;}
/// <summary>
/// Returns stateless session associated with current SQLite connection
/// </summary>
public IStatelessSession CurrentStatelessSession {get;}
/// <summary>
/// Creates new session associated with current SQLite connection
/// </summary>
public ISession OpenSession()
/// <summary>
/// Creates new stateless session associated with current SQLite connection
/// </summary>
public IStatelessSession OpenStatelessSession()

  Полагаю, сейчас у читателя возник вопрос: есть CurrentSession и OpenSession() – что из них следует использовать? Ответ: в большинстве случаев CurrentSession. Но если вы тестируете компонент, который по какой-либо причине вызывает Dispose() на сессии – то передавать ему CurrentSession нельзя, т.к. никто больше до нее не доберётся. В таких случаях компоненту стоит передать созданную методом OpenSession() сессию. И CurrentSession и все созданные OpenSession() получают одно и тоже соединение с БД, поэтому потери данных не произойдёт.


  Удачных тестов с Enhima!

четверг, 2 февраля 2012 г.

HNibernate. Enhima. SQLite in-memory в две строчки.

  В предыдущем сообщении я рассказывал про основы тестирования NHibernate с использованием  SQLite in-memory. А сейчас как и обещал, расскажу, как проводить тестирование в хорошем, не загромождающем код тестов инфраструктурными деталями стиле.

  Кто читал книжку Джейсона Дентлера (Jason Dentler) наверно заметил, насколько огромен и сложен код теста. В одном из прошлых проектов я реализовывал нечто подобное. Класс работал, и выполнял свои функции. Но мне казался неуклюжим.  Остались две важные претензии:

  • Базовый класс сам по себе большой и сложный (большее 300 строк кода).
  • Мешает пользоваться другими базовыми классами (нет желания копировать его из проекта в проект). Улыбка

  Поэтому для проекта Enhima решил создать новый компонент, в основу которого положил принципы легковесности, минимального влияния на код теста, никаких ужасных базовых классов-переростков, никаких конфигураций. Сделать схему перед тестом, и удалить после. Всё! Так родился SQLiteInMemorySchemaMaker. Он прост в использовании, его можно легко прикрутить к любому тесту. Например так:

[TestFixture]
public class DomainMapperFixture 
{
    private Configuration _config;
    private SQLiteInMemorySchemaMaker _schemaMaker;
    [TestFixtureSetUp]
    public void TestFxtureSetup()
    {
        _config =  new Configuration();
        _config
            .ConfigureSQLiteInMemory()
            .MapEntities(From.ThisApplication());
        _schemaMaker = new SQLiteInMemorySchemaMaker(_config);
    }
    [SetUp]
    public void Setup()
    {
        _schemaMaker.CreateSchema();
    }
    [TearDown]
    public void TearDown()
    {
        _schemaMaker.DropSchema();
    }
    [Test]
    public void Do_the_test()
    {
       //Skipped 
    }
}

  Как видно, код теста остался весьма скромен. И меня это очень радует. Улыбка  Если понравилось, можно начинать пользоваться проектом Enhima уже сейчас (доступен через nuget).

среда, 1 февраля 2012 г.

Nhibernate. Тестируем быстро c SQLite.

  Кому приходилось писать интеграционные тесты с базой данных, тот познал трудности таких тестов. На то есть две основные причины.
Очистка базы данных для каждого теста.
  Пересоздание базы данных весьма долгое (по времени выполнения) и трудоёмкое занятие. С годами выработались основные подходы к решению этой проблемы:
  • Создавать базу из эталонной. 
  • Откатывать транзакцию по завершении тестов. 
  • Ломать/Создавать (Drop/Create) схему БД перед каждым тестом. 
  Полноценный анализ этих методов выходит за рамки этого поста, поэтому дам лишь свои краткие субъективные оценки.  Первый вариант самый медленный и сложный (БД долго поднимать из бекапа, и трудно поддерживать бекап в актуальном состоянии).  Второй быстрее, но мне он кажется некрасивым. Да еще возможно проблемы при распределённых транзакциях (привет интеграционному тестированию с использованием удалённых WCF-сервисов). Последний вариант самый простой и понятный (особенно для NHiberate с использованием SchemaExport). 

Скорость выполнения тестов. 
  С проблемой скорости дело обстоит сложнее. Межпроцессные вызовы всегда длятся на несколько порядков медленнее внутрипроцессных, сервера БД склонны записывать состояние БД в файл, что тоже отнимает время. И это серьёзные трудности на пути к эффективному тестированию работы с БД. К счастью, для пользователей NHibernate есть решение проблемы - SQLite  in-memory. Давайте попробуем. Вот пример простого теста с использование in-memory SQLite.
[Test] 
public void In_memory_fails_on_no_such_table()
{
    var config = new Configuration();

    config
        .ConfigureSQLiteInMemory()
        .MapEntities(From.ThisApplication());

    var factory = config.BuildSessionFactory();

    var schemaExport = new SchemaExport(config);

    schemaExport.Create(true, true);

    using(var session = factory.OpenSession())
    using (var tx = session.BeginTransaction())
    {
        var product = new Product {Name = "Supercomputer"};
        session.Persist(product);
        tx.Commit();
    }
}

  Выглядит несложно, но... не работает. Падает с ошибкой "no such table HighLowGenerator". Хотя в скрипте есть dml-запрос на создание таблицы. Сначала вопрос куда делась "such table" может вводить в ступор. Дело в том, что схема БД (и все данные в ней) существует в памяти   пока открыто соединение. Как только соединение закрыто - всё, все данные и схема уничтожены. Это происходит в и нашем случае. SchemaExport честно создаёт схему БД в памяти, но после этого закрывает соединение. Сессия работает с новым открытым соединением, в котором нет ничего, даже таблиц. Вот и получаем no such table. Можно ли помочь беде!? Конечно! :)
  Сессия NHibernate не открывает и не закрывает внешние соединения, переданные ей извне (т.е. в метод ISessionFactory.OpenSession()). Достаточно лишь немного исправить код:
[Test] 
public void In_memory_succeeded()
{
    var config = new Configuration();

    config
        .ConfigurSQLiteInMemory()
        .MapEntities(From.ThisApplication());

    var factory = config.BuildSessionFactory();

    var connection = new SQLiteConnection("Data Source=:memory:;Version=3;New=True;");
    connection.Open();

    var schemaExport = new SchemaExport(config);

    schemaExport.Execute(true, true, false, connection, null);

    using(var session = factory.OpenSession(connection))
    using (var tx = session.BeginTransaction())
    {
        var product = new Product {Name = "Supercomputer"};
        session.Persist(product);
        session.Flush();
        tx.Commit();
    }
}
  Это работает, и быстро, практически мгновенно! Ниже в таблице сравнительный тест разных конфигураций SQLite (среднее за 100 прогонов на создание/удаление схемы БД для тестового проекта Enhima). "Анализ производительности" не претендует на точность, но представление о времени работы даёт.

Сравнительное время работы SQLite в разных конфигурациях.
In memory Virtual drive Real drive
Экспорт схемы, мс 2 19 1053
Полное время теста, мс 246 1929 105246
Способ очистки БД Закрытие соединения Удаление файла Удаление файла

  Конфигурация im-memory примерно на порядок быстрее, чем виртульный диск, и на три порядка быстрее настоящего диска. Так что у пользователей NHibernate нет повода не использовать SQLite для тестирования. :)
  Теоретические основы тестирования в связке NHibernate + SQLite я изложил. В следующий раз расскажу, как правильно организовать классы для тестирования с SQLite in-memory.