Страницы

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

AssemblyVersion и git. Давайте жить дружно.

Проблема.

  Несколько месяцев назад я рассказывал, как удобно и безболезненно создавать AssemblyVersion для проекта. Теперь настало время рассказать о решении второй части задачи – версионирование сборок и nuget-пакетов. Решение ограничено двумя условиями:
  • Система контроля версий – git.
  • Версия сборки и пакета должна хранится в git, или генерироваться из метаданных git.
  На выходе надо получить:

 

Решение

  Идею я позаимствовал у UppercuT, и немного ее расширил.  В основе лежат аннотированные теги git’a. Название тега должно соответствовать соглашению по именованию – Major.Minor-PrereleaseVersion, например 0.9-beta. Далее буду продолжать на основе репозитория enhima, и с использованием консоли git bash. Сначала поставим тег на первый commit в репозитории git.
Juso@ULTIMA /v/enhima (master)
$ git tag -a 0.9-beta f262324 -m "first public beta"

Juso@ULTIMA /v/enhima (master)
$ git tag
0.9-beta

  Где  f262324 – первые семь символов хеша ревизии, на которую надо повесить тег. Как видно, команда git tag показывает все теги в репозитории, но не даёт двух вещей: номера ревизии и хеша ревизии. К счастью, в git есть более полезная команда: git describe.
Juso@ULTIMA /v/enhima (master)
$ git describe --abbrev=64
0.9-beta-21-gf8542552ae0e9c7e478fe9ac3e6a02c2060e965a

  И в итоге мы получим все необходимые данные одной командой, остаётся только их разобрать с помощью регулярных выражений. Честно говоря, до этой задачи мне не приходилось плотно работать с регулярками, но rubular научит любого  строить регулярку за полчаса. Итак, у нас есть всё что нужно для версионирования:

image

  Хотя у вас наверно возник вопрос, откуда взялась цифра 21? Это количество commit’ов, сделанных после последнего аннотированного тега. Она конечно не является уникальным номером ревизии, но для целей создания AssemblyVersion подойдёт. А уникальность можно определить по хешу.

Генерируем AssemblyVersion с помощью rake


  Пора сделать последний шаг. Автоматизировать сборку скрипта с помощью какого-либо build-инструмента. Мой фаворит в последнее время – rake + albacore. (Как начать работу с rake для .net-программиста). И вот что у меня получилось:
desc "Generate solution version "
assemblyinfo do |asm|
#Assuming we have tag '0.9-beta' git describe --abbrev=64 returns 0.9-beta-18-g408122de9c62e64937f5c1956a27cf4af9648c12
#If we have tag '0.9' git describe --abbrev=64 returns 0.9-18-g408122de9c62e64937f5c1956a27cf4af9648c12
#It should be parsed by  to generate correct assembly and nuget package version

 output = `git describe --abbrev=64`
 version_parts = output.match /(\d+).(\d+)-?([a-zA-Z]*)-(\d+)-(\w{7})/
 major = version_parts[1]
 minor = version_parts[2]
 revision = version_parts[4] || 0
 version_type = version_parts[3]
 hash = version_parts[5]
 
 @version = "#{major}.#{minor}.#{revision}"
 @package_version = @version
 @package_version += "-#{version_type}"  if (version_type || "").length > 0
 
 @product_version = @version
 @product_version += ("-#{version_type}"  if (version_type || "").length > 0) + "-#{hash}"
 
 asm.version = @version
 asm.file_version = @version
 asm.custom_attributes :AssemblyInformationalVersionAttribute => @product_version
 asm.output_file = "SolutionVersion.cs"
 asm.description = @description
end

  И результат работы:
  1: using System.Reflection;
  2: using System.Runtime.InteropServices;
  3: [assembly: AssemblyDescription("Lightweight near zero-configuration library for easy conventional mapping for nhibernate.")]
  4: [assembly: AssemblyVersion("0.9.21")]
  5: [assembly: AssemblyFileVersion("0.9.21")]
  6: 
  7: [assembly: AssemblyInformationalVersionAttribute("0.9.21-beta-gf85425")]
  8: 
  9: 

  Именно то, что требовалось. Полную версию скрипта можно найти в репозитории enhima. Надеюсь это поможет вам подружить git и .net. :)

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

