Написание автоматизированных тестов для приложений работающих с БД часто становится источником многочисленных споров. Основная причина этих споров — подходы к тестированию. Можно выделить 3 основных подхода, которые при этом применяются:
-
Абстрагирование от БД. Подход предполагает, что взаимодействия с какими бы то ни было БД в тестах не происходит. Для этого все обращения к БД оборачивают в вызовы, например, IRepository и в тестах эти обращения замещаются с помощью mock-фреймворков (Rhino.Mocks, Moq, etc.). Из плюсов подхода можно выделить популярность паттерна репозиторий, что ускоряет знакомство с методикой. Из минусов — приходится замещать все БД-обращения (иногда их бывает довольно много), а также тесты жестко связываются с интерфесом репозиториев (в случае изменения интерфесов тесты придется также изменять)
-
Работа поверх тестовой БД. В тестах используется база, «похожая» на продакшн-базу, но с некоторыми тестовыми данными. Тесты, таким образом, работают в окружении, максимально близком к реальной среде, что, без сомнения, является большим плюсом. Из минусов можно выделить более низкую скорость выполнения тестов и проблемы поддержания тестовой БД в эталонном состоянии.
-
Работа поверх временных БД. В тестах используются временные базы, чаще всего, способные хранить структуру в памяти (SQLite) или на локальном диске (SQL CE). Из плюсов — высокая скорость работы и «приближенность» к реальному окружению (хоть и менее близкая, чем во втором пункте).
Стоит отметить и общую часть всех трех подходов в «простых» случаях: участки кода, которые не требуют «активного» доступа к БД, а лишь модифицируют сущности доменной модели, тестируются во всех подходах одинаково и взаимодействия с БД просто не требуют.
В своей работе я не использую репозитории, потому что в большинстве случаев они являются ненужной прослойкой, которая лишь усложняет доступ к данным.
Из других подходов, я стараюсь использовать последний, поскольку он наиболее лёгок и гибок в применении. Он особенно удобен при использовании ORM, поскольку в этом случае сам ORM обычно способен создать схему базы в соответствии с определенными маппингами и соглашениями.
Ниже я приведу пример базового тестового класса, при наследовании от которого я получаю временную sqlite базу со структурой, соответствующей текущему проекту, и 3 NHibernate-сессии открытые поверх этой БД. Несколько сессий я использую для удобства проверки коммитов: обычно настройку начальных данных я провожу в sessionArrange, в тестируемый код передается sessionAct и проверки осуществляются через sessionAssert, таким образом все «несохраненные» в тестируемом коде данные из sessionAssert будут просто не видны.
public class InMemoryDb : IDisposable
{
private static Configuration Configuration;
private static ISessionFactory SessionFactory;
protected ISession _sessionAssert;
protected ISession _sessionArrange;
protected ISession _sessionAct;
[SetUp]
public void _Setup()
{
Init();
}
public void Init()
{
if (Configuration == null)
{
var conf = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.AutoMappings.Add(AutomappingGenerator.CreateAutomappings));
//AutomappingGenerator.CreateAutomappings - статичная функция, возвращающая AutoPersistenceModel - конфигурацию NHibernate в текущем проекте.
SessionFactory = conf.BuildSessionFactory();
Configuration = conf.BuildConfiguration();
}
_sessionAct = SessionFactory.OpenSession();
_sessionArrange = SessionFactory.OpenSession(_sessionAct.Connection);
_sessionAssert = SessionFactory.OpenSession(_sessionAct.Connection);
_sessionAct.BeginTransaction();
_sessionArrange.BeginTransaction();
_sessionAssert.BeginTransaction();
var nullWriter = new StringWriter();
new SchemaExport(Configuration).Execute(false, true, false, _sessionAct.Connection, nullWriter);
}
public void Dispose()
{
_sessionAct.Dispose();
_sessionArrange.Dispose();
_sessionAssert.Dispose();
}
}
Второй подход я также использую в случаях, когда воспользоваться третьим не получается :) В этом случае проблему «очистки» БД я решаю путем выполнения всех операций внутри теста в рамках одной транзакции и отката этой транзакции по окончании теста. Базовый класс в этом случае выглядит совсем просто, но более специфично, поэтому, пожалуй, от выкладывания его я воздержусь :)