Search code examples
c#entity-frameworkunit-testingmicrosoft-fakes

Test Service using Entity Framework without dependency injection


I'm trying to test business logic in queries in services. So I don't want my tests to have real access to the database, because they are unit tests, not integration tests.

So I've made a simple example of my context and how I'm trying to shim it.

I have an entity

public class SomeEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

and a service

public class Service
{
    public int CountSomeEntites()
    {
        using (var ctx = new Realcontext())
        {
            int result = ctx.SomeEntities.Count();
            return result;
        }
    }
}

And this is the real context

public partial class Realcontext : DbContext
{
    public virtual DbSet<SomeEntity> SomeEntities { get; set; }

    public Realcontext() : base("name=Realcontext")
    {
        InitializeContext();
    }

    partial void InitializeContext();

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        throw new UnintentionalCodeFirstException();
    }
}

So I've tried to create a fake context and I detourned the constructor of the real context in my test method

This is the fake context

 public class FakeContext : DbContext
 {
    public DbSet<SomeEntity> SomeEntities { get; set; }

    public FakeContext()
    {
    }
}

And finally the test class

[TestClass]
public class ServiceTests
{
    [TestMethod]
    public void CountEmployee_ShoulReturnCorrectResult()
    {
        using (ShimsContext.Create())
        {
            ShimRealcontext.Constructor = context => GenerateFakeContext();
            ShimDbContext.AllInstances.Dispose = () => DummyDispose();

            Service service = new Service();
            int result = service.CountSomeEntites();

            Assert.AreEqual(result, 2);
        }
    }

    private FakeContext GenerateFakeContext()
    {
        FakeContext fakeContext = new FakeContext();
        fakeContext.SomeEntities.AddRange(new[]
        {
            new SomeEntity {Id = 1, Name = "entity1"},
            new SomeEntity {Id = 2, Name = "entity2"}
        });
        return fakeContext;
    }
}

When I run the test, the RealContext constructor is returned properly, a FakeContext is built in the GenerateFakeContext() method, it contains 2 SomeEntities and it is returned, but right after, in the service, the property SomeEntities of the variable ctx equals to null.

Is it because my variable ctx is declared as a new RealContext()? But calling the constructor of RealContext returns a FakeContext(), so isn't the variable supposed to be of type FakeContext?

Am I doing something wrong? Or is there any other way to test the service without accessing the real database?


Solution

  • I had the simlair situation and I solved it with build configuration and conditional compilation. It's not the best solution, but it worked for me and solved the problem. Here is the receipt:

    1. Create DataContext interface

    First you need to create an interface which will be implemented by both context classe you going to use. Let it be named just 'IMyDataContext'. Inside it you need to describe all DbSets you need to have access to.

    public interaface IMyDataContext
    {
        DbSet<SomeEntity> SomeEntities { get; set; }
    }
    

    And both your context classes need to impelemt it:

    public partial class RealDataContext : DataContext, IMyDataContext
    {
         DbSet<SomeEntity> SomeEntities { get; set; }
    
        /* Contructor, Initialization code, etc... */
    }
    
    public class FakeDataContext : DataContext, IMyDataContext
    {
         DbSet<SomeEntity> SomeEntities { get; set; }
    
        /* Mocking, test doubles, etc... */
    }
    

    By the way you can even make properies read-only at interface level.

    2. Add 'Test' build configuration

    Here you can find how to add new build configuration. I named my configuratin 'Test'. After new configuration is created, go to your DAL project properties, Build section on the left pane. In the 'Configuration' drop-down select the configuration you've just created and in input 'Conditional compilation symbols' type 'TEST'.

    3. Incapsulate context injection

    To be clear, my approach is still method/property based DI solution =)

    So now we need to implement some injection code. For simplicity you can add it right into your service or extract into another class if you need more abstraction. The main idea is to use conditional compilation direcitves instead of IoC framework.

    public class Service
    {
        // Injector method
        private IMyDataContext GetContext() {
            // Here is the main code
    
    #if TEST    // <-- In 'Test' configuration
                // we will use fake context
                return new FakeDataContext(); 
    #else
                // in any other case 
                // we will use real context
                return new RealDataContext(); 
    #endif
    
        }
    
        public int CountSomeEntites()
        {
           // the service works with interface and does know nothing
           // about the implementation
    
            using (IMyDataContext ctx = GetContext()) 
            {
                int result = ctx.SomeEntities.Count();
                return result;
            }
        }
    }
    

    Limitations

    The described approach solves the the problem you described, but it has a limitation: as IoC allows you switch contexts dynamically at runtime, conditional complation requires you to recompile the solution.

    In my case it's not a problem - my code isn't covered by tests for 100% and I don't run them on each build. Usually I run tests only before commiting the code, so it's very easy to switch the build configuration in VS, run tests, make sure that nothing was broke and then return to debug mode. In release mode you don't need to run test either. Even if you need - you can craete "Release build test mode" configuration and continue to use the same solution.

    Another problem is if you have continuos integration - you need to make additional setup to your build server. Here you have two ways:

    • Setup two build definitions: one for release and one for tests. If your server is setup to automatic release you need to be careful because test fails will be shown in the second one while the first is deployed.
    • Set complex build definition which builds your code in Test configuration for the first time, runs test and if they are OK - then recompiles the code in target configuration and prepare to deploy.

    Thus, as any solution this one is yet another compromise between simplisity and flexibility.

    UPDATE

    After some time I understand that the way I described above is very heavy. I mean - build configurations. In case of only two IDataContext implementations: 'Core' and 'Fake' you can simply use bool paramenter and simple if/else branches instead of compilation directives #if/#else/#endif and all the head ache configuring your build server.

    If you have more than two implementations - you can use enum and switch block. A probem here is to define what you will return in default case or if value is out of enum's range.

    But the main benefit of such approach is that you can be no longer bound to compilation time. Injector parameter could be changed at any time, for example using web.config and ConfigurationManager. Using it you could event switch your data context at run time.