Метафоры против наукообразия.

  Я устал. Устал читать однообразно-наукообразный псевдопонятный многобуквенный код. Эту фразу было неудобно читать, да? Она по сути ничего не значащая, зато насыщена “умными” словами.  Вот один из скромных образцов кода, который вызывает у меня столько нелестные чувства:

public interface ITimeService
{
    DateTime Now { get; }
}

  На первый взгляд ничего особенного в нём нет. Но у меня есть один маленький вопрос: какую метафору из жизни олицетворяет ITimeService? Городскую службу по обслуживанию уличных часов? Службу времени 09? Первая ссылка в поиске по time service показывает на сайт мастерской по ремонту часов, а гугл-переводчик  переводит как “срок службы” Улыбка Думаю многие из вас уже догадались, какая метафора правильная:

public interface IClock
{
    DateTime Now { get; }
}

  Намного ближе к жизни, нежели мутный ITimeService, и отражает суть того, как 99% из нас узнают время. (Ладно, у многих сейчас нет часов, и время узнают из телефона. Но как вы назовёте то место в телефоне, где видны заветные цифры текущего времени? Зуб даю – часы). Чтобы сделать различие более явным, приведу куда более прижившийся пример:

public interface IRepository
{
    void Save(object entity);
    object Get(object id);
}

  Полагаю, что-то вроде этого каждый приличный программист видел тысячу раз. Давайте выкинем метафору репозитория, и назовём это “по научному”:

public interface IPersistenceService
{
    void Save(object entity);
    object Get(object id);
}

  Выглядит ужасно, да? К счастью, такого я никогда не видел. И надеюсь никто не видел этого ужаса. И чтобы убедить окончательно, приведу еще один, контрольный пример – NUnit – MSTest. Отцом-идеологом  NUnit был Кент Бек (Kent Beck). Человек с живым умом, выражающий мысли простым ясным языком. И вот его метафоры:



  • TestFixture – испытательный стенд.
  • Setup – cборка; настройка; монтаж.
  • Test – проверка; испытание.
  • TearDown – демонтаж.

    А вот что родили в недрах Microsoft, в муках изобретая новые термины для уже устоявшихся понятий:



  • TestClass.
  • TestInitialize
  • TestMethod
  • TestCleanup

  В переводе не нуждается. Ни намека на метафору. Наименование противоречит даже простому правилу: названия переменных/классов/методов и т.д. должны говорить об их назначении, а не о деталях реализации. Не говоря уже о том, что громадьё атрибутов начинающихся на Test даст под дых любому Intellisense’у.


  Итого: давайте простые названия своим классам, ищите метафоры из реального мира. Никто не называет рот “телесным отверстием для приёма пищи”. Это смешно.  Не делайте таких глупостей.

пятница, 17 февраля 2012 г.

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

  В прошлый раз я рассматривал базовые возможности одной из самых популярных библиотек для модульного тестирования – NUnit. Для этого блога немного модифицировал тесты и тестируемый код, дабы показать возможности других инструментов. Давайте напомню, у нас есть тест класса Version:
[Test]
public void Is_not_good_testing_because_some_asserts_are_missed()
{
    var version = Version.ParseWithError("1.2.3.4");

    Assert.AreEqual(1, version.Major);
    Assert.AreEqual(2, version.Minor);
    Assert.AreEqual(3, version.Build);
    Assert.AreEqual(4, version.Revision);
}

  Пусть в коде будут две ошибки (перепутанные индексы в массиве):
public static Version ParseWithError(string version)
{
    var versionNumbers = version.Split('.');

    var major = Convert.ToInt32(versionNumbers[0]);
    var minor = Convert.ToInt32(versionNumbers[1]);
    var build = Convert.ToInt32(versionNumbers[3]); //error
    var revision = Convert.ToInt32(versionNumbers[2]); //error

    return new Version(major, minor, build, revision);
}

  Запустим тест, и посмотрим,  что нам скажет NUnit:


