outcoldman
outcoldman Denis Gladkikh

Тестирование NHibernate приложений на примере MbUnit

.NET, C#, MbUnit, NHibernate, TDD, Test, and ORM

Несколько раз встречал подобный вопрос: как тестировать приложения, использующие 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

Have feedback or questions? Looking for consultation?

My expertise: MongoDB, ElasticSearch, Splunk, and other databases. Docker, Kubernetes. Logging, Metrics. Performance, memory leaks.

Send me an email to public@denis.gladkikh.email.

The content on this site represents my own personal opinions and thoughts at the time of posting.

Content licensed under the Creative Commons CC BY 4.0.