Страницы

воскресенье, 25 марта 2012 г.

Эффективное тестирование. Скажи нет мock’ам – NHibernate нам в помощь.

  В прошлый раз я завел часы (IClock) в тестовый проект, и заставил их исправно работать в пользу тестов. Теперь время за вторым объектом инфраструктуры – NHibernate в лице ISessionFactory. Кто читал мои сообщения про NHibernate и SQLite – наверняка знает, что я собираюсь предложить. NHibernate – это как раз пример весьма хорошей инфраструктуры, которая позволяет тестировать быстро.

  NHibernate = быстрые тесты доступа к базе данных.

  Я буду использовать вспомогательный класс из своей библиотеки Enhima - SQLiteInMemoryTestHelper. Он открывает сессии на открытом соединении SQLite in memory, поэтому сохранённые данные не будут потеряны. Кроме того, нам понадобится ввести дополнительный интерфейс, ISessionManager. Просто потому, что реализовывать полноценный ISessionFactory было бы слишком накладно. (Хотя можно подумать о Castle.DynamicProxy – но это сложнее, а избыточная сложность нам не к чему). ISessionManager включается в себя лишь два метода от ISessionFactory:

public interface ISessionManager
{
    ISession OpenSession();
    IStatelessSession OpenStatelessSession();
}
public class EnhimaSessionManager : ISessionManager
{
    private readonly SQLiteInMemorySchemaMaker _schemaMaker;
    public EnhimaSessionManager(SQLiteInMemorySchemaMaker schemaMaker)
    {
        _schemaMaker = schemaMaker;
    }
    public ISession OpenSession()
    {
        return _schemaMaker.OpenSession();
    }
    public IStatelessSession OpenStatelessSession()
    {
        return _schemaMaker.OpenStatelessSession();
    }
}

  Удалённые вызовы.


  Осталось поговорить о последний зависимости – IRateProvider. Как я и говорил раннее, IRateProvider – это удалённый сервис. Использовать удалённый сервис, даже специально предназначенный для тестирования не стоит как минимум по двум важнейшим причинам:



  • Детерминизм. Сеть может быть недоступена, на сервере проводятся регламентные работы. Тысячи причин по которым сервис может оказаться недоступен. И любая из них приведёт к ошибкам в тестах, хотя наш код по-прежнему работает.
  • Скорость. Удалённые вызовы всегда медленные, на 4-5 порядков медленнее внутрипроцессных вызовов. А полное тестирование системы как правило вызывает удалённые сервисы сотни раз. Тесты начинают занимать минуты - непозволительно долго.

  Поэтому выбора нет – необходимо написать заглушку. Не стоит пытаться сделать заглушку на интерфейс удалённой системы. Лучше сделать заглушку только на собственный интерфейс адаптера, и использовать во всех тестах времени разработки. При этом сам адаптер под реальную систему должен разрабатываться в отдельном проекте, и подключать к проекту уже готовую сборку адаптера. Степень сложности реализованной заглушки зависит от сложности задачи, и вполне возможно имеет смысл написать многорежимную заглушку: статический тестовый режим, и режим эмуляции реального сервиса. Почему нужен первый режим понятно думаю понятно – так проще тестировать. Чтобы понять, зачем нужен второй представим ситуацию, что мы показываем клиенту нашу систему, и внезапно, отключается сеть. Тогда у нас есть два варианта ответа:



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

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

public interface IRateProvider
{
    decimal GetRateOn(DateTime date);
}
public class RateProviderStub : IRateProvider
{
    public decimal Rate;
    public RateProviderStub()
    {
        Rate = 32.5m;
    }
    public decimal GetRateOn(DateTime date)
    {
        return Rate;
    }
}

  Собираем всё вместе.


  Теперь у нас всё есть для написания нового теста, основанного на использовании инфраструктуры и заглушек:

[Test]
public void Ensure_valid_prices_would_be_updated()
{
    var configuration = new Configuration().DataBaseIntegration(db => db.LogSqlInConsole = true );
    configuration.ConfigureSQLiteInMemory();
    configuration.MapEntities(From.ThisApplication());
    var testHelper = new SQLiteInMemoryTestHelper(configuration);
    testHelper.CreateSchema();
    var manager = new EnhimaSessionManager(testHelper);
    var today = Clock.FixedTime.Today;
    var yesterday = today.AddDays(-1);
    var priceToUpdate = new Price(10, yesterday) { ValidFrom = yesterday, ValidTo = today };
    var priceToSkip = new Price(20, yesterday) { ValidFrom = yesterday, ValidTo = yesterday };
    testHelper.Persist(priceToUpdate);
    testHelper.Persist(priceToSkip);
    var rateProviderStub = new RateProviderStub();
    var task = new UpdatePricesTask(manager, Clock.FixedTime, rateProviderStub);
    task.Run();
    var savedPriceToUpdate = testHelper.Load<Price>(priceToUpdate.Id);
    var savedPriceToSkip = testHelper.Load<Price>(priceToSkip.Id);
    savedPriceToUpdate.Satisfy(x =>
                                x.LocalAmount == 325m &&
                                x.LastUpdated == Clock.FixedTime.Today);
            
    savedPriceToSkip.Satisfy(x =>
                                x.LocalAmount == 0 &&
                                x.LastUpdated == yesterday);
}

  На мой вкус, так намного проще и понятнее, а значит быстрее в реализации и поддержке. Поэтому нет причин использовать mock-объекты. Но чтобы быть честным, упомяну о еще одном параметре – время выполнения тестов. Тест с mock-объектами выполняется примерно 110 мс, а тест с инфраструктурой и заглушками примерно 950 мс (+ 2000мс на инициализацию NHibernate, но это время может быть размазано время тестов всех тестов). Да, время увеличилось, но увеличилось приемлемо. Кроме того, увеличился и объём тестируемого кода, а значит потребуется меньше тестов. Всё это существенно повышает производительность труда.


  PS. Пока я писал полуинтеграционный тест – обнаружил ошибку в запросе, которую не мог обнаружить mock-тест. Улыбка

Комментариев нет:

Отправить комментарий