Making Mockery of Extension Methods
April 10, 2014
Recently I have been looking at ServiceStack's OrmLite "Micro ORM" as a light-weight alternative to Entity Framework. It is relatively easy to use and very powerful, with capability for both code-first and database-first development. After learning the basic interaction, it was time to flip back into TDD-mode.
And then I found quite the challenge: I wanted to write unit tests that insure that I'm using OrmLite correctly. I was not interested (for the time being) in testing OrmLite's interaction with SQL Server itself. That is, I wanted behavioral unit tests rather than database integration tests. Time for a mock. But what would I mock? This ORM framework makes extensive use of extension methods that run off of the core IDbConnection
interface from the .Net framework - so it would seem that there is no way to take advantage of Dependency Injection.
Enter the static delegates method promoted by Daniel Cazzulino. OK, so we have Constructor and Property Injection methods already. And now they are joined by have Delegate Injection. Let us take this simple example from a hypothetical repository class:
var dbFactory = new OrmLiteConnectionFactory(connectionString, SqlServerDialect.Provider); using (IDbConnection db = dbFactory.OpenDbConnection()) { using (var tran = db.OpenTransaction()) { db.Save(new BusinessEntity()); tran.Commit(); } }
Refactoring the class to use constructor dependency injection, inserting an IDbConnectionFactory
instance instead, is trivial and allows us to write unit tests that have a mock version of IDbConnectionFactory
. But OpenTransaction()
and Save()
are all extension methods. How do we replace them?
Using Cazzulino's technique, we can create a static class containing static delegates, and then insert those delegates into the repository. When it comes time for unit testing, just replace those static delegates with inline delegates – thus, effectively mocking the methods. Here's the original signature for OpenTransaction
:
public static IDbTransaction OpenTransaction(this IDbConnection dbConn)
This can be represented with a Func<T, Tresult> delegate:
public static Func<IDbConnection, IDbTransaction> OpenTransaction = (connection) => ReadConnectionExtensions.OpenTransaction(connection);
The Save<T>()
method is a bit more troublesome, since it is itself a generic. In particular, I want to address this overload of Save():
public static int Save<T>(this IDbConnection dbConn, params T[] objs)
The <T>
threw me off – where do you declare it? You can't use put the T
after Save
in the Func
. Then I realized it just needs to go on the static class. And what of params T[]
? Convert it to an array of T:
public static class DelegateFactory<T> { public static Func<IDbConnection, T[], int> Save = (connection, items) => { return OrmLiteWriteConnectionExtensions.Save(connection, items); }; }
However, we don't really want the generic T
applied to the non-generic methods, so perhaps we should create two different classes. And I just learned something new through trial and success… <T>
allows for class name overloading! Enough with the chatter. Here is a complete example with a happy-path test and one negative test that ensures the Commit()
isn't called when there's an error.
using Microsoft.VisualStudio.TestTools.UnitTesting; using ServiceStack.Data;using ServiceStack.OrmLite; using System; using System.Data; using System.Linq; namespace TestProject { public class BusinessEntity { } public class Repository<T> where T: class { private readonly IDbConnectionFactory dbFactory; public Repository(IDbConnectionFactory dbFactory) { if (dbFactory == null) { throw new ArgumentNullException("dbFactory"); } this.dbFactory = dbFactory; } public int Save(T input) { int rowsAffected = 0; using (IDbConnection db = dbFactory.OpenDbConnection()) { using (var tran = DelegateFactory.OpenTransaction(db)) { rowsAffected = DelegateFactory<T>.Save(db, new[] { input }); tran.Commit(); } } return rowsAffected; } } public static class DelegateFactory { public static Func<IDbConnection, IDbTransaction> OpenTransaction = (connection) => { return ReadConnectionExtensions.OpenTransaction(connection); }; } public static class DelegateFactory<T> { public static Func<IDbConnection, T[], int> Save = (connection, items) => { return OrmLiteWriteConnectionExtensions.Save(connection, items); }; } [TestClass] public class UnitTest1 { [TestMethod] public void SaveANewObjectWithProperTransactionManagement() { // Prepare input var input = new BusinessEntity(); // Use moq where we can var mockRepository = new Moq.MockRepository(Moq.MockBehavior.Strict); var mockFactory = mockRepository.Create<IDbConnectionFactory>(); var dbConnection = mockRepository.Create<IDbConnection>(); mockFactory.Setup(x => x.OpenDbConnection()) .Returns(dbConnection.Object); dbConnection.Setup(x => x.Dispose()); var mockTransaction = mockRepository.Create<IDbTransaction>(); mockTransaction.Setup(x => x.Commit()); mockTransaction.Setup(x => x.Dispose()); // And use the delegate methods elsewhere var expectedReturnValue = 1; DelegateFactory.OpenTransaction = (connection) => { return mockTransaction.Object; }; DelegateFactory<BusinessEntity>.Save = (connection, items) => { Assert.AreSame(dbConnection.Object, connection, "wrong connection object used for Save"); Assert.IsNotNull(items, "items array is null"); Assert.AreEqual(1, items.Count(), "items array count"); Assert.AreSame(input, items[0], "wrong item sent to the Save comand"); return expectedReturnValue; }; // Call the system under test var system = new Repository<BusinessEntity>(mockFactory.Object); var response = system.Save(input); // Evaluate the results Assert.AreEqual(expectedReturnValue, response); mockRepository.VerifyAll(); } [TestMethod] public void CommitIsNeverCalledWhenSaveEncountersAnException() { // Prepare input var input = new BusinessEntity(); // Use moq where we can var mockRepository = new Moq.MockRepository(Moq.MockBehavior.Strict); var mockFactory = mockRepository.Create<IDbConnectionFactory>(); var dbConnection = mockRepository.Create<IDbConnection>(); mockFactory.Setup(x => x.OpenDbConnection()) .Returns(dbConnection.Object); dbConnection.Setup(x => x.Dispose()); var mockTransaction = mockRepository.Create<IDbTransaction>(); // **** Commit isn't allow **** //mockTransaction.Setup(x => x.Commit()); mockTransaction.Setup(x => x.Dispose()); // And use the delegate methods elsewhere DelegateFactory.OpenTransaction = (connection) => { return mockTransaction.Object; }; DelegateFactory<BusinessEntity>.Save = (connection, items) => { Assert.AreSame(dbConnection.Object, connection, "wrong connection object used for Save"); Assert.IsNotNull(items, "items array is null"); Assert.AreEqual(1, items.Count(), "items array count"); Assert.AreSame(input, items[0], "wrong item sent to the Save command"); // **** Inject an exception *** // don't worry that this isn't a SQL exception - just make sure to // test that this same exception occurs when Save is called throw new InvalidCastException(); }; // Call the system under test var system = new Repository<BusinessEntity>(mockFactory.Object); try { system.Save(input); } catch (InvalidCastException) { // Evaluate the results mockRepository.VerifyAll(); } catch (Exception ex) { Assert.Fail("wrong exception - caught " + ex.GetType().ToString()); } } } }
No TrackBacks
TrackBack URL: http://www.safnet.com/fcgi-bin/mt/mt-tb.cgi/121
Leave a comment