开发者

Fake DbContext of Entity Framework 4.1 to Test

I'm using this tutorial to Fake my DbContext and test: http://refactorthis.wordpress.com/2011/05/31/mock-faking-dbcontext-in-entity-framework-4-1-with-a-generic-repository/

But i have to change the FakeMainModuleContext implementation to use in my Controllers:

public class FakeQuestiona2011Context : IQuestiona2011Context
{
    private IDbSet<Credencial> _credencial;
    private IDbSet<Perfil> _perfil;
    private IDbSet<Apurador> _apurador;
    private IDbSet<Entrevistado> _entrevistado;
    private IDbSet<Setor> _setor;
    private IDbSet<Secretaria> _secretaria;
    private IDbSet<Pesquisa> _pesquisa;
    private IDbSet<Pergunta> _pergunta;
    private IDbSet<Resposta> _resposta;

    public IDbSet<Credencial> Credencial { get { return _credencial ?? (_credencial = new FakeDbSet<Credencial>()); } set { } }
    public IDbSet<Perfil> Perfil { get { return _perfil ?? (_perfil = new FakeDbSet<Perfil>()); } set { } }
    public IDbSet<Apurador> Apurador { get { return _apurador ?? (_apurador = new FakeDbSet<Apurador>()); } set { } }
    public IDbSet<Entrevistado> Entrevistado { get { return _entrevistado ?? (_entrevistado = new FakeDbSet<Entrevistado>()); } set { } }
    public IDbSet<Setor> Setor { get { return _setor ?? (_setor = new FakeDbSet<Setor>()); } set { } }
    public IDbSet<Secret开发者_运维知识库aria> Secretaria { get { return _secretaria ?? (_secretaria = new FakeDbSet<Secretaria>()); } set { } }
    public IDbSet<Pesquisa> Pesquisa { get { return _pesquisa ?? (_pesquisa = new FakeDbSet<Pesquisa>()); } set { } }
    public IDbSet<Pergunta> Pergunta { get { return _pergunta ?? (_pergunta = new FakeDbSet<Pergunta>()); } set { } }
    public IDbSet<Resposta> Resposta { get { return _resposta ?? (_resposta = new FakeDbSet<Resposta>()); } set { } }

    public void SaveChanges()
    {
        // do nothing (probably set a variable as saved for testing)
    }
}

And my test like that:

[TestMethod]
public void IndexTest()
{
    IQuestiona2011Context fakeContext = new FakeQuestiona2011Context();
    var mockAuthenticationService = new Mock<IAuthenticationService>();

    var apuradores = new List<Apurador>
    {
        new Apurador() { Matricula = "1234", Nome = "Acaz Souza Pereira", Email = "acaz@telecom.inf.br", Ramal = "1234" },
        new Apurador() { Matricula = "4321", Nome = "Samla Souza Pereira", Email = "samla@telecom.inf.br", Ramal = "4321" },
        new Apurador() { Matricula = "4213", Nome = "Valderli Souza Pereira", Email = "valderli@telecom.inf.br", Ramal = "4213" }
    };
    apuradores.ForEach(apurador => fakeContext.Apurador.Add(apurador));

    ApuradorController apuradorController = new ApuradorController(fakeContext, mockAuthenticationService.Object);
    ActionResult actionResult = apuradorController.Index();

    Assert.IsNotNull(actionResult);
    Assert.IsInstanceOfType(actionResult, typeof(ViewResult));

    ViewResult viewResult = (ViewResult)actionResult;

    Assert.IsInstanceOfType(viewResult.ViewData.Model, typeof(IndexViewModel));

    IndexViewModel indexViewModel = (IndexViewModel)viewResult.ViewData.Model;

    Assert.AreEqual(3, indexViewModel.Apuradores.Count);
}

I'm doing it right?


Unfortunately you are not doing it right because that article is wrong. It pretends that FakeContext will make your code unit testable but it will not. Once you expose IDbSet or IQueryable to your controller and you fake the set with in memory collection you can never be sure that your unit test really tests your code. It is very easy to write a LINQ query in your controller which will pass your unit test (because FakeContext uses LINQ-to-Objects) but fails at runtime (because your real context uses LINQ-to-Entities). That makes whole purpose of your unit testing useless.

My opinion: Don't bother with faking context if you want to expose sets to controller. Instead use integration tests with real database for testing. That is the only way how to validate that LINQ queries defined in controller do what you expect.

