Тестирование NHibernate приложений на примере MbUnit
- modified:
- reading: 6 minutes
Несколько раз встречал подобный вопрос: как тестировать приложения, использующие ORM NHibernate, точнее встречал проблемы с тестированием на форумах GotDotNet. Для меня проблема не очень понятна, вроде всегда было все просто. Но все же опишу небольшой пост об этом, чтобы в будущем можно было ссылаться на него.
NHibernate предоставляет нам несколько возможностей хранить конфигурацию. Одна из таких возможностей – это конфигурировать все на лету, а именно в коде приложения. В этом случае мы можем создать свой класс Configuration, отнаследовать его от класса Configuration NHibernate и внести необходимую логику по конфигурации приложения, выглядеть это будет примерно так:
public class Configuration : global::NHibernate.Cfg.Configuration
{
#region Singleton
private static readonly object _locker = new object();
private static Configuration _config;
private static ISessionFactory _factory;
/// <summary>
/// Thread safe current NHibernate configuration
/// </summary>
public static Configuration Current
{
get
{
if (_config == null)
{
lock (_locker)
{
if (_config == null)
{
CreateConfiguration();
}
}
}
return _config;
}
}
/// <summary>
/// Create Configuration of NHibernate
/// </summary>
public static void ReConfigurate()
{
if (_config != null)
{
lock (_locker)
{
if (_config != null)
{
CreateConfiguration();
}
}
}
}
private static void CreateConfiguration()
{
_config = new Configuration();
_config.SetProperty(Environment.ConnectionProvider, typeof(DriverConnectionProvider).FullName);
_config.SetProperty(Environment.Dialect, typeof(MsSql2005Dialect).FullName);
_config.SetProperty(Environment.ConnectionString, GetConnectionString());
_config.SetProperty(Environment.Isolation, "ReadCommitted");
_config.SetProperty(Environment.CommandTimeout, "600");
_config.SetProperty(Environment.MaxFetchDepth, "4");
_config.SetProperty(Environment.ProxyFactoryFactoryClass,
"NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle");
_factory = null;
}
/// <summary>
/// Thread safe NHibernate Session Factory
/// </summary>
public static ISessionFactory Factory
{
get
{
if (_factory == null)
{
lock (_locker)
{
if (_factory == null)
{
_factory = Current.BuildSessionFactory();
}
}
}
return _factory;
}
}
#endregion
private static string GetConnectionString()
{
return ConfigurationManager.ConnectionStrings["Main"].ConnectionString;
}
}
Основная информация инициализируется в методе CreateConfiguration, который устанавливает тип базы данных, провайдера, тип изоляции и некоторые другие основные параметры. Согласитесь, что вряд ли вы будете эти параметры менять раз в неделю? В большинстве случаев они устанавливаются при первом создании проекта, и, более того, часто бывает, что эти параметры просто копируют из предыдущих приложений. Единственное, что меняется часто – это ConnectionString, получение которого я вынес в отдельный метод.
Для тестирования создадим простейшую бизнес модель, состоящую из одного класса User:
public class User
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string Surname { get; set; }
}
Маппинг этого класса будет выглядеть так:
<class name="User" table="[User]" >
<id name="Id" unsaved-value="0">
<generator class="identity"/>
</id>
<property name="Name" type="String"/>
<property name="Surname" type="String"/>
</class>
Для работы с сессиями создадим класс DataContext (название позаимствовал из Linq-to-SQL) следующего вида:
/// <summary>
/// Context for work with NHibernate Sessions
/// </summary>
public class DataContext : IDisposable
{
private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
/// <summary>
/// Create new context and open session
/// </summary>
public DataContext()
{
OpenSession();
}
/// <summary>
/// Create new context and open session
/// </summary>
/// <param name="fBeginTransaction">need to begin transaction</param>
public DataContext(bool fBeginTransaction)
{
OpenSession();
if (fBeginTransaction)
BeginTransaction();
}
private ISession _session;
private ITransaction _transaction;
/// <summary>
/// Return current session
/// </summary>
public ISession CurrentSession
{
get
{
try
{
OpenSession();
}
catch (HibernateException e)
{
Log.Error("Cannot open session", e);
throw;
}
return _session;
}
}
/// <summary>
/// Open session if not opened before
/// </summary>
public void OpenSession()
{
if (_session == null)
{
_session = Configuration.Factory.OpenSession();
_session.FlushMode = FlushMode.Commit;
}
}
/// <summary>
/// Close session if it opened
/// </summary>
public void CloseSession()
{
try
{
if (_session != null && _session.IsOpen)
{
_session.Close();
}
_session = null;
}
catch (HibernateException e)
{
Log.Error("Cannot close session", e);
throw;
}
}
/// <summary>
/// Begin transaction if it not started before
/// </summary>
public void BeginTransaction()
{
try
{
if (_transaction == null)
{
_transaction = CurrentSession.BeginTransaction();
}
}
catch (HibernateException e)
{
Log.Error("Cannot begin transaction", e);
throw;
}
}
/// <summary>
/// Commit current transaction if it opened
/// </summary>
public void CommitTransaction()
{
try
{
if (_transaction != null && !_transaction.WasCommitted &&
!_transaction.WasRolledBack)
{
_transaction.Commit();
}
_transaction = null;
}
catch (HibernateException e)
{
Log.Error("Cannot commit transaction", e);
throw;
}
}
/// <summary>
/// Rollback current transaction
/// </summary>
public void RollbackTransaction()
{
try
{
if (_transaction != null && !_transaction.WasCommitted &&
!_transaction.WasRolledBack)
_transaction.Rollback();
_transaction = null;
}
catch (HibernateException e)
{
Log.Error("Cannot rollback transaction", e);
throw;
}
finally
{
CloseSession();
}
}
/// <summary>
/// Commit transaction if requied and close session
/// </summary>
public void Dispose()
{
CommitTransaction();
CloseSession();
}
}
Теперь у нас есть испытуемые, можно приступить к написанию тестов и самого приложения. Замечу, что все классы по работе с NHibernate и классы бизнес логики я положил в библиотеку NHibernateTestConsoleApplication.Core.
Итак, создаем еще одну библиотеку NHibernateTestConsoleApplication.Test, в которой и напишем первый тест:
[TestFixture]
public class UsersTest
{
[TestFixtureSetUp]
public void TestFixtureSetUp()
{
Configuration.ReConfigurate();
Configuration.Current.AddAssembly(typeof (User).Assembly);
}
[Test]
public void UsersShouldBeSave()
{
using (DataContext dataContext = new DataContext(true))
{
User user = new User { Name = "User1_Name", Surname = "User1_Surname" };
dataContext.CurrentSession.Save(user);
}
IList<User> users;
using (DataContext dataContext = new DataContext())
{
users = dataContext.CurrentSession.CreateCriteria<User>()
.Add(Restrictions.Eq("Name", "User1_Name"))
.List<User>();
}
Assert.AreEqual(users.Count, 1);
Assert.AreEqual(users[0].Surname, "User1_Surname");
using (DataContext dataContext = new DataContext(true))
{
dataContext.CurrentSession.Delete(users[0]);
}
}
}
Метод TestFixtureSetUp конфигурирует NHibernate, а так же добавляет все маппинги из библиотеки, в которой находится тип User. UsersShouldBeSave – обычный метод для тестирования логики над пользователями (вообще лучше этот метод разбить на несколько), но для нас важна не эта тема. Так как мы написали метод GetConnectionString в Configuration так, что он обращается к конфигурации app.config, то мы можем просто добавить такой файл к библиотеке NHibernateTestConsoleApplication.Test следующего содержания:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="Main" connectionString="user id=XXX; password=XXX; server=(local); database=XXX;" providerName="System.Data.SqlClient" />
</connectionStrings>
</configuration>
Это самая простейшая конфигурация проекта с использованием NHibernate для тестирования. Чем он хорош – это тем, что у вас всего лишь одна конфигурация на все проекты – основной проект – программы и проект для тестирования. И в случае, если в будущем вы поставите другой timeout в приложении, то тесты будут работать на той же настройке. Чем этот метод конфигурирования плох, если что то хочется поменять в настройке – нужно будет пересобирать проект, отчасти это лечится тем, что некоторые параметры так же как ConnectionString можно вынести в настройки в app.config и иметь какие-то дефолтные значения в случае, если эти настройки не выставлены в app.config.
Так же можно вынести все настройки в app.config и считывать их оттуда. Вариант плох тем, что эти настройки нужно будет синхронизировать между приложениями (тестовым и основным). Так же можно хранить конфигурацию в файле nhibernate.cfg.xml, но вот как раз с этим вариантом будут проблемы с тестированием, так как Domain тестируемого приложения будет находится в корневой папке Visual Studio, а не в папке bin вашего приложения, потому вы будете испытывать трудности в том, чтобы разместить этот файл настройки в необходимый каталог.
Скачать пример: NHibernateTestConsoleApplication.zip