I decided to start writing unit tests in our application. It uses Entity Framework with a repository pattern.
Now I want to start testing logic classes which are using the repositories. I provide a simple example here.
Three of my methods in the class GenericRepository:
public class GenericRepository : IRepository
{
public IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class
{
var entityName = GetEntityName<TEntity>();
return Context.CreateQuery<TEntity>(entityName);
}
private string GetEntityName<TEntity>() where TEntity : class
{
return typeof(TEntity).Name;
}
public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class
{
return GetQuery<TEntity>().Where(predicate).AsEnumerable();
}
}
A simple logic class returning distinct years from a calendar table in descending order (yes I know the word calendar is misspelled in our code):
public class GetDistinctYearsFromCalendar
{
private readonly IRepository _repository;
public GetDistinctYearsFromCalendar()
{
_repository = new GenericRepository();
}
internal GetDistinctYearsFromCalendar(IRepository repository)
{
_repository = repository;
}
public int[] Get()
{
return _repository.Find<Calender_Tbl>(c => c.Year.HasValue).Select(c => c.Year.Value).Distinct().OrderBy(c => c).Reverse().ToArray();
}
}
And here is my first test:
[TestFixture]
public class GetDistinctYearsFromCalendarTest
{
[Test]
public void ReturnsDistinctDatesInCorrectOrder()
{
var repositoryMock = new Mock<IRepository>();
repositoryMock.Setup(r => r.Find<Calender_Tbl>(c => c.Year.HasValue)).Returns(new List<Calender_Tbl>
{
new Calender_Tbl
{
Date =
new DateTime(2010, 1, 1),
Year = 2010
},
new Calender_Tbl
{
Date =
new DateTime(2010, 2, 1),
Year = 2010
},
new Calender_Tbl
{
Date =
new DateTime(2011, 1, 1),
Year = 2011
}
}.AsQueryable());
var getDistinct = new GetDistinctYearsFromCalendar(repositoryMock.Object).Get();
Assert.AreEqual(2, getDistinct.Count(), "Returns more years than distinct.");
Assert.AreEqual(2011, getDistinct[0], "Incorrect order, latest years not first.");
Assert.AreEqual(2010, getDistinct[1], "Wrong year.");
}
}
This is working fine. But this is not actually what I want to do. Since I have to setup the method Find on the mock object I also need to know how it is going to be called in my logic class. If I would like to do TDD I don't want to mind about this. All I want to know is which Calendar entities my repository should provide. I would like to setup the GetQuery method. Like this:
repositoryMock.Setup(r => r.GetQuery<Calender_Tbl>()).Returns(new List<Calender_Tbl>
{
new Calender_Tbl
{
Date =
new DateTime(2010, 1, 1),
Year = 2010
},
new Calender_Tbl
{
Date =
new DateTime(2010, 2, 1),
Year = 2010
},
new Calender_Tbl
{
Date =
new DateTime(2011, 1, 1),
Year = 2011
}
}.AsQueryable());
So when Find is calling GetQuery internally in the GenericRepository class it should get the correct Calendar entities that I setup in GetQuery. But this is not working of course. Since I haven't setup the Find method 开发者_开发技巧of my mock object I don't get any entities.
So what to do? Of course I could probably use Moles or some other framework which mocks everything but I don't want to do that. Is there anything I can do in the design of the class or test to solve the issue?
It is not the end of the world if I have to go with my current solution but what if the property year turns into a not nullable int? Then of course I will have to change my implementation in the logic class but I would also have to change the test. I would like to try to avoid this.
I can see two ways:
public class MockRepository : IRepository
{
private List<object> entities;
public MockRepository(params object[] entitites)
{
this.entities = entities.ToList();
}
public IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class
{
return this.entities.OfType<TEntity>().AsQueryable();
}
public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class
{
return GetQuery<TEntity>().Where(predicate).AsEnumerable();
}
}
That's the easiest and my preferred way. Moq isn't the hammer for everything ;)
Alternatively, if you really insist on using Moq (I'm flattered, but it's very much unnecessary in this case, as you can do state based testing on the returned entities), you can do:
public class GenericRepository : IRepository
{
public virtual IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class
{
var entityName = GetEntityName<TEntity>();
return Context.CreateQuery<TEntity>(entityName);
}
private string GetEntityName<TEntity>() where TEntity : class
{
return typeof(TEntity).Name;
}
public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class
{
return GetQuery<TEntity>().Where(predicate).AsEnumerable();
}
}
And then use Moq to override the behavior of GetQuery:
var repository = new Mock<GenericRepository> { CallBase = true };
repository.Setup(x => x.GetQuery<Foo>()).Returns(theFoos.AsQueryable());
What will happen is that the Find method will get executed on the GenericRepository class, which will in turn the GetQuery, which has been overwritten by Moq to provide the fixed set of entities.
I set CallBase = true explicitly just in case you happen to make Find virtual too, so that we ensure it's always called. Not technically needed if the Find isn't virtual, as it will always be invoked on the actual class the mock is inheriting/mocking from.
I'd go for the first option, much simpler to understand what's going on, and it can be reused outside of the context of a single particular test (just pass any entities you need and it will work for everything).
Recently, a new tool called Effort has come out for EF 6+ that I found to be tremendously helpful for unit testing against a fake DB. See http://effort.codeplex.com/wikipage?title=Tutorials&referringTitle=Home.
Add it by using this package manager console command:
PM> Install-Package Effort.EF6
Then add an interface for your DbContext, say, if you are using the AdventureWorks database (see https://sql2012kitdb.codeplex.com/):
Then update your DbContext to add two new parameterized constructors:
///
/// Create a new context based on database name or connection string.
///
/// Database name or connection string
public AdventureWorksEntities(string nameOrConnectionString)
: base(nameOrConnectionString)
{
this.Configuration.LazyLoadingEnabled = false;
}
public AdventureWorksEntities(DbConnection connection)
: base(connection, true)
{
this.Configuration.LazyLoadingEnabled = false;
}
Add a constructor that takes the interface to your repository:
private IAdventureWorksDbContext _dbContext;
public ProductRepository(IAdventureWorksDbContext dbContext)
{
dbContext.Configuration.AutoDetectChangesEnabled = false;
this._dbContext = dbContext;
}
Then add an interface to your unit testing project and associated class:
public interface ITestDatabase : IDisposable
{
IAdventureWorksDbContext CreateContext();
void Dispose(IAdventureWorksDbContext context);
}
Add some fake data to your unit testing project:
public class ProductsTestData
{
public static void AddTestData(IAdventureWorksDbContext dbContext)
{
dbContext.Products.Add(new Product() { Id = new Guid("23ab9e4e-138a-4223-bb42-1dd176d8583cB"), Name = "Product A", CreatedDate = DateTime.Now, Description = "Product description..." });
dbContext.Products.Add(new Product() { Id = new Guid("97e1835f-4c1b-4b87-a514-4a17c019df00"), Name = "Product B", CreatedDate = DateTime.Now });
dbContext.SaveChanges();
}
}
Now setup your unit testing class:
[TestClass]
public class ProductsTest
{
private ITestDatabase _testDatabaseStrategy;
private ProductRepository _productRepository;
private IAdventureWorksDbContext _context;
[TestInitialize]
public void SetupTest()
{
// create the test strategy. This will initialise a new database
_testDatabaseStrategy = CreateTestStrategy();
// add test data to the database instance
using (_context = _testDatabaseStrategy.CreateContext())
{
ProductsTestData.AddTestData(_context);
_context.SaveChanges();
}
// initialise the repository we are testing
_context = _testDatabaseStrategy.CreateContext();
_productRepository = new ProductRepository(_context);
}
protected ITestDatabase CreateTestStrategy()
{
return new EffortDatabaseStrategy();
}
[TestCleanup]
public void CleanupTest()
{
// dispose of the database and connection
_testDatabaseStrategy.Dispose(_context);
_context = null;
}
[TestMethod]
public void GetProductsByTagName()
{
IEnumerable<Product> products = _productRepository.GetProductsByTagName("Tag 1", false);
Assert.AreEqual(1, products.Count());
}
Where EffortDatabaseStrategy is:
public class EffortDatabaseStrategy : ITestDatabase
{
public EffortDatabaseStrategy()
{
}
private DbConnection _connection;
public IAdventureWorksDbContext CreateContext()
{
if (_connection == null)
{
_connection = Effort.DbConnectionFactory.CreateTransient();
}
var context = new AdventureWorksDbContext(_connection);
return context;
}
public void Dispose(IAdventureWorksDbContext context)
{
if (context != null)
{
context.Dispose();
}
}
public void Dispose()
{
}
}
For full details, please see http://www.codeproject.com/Articles/460175/Two-strategies-for-testing-Entity-Framework-Effort?msg=5122027#xx5122027xx.
精彩评论