Sure, if you want to call just ToList or FirstOrDefault on your sets your FakeContext will serve you well but once you do anything more complex you can find a trap pretty soon (just put the string "Cannot be translated into a store expression" into Google - all these problems will appear only when you run Linq-to-entities but they will pass your tests with Linq-to-objects).

This is quite common question so you can check some other examples:

  • To return IQueryable or not return IQueryable
  • Unit Testing DbContext
  • ASP.NET MVC3 and Entity Framework Code first architecture
  • Organizationally, where should I put common queries when using Entity Framework Code First?
  • Is it possible to stub Entity Framework context and classes to test data access layer?


"Unfortunately you are not doing it right because that article is wrong. It pretends that FakeContext will make your code unit testable but it will not"

I am the creator of the blog post that you refer to. The problem I see here is a misunderstanding of the fundamentals of N-Layered unit testing. My post is not meant to be used directly to test controller logic.

A unit test should do exactly as the name implies and test 'One Unit'. If I am testing a controller (as you are doing above) I forget all about the data access. I should be removing all of the calls to database context in my mind and replacing them with a black box method call as if those operations were unknown to me. It is the code around those operations that I am interested in testing.

Example:

In my MVC application we use the repository pattern. I have a repository, say CustomerRepository : ICustomerRepository, which will perform all of my Customer database operations.

If I were to test my controllers would I want the tests to test my repository, my database access, and the controller logic itself? of course not! there are many 'units' in this pipeline. What you want to do is create a fake repository which implements ICustomerRepository to allow you to test the controller logic in isolation.

This to the best of my knowledge cannot be done on the database context alone. (except maybe for using Microsoft Moles, which you can check out if you want). This is simply because all queries are performed outside of the context in your controller class.

If I wanted to test the CustomerRepository logic how would I do that? The easiest way is to use a fake context. This will allow me to make sure that when I'm trying to get a customer by id, it actually gets the customer by id and so forth. Repository methods are very simple and the "Cannot be translated into a store expression" problem will not usually surface. Though in some minor cases it may (sometimes due to incorrectly written linq queries) in these cases it is important to also perform integration tests that will test your code all the way through to the database. These problems will be found in integration testing. I have used this N-Layered technique for quite a while now and have found no problems with this.

Integration Tests

Obviously testing your app against the database itself is a costly exercise and once you get tens of thousands of tests it becomes a nightmare, on the other hand it best mimics how the code will be used in the 'real world'. These tests are important (from the ui to the database) also and they will be performed as part of the integration tests, NOT unit tests.

Acaz, what you really need in your scenario is a repository which is mockable/fakeable. If you wish to test your controllers as you are doing then your controller should be taking in an object which wraps the database functionality. Then it can return anything you need it to in order to test all aspects of your controller's functionality.

see http://msdn.microsoft.com/en-us/library/ff714955.aspx

In order to test the repository itself (debated if needed in all cases) you will want to either fake the context or use something along the lines of the 'Moles' framework.

LINQ is inherently hard to test. The fact that the query is defined outside of the context using extension methods gives us great flexibility but creates a testing nightmare. Wrap your context in a repository and this problem goes away.

sorry so long :)


As Ladislav Mrnka mentioned, you should test Linq-to-Entity but not Linq-to-Object. I normally used Sql CE as testing DB and always recreate the database before each test. This may make test a little bit slow but so far I'm OK with the performance for my 100+ unit tests.

First, change the connection string setting with SqlCe in the App.config of you test project.

<connectionStrings>
    <add name="MyDbContext"
       connectionString="Data Source=|DataDirectory|MyDb.sdf"
         providerName="System.Data.SqlServerCe.4.0"
         />
</connectionStrings>

Second, set the db initializer with DropCreateDatabaseAlways.

Database.SetInitializer<MyDbContext>(new DropCreateDatabaseAlways<MyDbContext>());

And Then, force EF to initialize before running each test.

public void Setup() {
    Database.SetInitializer<MyDbContext>(new DropCreateDatabaseAlways<MyDbContext>());

    context = new MyDbContext();
    context.Database.Initialize(force: true);
}

If you are using xunit, call Setup method in your constructor. If you are using MSTest, put TestInitializeAttribute on that method. If nunit.......


You can create a Fake DbContext by using Effort for EF 6+. See https://effort.codeplex.com/. Effort stands for Entity Framework Fake ObjectContext Realization Tool.

For an article with a working sample, please see http://www.codeproject.com/Tips/1036630/Using-Effort-Entity-Framework-Unit-Testing-Tool or http://www.codeproject.com/Articles/460175/Two-strategies-for-testing-Entity-Framework-Effort?msg=5122027#xx5122027xx.


