Reputation: 2231
I have an application that contains methods that work with data using Entity Framework 4.2 Code First and a MySQL database. I am trying to figure out a good way to write an MSTest unit test for these methods. For example:
DataModel:
public class User
{
public User() { }
[Key]
public int UserID { get; set; }
public string Role { get; set; }
}
public class AppDbContext : DbContext
{
public DbSet<User> Users { get; set; }
}
Business Layer:
public class Bus
{
public bool UserIsInRole(int userID, string role)
{
using(var context = new AppDbContext())
{
User user = context.Users.SingleOrDefault(p => p.UserID == userID);
if (user == null)
return false;
return user.Roles.Split(',').Contains(role);
}
}
}
I am trying to write a set of unit tests for the UserIsInRole function, but I want to try to isolate myself from actually having to read and write to the actual database since I cannot guarantee its state before the test. Setting up/tearing down a database just for this test would take too long.
I have ran into many articles about using fake DbContext, such as here, here, and here but they all seem to have some pros and cons. One group of people say that one should not write unit tests against EF and that this belongs to integration testing and that any fake DbContext don't behave enough like the real thing for the purpose of acceptable tests.
I think code like this lies someplace in the middle of the argument. Ideally, I want to create a set of temporary, in-memory objects that represent the desired data without having to actually store it into the database.
How would you change the above and write a set of tests that verify that the UserIsInRole method:
Keep in mind that this is a simplified example and that code could actually contain multiple queries of arbitrary complexity so I am hoping to find something a bit more comprehensive than, say, moving every query to a virtual function that is replaced by the test framework to return a predefined User record.
Upvotes: 2
Views: 1232
Reputation: 364279
Your code is not testable. How do you want to fake something if you use new
directly in system under test?
Improve your code:
public class Bus
{
public bool UserIsInRole(int userID, string role)
{
using(var context = CreateContext())
{
User user = ExecuteGetUserQuery(context, userId);
if (user == null)
return false;
return user.Roles.Split(',').Contains(role);
}
}
protected virtual IAppDbContext CreateContext()
{
return new AppDbContext();
}
protected virtual User ExecuteGetUserQuery(IAppDbContext context, int userId)
{
return context.Users.SingleOrDefault(p => p.UserID == userID);
}
}
Now without introducing any new class (just single interface for your context) we made your code testable:
UserIsInRole
logicWhen you want to write unit test for UserIsInRole
(and other pure unit tests) you can make test derived implementation Bus
class and return any fake data from override version of ExecuteGetUserQuery
. By overriding CreateContext
you will also make your tests completely independent on database or EF. Overriding those methods will not result in testing different logic because you are going to fake these data anyway. The tested UserIsInRole
method is not altered in derived class.
Sure instead of providing virtual methods you can move this functionality into separate class or classes and use stubs or mocks but for simple scenarios this works. If you need to test interaction with database you will write integration test only for ExecuteGetUserQuery
.
Upvotes: 3
Reputation: 34248
I personally think a function like IsUserInRole belongs to business logic not data storage, however one of the problems i have seen a lot is that developers often tightly couple these things together. In the case of something like IsUserInRole the implementation is normally to lookup all roles which a user belongs to from the and see if the user is in the specified role. This means that you actually cant test this piece of functionality without having some kind of database attached.
I personally think that the answer to this problem is to de-couple the code from a specific implementation of the database using a repository pattern. This allows you to mock the repository without needing to refactor the code. I understand that this its technically possible to do this with DBContext mocking but I like repositories more as they are less tightly coupled to the specific type of data storage mechanism, ie not tied to EF in this example.
Here's a link to my blog around this and how I've personally solved the problem. What I've done has served me well so take a look at the example and how I've implemented unit tests with a back end data store to EF 4.
http://blog.staticvoid.co.nz/2011/10/staticvoid-repository-pattern-nuget.html
Upvotes: 0
Reputation: 16980
If you don't like to mock DbContext, you can mock the data objects and extract some of the buiness logic into DB-independent testable methods.
public class Bus
{
private UnitTestable impl;
public bool UserIsInRole(int userID, string role)
{
using(var context = new AppDbContext())
{
return impl.UserInRole(context.Users, role);
}
}
}
public class UnitTestable
{
public bool UserInRole(IQueryable<User> users, string role)
{
User user = users.SingleOrDefault(p => p.UserID == userID);
if (user == null)
return false;
return user.Roles.Split(',').Contains(role);
}
}
To mock the User object in tests you might need to make its properties virtual or extract an interface from it.
Upvotes: 0
Reputation: 31610
Take a look at Rowan's post about unit testing with a fake db context: http://romiller.com/2012/02/14/testing-with-a-fake-dbcontext/
Upvotes: -1
Reputation: 15130
I would seperate the knowledge of EF from the rest of your domain. If you disect DbSet you'll find that it implements IQueryable which is enough for EF to work. Create an interface that defines your domain context and make your different concrete implementations (EF and Fake) implement that interface like:
public class User
{
public User() { }
[Key]
public int UserID { get; set; }
public string Role { get; set; }
}
public interface IAppDomain
{
public IQueryable<User> Users { get; }
}
public class AppDbContext : DbContext, IAppDomain
{
// exposure for EF
public DbSet<User> Users { get; set; }
IAppDomain.IQueryable<User> Users { get { return ((AppDbContext)this).Users; }
}
public class FakeAppDomain : IAppDomain
{
private List<User> _sampleUsers = new List<User>(){
new User() { UserID = 1, Role = "test" }
}
public IQueryable<User> Users { get { return _sampleUsers; } }
}
This can be used in ways like:
IQueryable<User> GetUsersByManagerRole(IAppDomain domain)
{
return from u in domain.Users
where u.Role == "Manager"
select u;
}
This allows you to create a fake implementation that takes any type of sample input. Next in your unit test, you create a new FakeDomainContext in which you set the state in the desired way for your unit test. Want to test of users with a certain role can be found? Create a FakeDomainContext with users with some test roles and try to find them. Easy and clean.
Upvotes: 2