by Scott DePouw via Scott DePouw's Blog on 3/24/2010 10:56:21 AM
I have recently begun working on a project that utilizes LINQ to SQL as the layer of persistence. The project has a set of repositories that are used to access the data from LINQ to SQL, which then convert the objects generated by LINQ to SQL to domain objects. An initial implementation of a common repository method looks something like this:
public class LegoRepository
{
private readonly IMapper<Lego, LinqToSqlObjects.Lego> _legoMapper;
// Repository initialization.
public Lego GetById(int legoId)
using (var context = new ChristiansenDataContext())
LinqToSqlObjects.Lego legoEntity = context.Legos.
SingleOrDefault(lego => lego.Id == legoId);
if (legoEntity == null)
throw new LegoDoesNotExistException(int legoId);
}
return _mapper.Map(legoEntity);
// Rest of class.
In this example, the code is searching the database for a Lego with the given legoId. If it finds one, it creates a Lego domain object via the IMapper object and returns it. If it does not find one, however, it throws an exception. Simple integration tests can be set up to show that all of the pieces are working together (domain, mapper, repository, and database) in order to successfully fetch a Lego from persistence. We could also write an integration test to assure that the LegoDoesNotExistException gets thrown when a Lego with a given ID does not exist in the database. However, if we could control what context.Legos returns, we wouldn’t be dependent on any external persistence layer to test this.
How do we unit test something like this? The above example is creating a new ChristiansenDataContext in the method, with no way to change it. The code will always try to connect to the database in this case. The first thing we need to do is pull that dependency out of our repository. Let us create an interface IAdapter.
public interface IAdapter : IDisposable
Table<Activity> Legos { get; }
This interface is responsible for exposing the behavior we expect from a DataContext object. In this example, we only need the collection of Lego objects. Down the road we can add whatever other behaviors we need (data from other tables, methods like SubmitChanges(), and so on). Since IAdapter implements IDisposable, we can use it in place of the DataContext in the respository. Here’s what the implementation of IAdapter would look like:
public class Adapter : IAdapter
private readonly ChristiansenDataContext _context = new ChristiansenDataContext();
public void Dispose()
_context.Dispose();
public Table<Lego> Legos
get { return _context.Legos; }
However, the repository can’t just hold on to a single instance of an IAdapter (i.e. a database connection), so we need a way to get a new IAdapter whenever we want it. Let’s create an interface that will get us an instance of IAdapter whenever we request it:
public interface IPersistenceLayer
IAdapter GetAdapter();
public class PersistenceLayer : IPersistenceLayer
public IAdapter GetAdapter()
return new Adapter();
Our repository can be instantiated with an IPersistenceLayer object, which can be injected in testing so that we can return a mock IAdapter.
private readonly IPersistenceLayer _persistenceLayer;
using (var adapter = _persistenceLayer.GetAdapter())
LinqToSqlObjects.Lego legoEntity = adapter.Legos.
Now let’s write a test that will expect the exception to be thrown when a Lego with the given ID is not found in the database (in this example I’m using NUnit and Rhino Mocks):
[Test]
[ExpectedException(typeof(LegoNotFoundException))]
public void GetByIdThrowsExceptionWhenLegoWithGivenIdDoesNotExist()
// Error! The Table class does not expose constructors.
var legos = new Table<LinqToSqlObjects.Lego>();
Expect.Call(_mockedPersistenceLayer.GetAdapter()).Return(_mockedAdapter);
Expect.Call(_mockedAdapter.Legos).Return(legos);
_mockRepository.ReplayAll();
_legoRepository.GetById(0);
_mockRepository.VerifyAll();
As indicated by the comment, we cannot instantiate a new instance of Table<T>. Additionally, Table<T> is a sealed class, so we cannot inherit from it and use that to create fake data. What do we do? The whole purpose of this was to be able to create fake data, but because Table is locked down, we can’t… Or can we?
public interface ITableWrapper<TEntity>
where TEntity : class
IEnumerable<TEntity> Collection { get; }
void InsertOnSubmit(TEntity entity);
public class TableWrapper<TEntity> : ITableWrapper<TEntity>
private readonly Table<TEntity> _table;
public TableWrapper(Table<TEntity> table)
_table = table;
public IEnumerable<TEntity> Collection
get { return _table; }
public void InsertOnSubmit(TEntity entity)
_table.InsertOnSubmit(entity);
To get around the issues caused by Table<T>, we will instead wrap Table<T> in a wrapper interface. Just like with IAdapter, we expose only the behaviors we need. InsertOnSubmit() would have been added after we needed to call that method. We’ll have to alter IAdapter slightly: instead of returning Table<Lego> we’re going to return ITableWrapper<Lego>.
ITableWrapper<Activity> Legos { get; }
/* ... */
public ITableWrapper<Lego> Legos
get { return new TableWrapper<Lego>(_context.Legos); }
In the repository, instead of saying “adapter.Legos” to access the collection, we have to say “adpater.Legos.Collection” instead.
LinqToSqlObjects.Lego legoEntity = adapter.Legos.Collection.
// Rest of method
The good news, however, is that from this one extra word we can now easily test GetById():
// IEnumerable<T> is very easy to mock. :)
IEnumerable<LinqToSqlObjects.Lego> legos = new Table<LinqToSqlObjects.Lego>();
Expect.Call(_mockedAdapter.Legos).Return(_mockedTableWrapper);
Expect.Call(_mockedTableWrapper.Collection).Return(legos);
When the test is run, Collection will return an empty enumerable of Legos. legoEntity will therefore be null and the exception will be thrown. Congratulations, we’ve successfully created testable LINQ to SQL code! We have decoupled where our data originates from how we manipulate the data after the fact. Because we have done this, unit testing these repositories is incredibly simple.
Original Post: Let’s Unit Test Some LINQ to SQL
The content of the postings is owned by the respective author. CSharpFeeds is not responsible for the contents of the postings. This site is automatically generated and cannot be reviewed for abusive content. If you find abusive content on CSharpFeeds, please contact us. Designated trademarks and brands are the property of their respective owners. All rights reserved.