I know we shouldn't do it but sometimes you have to anyway (your boss might ask you too for instance and wouldn't change his mind).

So as I had to do it I leave it here it might help some people. I'm quite new to c# / .net and all so it's far from being optimized/clean I suppose but it seems to work.

following MSDN Find here the missing class and using a bit of reflection I managed to add one way properties : Key elements here are the AddNavigationProperty and RefreshNavigationProperties. If anyone has suggestion to improve this code I'll gladly take them

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;

namespace MockFactory
{
    public class TestDbSet<TEntity> : DbSet<TEntity>, IQueryable, IEnumerable<TEntity>, IDbAsyncEnumerable<TEntity>
        where TEntity : class
    {
        public readonly ObservableCollection<TEntity> _data;
        private readonly IQueryable _query;
        private readonly Dictionary<Type, object> entities;

        public TestDbSet()
        {
            _data = new ObservableCollection<TEntity>();
            _query = _data.AsQueryable();

            entities = new Dictionary<Type, object>();
        }

        public override ObservableCollection<TEntity> Local
        {
            get { return _data; }
        }

        IDbAsyncEnumerator<TEntity> IDbAsyncEnumerable<TEntity>.GetAsyncEnumerator()
        {
            return new TestDbAsyncEnumerator<TEntity>(_data.GetEnumerator());
        }

        IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator()
        {
            return _data.GetEnumerator();
        }

        Type IQueryable.ElementType
        {
            get { return _query.ElementType; }
        }

        Expression IQueryable.Expression
        {
            get { return _query.Expression; }
        }

        IQueryProvider IQueryable.Provider
        {
            get { return new TestDbAsyncQueryProvider<TEntity>(_query.Provider); }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _data.GetEnumerator();
        }

        public void AddNavigationProperty<T>(DbSet<T> dbSet) where T : class
        {
            entities.Add(typeof (T), dbSet);
        }

        public void RefreshNavigationProperty(TEntity item)
        {
            foreach (var entity in entities)
            {
                var property = item.GetType().GetProperty(entity.Key.Name);

                var type =
                    (int)item.GetType().GetProperty(entity.Key.Name.Replace(typeof(TEntity).Name, "")).GetValue(item);

                var dbSets = (IEnumerable<object>)entity.Value.GetType().GetField("_data").GetValue(entity.Value);

                var dbSet = dbSets.Single(x => (int)x.GetType().GetProperty("Id").GetValue(x) == type);
                property.SetValue(item, dbSet);
            }
        }

        public override TEntity Add(TEntity item)
        {
            RefreshNavigationProperty(item);
            _data.Add(item);
            return item;
        }

        public override TEntity Remove(TEntity item)
        {
            _data.Remove(item);
            return item;
        }

        public override TEntity Attach(TEntity item)
        {
            _data.Add(item);
            return item;
        }

        public override TEntity Create()
        {
            return Activator.CreateInstance<TEntity>();
        }

        public override TDerivedEntity Create<TDerivedEntity>()
        {
            return Activator.CreateInstance<TDerivedEntity>();
        }
    }
}

You can then create your context

 public TestContext()
        {
            TypeUsers = new TestDbSet<TypeUser>();
            StatusUsers = new TestDbSet<StatusUser>();

            TypeUsers.Add(new TypeUser {Description = "FI", Id = 1});
            TypeUsers.Add(new TypeUser {Description = "HR", Id = 2});

            StatusUsers.Add(new StatusUser { Description = "Created", Id = 1 });
            StatusUsers.Add(new StatusUser { Description = "Deleted", Id = 2 });
            StatusUsers.Add(new StatusUser { Description = "PendingHR", Id = 3 });


            Users = new TestDbSet<User>();

            ((TestDbSet<User>) Users).AddNavigationProperty(StatusUsers);
           ((TestDbSet<User>)Users).AddNavigationProperty(TypeUsers);

        }

        public override DbSet<TypeUser> TypeUsers { get; set; }
        public override DbSet<StatusUser> StatusUsers { get; set; }
        public override DbSet<User> Users { get; set; }
        public int SaveChangesCount { get; private set; }

        public override int SaveChanges(string modifierId)
        {
            SaveChangesCount++;
            return 1;
        }
    }

Finally do not forget in your test to refresh the navigation properties before doing the assert (there should be a better way but I couldn't find it )

ContextFactory.Entity.Users.Each(((TestDbSet<User>) ContextFactory.Entity.Users).RefreshNavigationProperty);
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