In my last post i experimented with a way to run my integration tests without polluting my database. I created a specific TransactionFactory which NHibernate would use to create transactions whenever they were requested. My factory returned a transaction which would actually rollback the transaction, instead of committing it. This way, the transactions were not committed during test runs, and the code being tested did not have to be changed. It works, but it gets messy very quickly if an integration test would call multiple methods that were transactional. The test transaction would have to stay alive during the entire test, and would have to be reused by all the transactional methods called in that test, all without changing the code being tested. It's doable, but i didn't feel too good about it.
I looked for other options and i believe i've found a pretty good one. It would be great if i could somehow wrap every data access call caused by a test into one MS DTC transaction. Then, after the test i'd simply have to abort the MS DTC transaction. The database stays clean, the tests remain independent of what may already be in the database and most importantly, i wouldn't have to change my production code.
So, what do i need here? First i need a way to connect an NHibernate session to the current DTC transaction. And i also need a custom TransactionFactory for NHibernate to use which will return dummy transaction objects which do nothing. The production code will receive these transaction dummies whenever a transaction is requested and will call Commit() on them when needed. Obviously, the Commit() implementation of my dummy transaction is a no-op.
Let's start with connecting an NHibernate session to a DTC transaction. NHibernate provides access to the underlying database connection through the ISession.Connection property which returns an IDbConnection. Most objects that implement IDbConnection have an EnlistDistributedTransaction() method, which does exactly what we want, but it's not defined in the IDbConnection interface. The only one that doesn't have this method as far as i know, is the implementation in the System.Data.SqlServerCe namespace. I don't use SqlServerCe so that's not a problem for me.
I want my production code to request NHibernate sessions through my own provider which implements the following interface:
public interface ISessionProvider
{
public ISession GetNewSession();
}
At runtime, the real SessionProvider will be used which will provide NHibernate sessions which use the default AdoNetTransactionFactory and AdoTransactions. At unit-test time, the following ISessionProvider will be used:
public class SessionUsingDtcProvider : ISessionProvider
{
private ISessionFactory _sessionFactory = null;
private ISessionFactory SessionFactory
{
get
{
if (_sessionFactory == null)
{
NHibernate.Cfg.Configuration configuration =
new NHibernate.Cfg.Configuration()
.AddAssembly("MyMappingAssembly");
ISessionFactory sessionFactory =
configuration.BuildSessionFactory();
// overwrite the NHibernate TransactionFactory so it will use our
// own factory. check the previous post if you don't know why
// i do it this way
PropertyInfo propertyInfo = sessionFactory.Settings.GetType()
.GetProperty("TransactionFactory");
propertyInfo.SetValue(sessionFactory.Settings,
new TestTransactionFactory(), null);
}
return _sessionFactory;
}
}
public ISession GetNewSession()
{
ISession session = SessionFactory.OpenSession();
EnlistInDtcTransaction(session.Connection);
return session;
}
private void EnlistInDtcTransaction(IDbConnection connection)
{
MethodInfo methodInfo = connection.GetType()
.GetMethod("EnlistDistributedTransaction",
BindingFlags.Public | BindingFlags.Instance);
methodInfo.Invoke(connection, new object[]
{ (System.EnterpriseServices.ITransaction)
ContextUtil.Transaction
});
}
}
So the EnlistDistributedTransaction() method is called through reflection because it's not defined in the interface. I wouldn't do this for production code, but for test-code i think it's OK.
The TestTransactionFactory class looks like this:
public class TestTransactionFactory : ITransactionFactory
{
public void Configure(System.Collections.IDictionary props)
{
}
public ITransaction CreateTransaction(ISessionImplementor session)
{
return new TestTransaction();
}
}
And the TestTransaction is as dumb as it possibly could be:
public class TestTransaction : ITransaction
{
public void Begin(System.Data.IsolationLevel isolationLevel)
{
}
public void Begin()
{
}
public void Commit()
{
}
public void Enlist(System.Data.IDbCommand command)
{
}
public bool IsActive
{
get { return true; }
}
public void Rollback()
{
}
public bool WasCommitted
{
get { return false; }
}
public bool WasRolledBack
{
get { return false; }
}
public void Dispose()
{
}
}
Now suppose you have the following code in your business layer:
public class CustomerService
{
private ISessionProvider _sessionProvider;
public CustomerService(ISessionProvider sessionProvider)
{
_sessionProvider = sessionProvider;
}
public void SaveCustomer(Customer customer)
{
using (ISession session = _sessionProvider.GetNewSession())
using (ITransaction transaction = session.BeginTransaction())
{
session.SaveOrUpdate(customer);
session.Flush();
transaction.Commit();
}
}
}
As you can see, this code requests a transaction and commits the transaction after the data is inserted or updated in the database. At runtime, this is exactly what i want. But during my tests, i don't want this to actually be commited.
So now i could test this without polluting my database like this:
[TestClass]
public class MyTestClass
{
[TestInitialize]
public void TestInitialize()
{
SetUpDtcTransaction();
}
private void SetUpDtcTransaction()
{
ServiceConfig serviceConfig = new ServiceConfig();
serviceConfig.Transaction = TransactionOption.RequiresNew;
ServiceDomain.Enter(serviceConfig);
ContextUtil.MyTransactionVote = TransactionVote.Commit;
}
[TestCleanup]
public void TestCleanup()
{
AbortDtcTransaction();
}
private void AbortDtcTransaction()
{
ContextUtil.MyTransactionVote = TransactionVote.Abort;
ServiceDomain.Leave();
}
[TestMethod]
public void TestSaveCustomer()
{
CustomerService service =
new CustomerService(new SessionUsingDtcProvider());
Customer customer = new Customer();
customer.Name = "Davy Brion";
service.SaveCustomer(customer);
Assert.IsTrue(customer.Id > 0);
}
}
Mission accomplished ![]()
Note: this test instantiates the SessionUsingDtcProvider which will create the NHibernate SessionFactory which is a rather expensive operation... In your production code, the correct SessionProvider should only be created once and made available so the rest of the code has easy access to it.
Update: it's no longer necessary to set NHibernate's TransactionFactory through reflection. Read this...