Expected: 3
But was: 4

  В коде две очевидных ошибки, но NUnit сказал нам лишь об одной из них. Почему? Дело в том, что Assert в случае ошибки бросает исключение, и далее тест не выполняется. До Assert’а свойства Revision тест просто не дошёл. С такой ситуацией можно жить, но плохо. Мы не знаем проблему полностью. Вторую ошибку обнаружим только после исправления первой. Классики тестирования рекомендуют писать только один Assert на тест. Такой способ можно применить и к этой ситуации.  Мы увидим все ошибки, но один простой тест будет размазан на четыре. А что если нам надо проверить 16 свойств? Писать 16 тестов совсем не хочется. Так есть ли способ совместить полноту тестирования с лаконичностью? Конечно есть! Улыбка

SharpTestsEx


  Лидер проекта NHibernate написал весьма симпатичную библиотеку SharpTestsEx (если писать SharpTestSex  запомнить будет легче!). Она позволяет выполнять несколько проверок за один раз. SharpTestsEx активно использует методы расширения и лямбда выражения. SharpTest для выглядит так:
[Test]
public void SharpTestsEx_helps_to_improve_multi_assertion()
{
    var version = Version.ParseWithError("1.2.3.4");

    version.Satisfy(x =>
                    x.Major == 1 &&
                    x.Minor == 2 &&
                    x.Build == 3 &&
                    x.Revision == 4);
}

  И вот сообщение, которые мы получим в итоге:


SharpTestsEx.AssertException : Version = '1.2.4.3' Should Satisfy (x => x.Build == 3)
Compared values was: 4 == 3
And
Version = '1.2.4.3' Should Satisfy (x => x.Revision == 4)
Compared values was: 3 == 4

  Теперь мы видим в сообщения обе ошибки,  указанием свойств и несовпадающих значений в рамках одного теста. Кроме тестов с помощью лямбда-выражений SharpTestsEx содержит еще и неплохое FluentApi для тестирования простых значений (особенна полезно возможность применить к какому-либо значению – строке, числу, дате и др.  несколько Assert’ов). Информацию о них можно найти в документации. Крайне рекомендую иметь этот инструмент в вашем арсенале.

 


FluentAssertions


  В отличии от SharpTestsEx FluentAssertions не позволяет провести несколько тестов за один проход. Зато обладает мощными возможностями для сравнения между собой различных объектов. С его помощью предыдущую задачу можно решить с помощью сравнения с эталоном. Например так:
[Test]
public void FluentAssertions_helps_to_improve_multi_assertion()
{
    var version = Version.ParseWithError("1.2.3.4");

    version.ShouldHave().AllProperties().EqualTo(new Version(1,2,3,4));
}

   В качестве эталонного объекта можно использовать даже анонимный тип:
[Test]
public void FluentAssertions_with_an()
{
    var version = Version.ParseWithError("1.2.3.4");

    version.ShouldHave().AllProperties().EqualTo(
        new {Major = 1, Minor = 2, Build = 3, Revision = 4 });
}

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


Expected property Revision to be 4, but found 3.

  Хочется надеется, в будущих версиях будут проверятся все свойства, и агрегированное сообщение об ошибках. Тем не менее, во многих случаях можно пожертвовать полным выводом ошибок ради лаконичности и красоты. Код тестов нуждается в поддержке, как и основной код системы. А FluentAssertions избавляет от необходимости добавлять каждое новое свойство в код теста. Скажем, если у нас есть еще один класс ShortVersion:
public class ShortVersion
{
    public int Major { get; set; }

    public int Minor { get; set; }
}

  И надо протестировать метод Version.ToShortVersion(). С FluentAssertions можно написать тест так:
[Test]
public void FluentAssertions_test_shared_fields()
{
    var version = Version.Parse("1.2.3.4");

    var shortVersion = version.ToShortVersion();

    shortVersion.ShouldHave().SharedProperties().EqualTo(version);
}

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

  Как и SharpTestsEx, FluentAssertions облаладет отличным FluentAPI для написания выразительного кода для многих  видов Assert.ов. FluentAssertions пришёл в мой код недавно, но с каждым днём применяю всё чаще и чаще. Попробуйте, возможно он еще не раз вас выручит!

  Код примеров лежит здесь.

понедельник, 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.