Testing classes with dependencies (C # + Entity Framework)

3

I have a "Sale" class (class summary just below) and in it I have a "Budget" property. What would be the best way to do a unit test in this class? I came across 2 problems:

  • The mock frameworks require that we leave the methods we need to "mock" as virtual, which I find somewhat risky to open this loop of power override in a method where I see no sense in allowing this, except for the own mock requirement. Home
  • Another way would be to create an interface for this class "Budget" to mock the interface instead of the class itself, but from what I've seen so far, Entity Framerwork can not resolve when I have a property as an interface. >

What is the best solution or best practice for this?

public class Venda
{
    public Venda(Orcamento orcamento)
    {
        this.VendaItens = new List<VendaItem>();

        if (orcamento.Valido() && orcamento.Pendente())
            this.CarregaItensAPartirDoOrcamento();
    }

    public Orcamento Orcamento { get; private set; }

    public IList<VendaItem> VendaItens { get; private set; }

    public VendaItem AdicionaProduto(Produto produto, int quantidade)
    {
        var item = new VendaItem(produto, produto.ValorUnitario, quantidade);

        this.VendaItens.Add(item);

        return item;
    }

    private void CarregaItensAPartirDoOrcamento()
    {
        foreach (var orcamentoItem in this.Orcamento.OrcamentoItems)
        {
            var item = new VendaItem(orcamentoItem.Produto, orcamentoItem.ValorUnitario, orcamentoItem.Quantidade);
            this.VendaItens.Add(item);
        }
    }
}
    
asked by anonymous 10.08.2015 / 16:50

1 answer

7

I'll go through my testing way. I do not know if it's the best, but it can be a starting point for us to produce something more concise and coherent.

The method of testing and Mocks of Gypsy

It took me a long time to find something that was good enough for a Mock and I never found it. The texts I come across about it usually talk about everything and do not explain anything, so I decided to make some homemade Mocks that work well for my case.

I'm going to go through a series of steps to produce a test model quickly.

Method 1: Not using a pre-existing database

This method is more interesting from the point of view of the test because it does not have an already ready base and its addictions.

Step 1: Extract the Interface from DbContext and use IDbSet instead of DbSet

Extracting the context interface is simple:

  • In your context class, right-click its name, select Refactor > Extract Interface ;
  • Visual Studio will suggest a name. Click Ok to generate the interface and modify the context class to use its interface.

It will look something like this:

namespace MeuProjeto.Models
{
    public class MeuProjetoContext : DbContext, MeuProjeto.Models.IMeuProjetoContext
    { 
        ...
    }
}

Now replace all occurrences of DbSet with IDbSet . This makes the system and the test project use the same context, but with different DbSet ployments.

That is, mine looked something like this:

namespace MeuProjeto.Models
{
    public class MeuProjetoContext : DbContext, MeuProjeto.Models.IMeuProjetoContext
    { 
        ...
        public IDbSet<Colaborador> Colaboradores { get; set; }
        public IDbSet<Login> Logins { get; set; }
        ...
    }
}

Step 2: Create a Test Project with the directories Controllers and Models

This part does not have much secrecy:

Step3:CreateaLieContextinModels(intestdesign)

I'massumingthathereyou'vealreadyaddedthereferenceofthemainprojectinthetestproject(right-clickReferenceAddReference...).>

AlsoinstalltheEntityFrameworkinthetestproject.JustaddingthereferencetoSystem.Entity.Datadoesnothelp.

Mylookslikethis:

namespace MeuProjeto.Testes.Models { public class MeuProjetoFakeContext : DbContext, MeuProjeto.Models.IMeuProjetoContext { ... public IDbSet<Colaborador> Colaboradores { get; set; } public IDbSet<Login> Logins { get; set; } ... } }

The advantage is that any updates to the interface you make will make you upgrade the lie context interface with just one click.

Step 4: Implement a% generic

My looks like this:

namespace MeuProjeto.Testes.Models
{
    public class FakeDbSet<T> : IDbSet<T>
    where T : class
    {
        HashSet<T> _dados;
        IQueryable _query;

        public FakeDbSet()
        {
            // Aqui não precisa ser HashSet. Pode ser uma Lista.
            _dados = new HashSet<T>();
            _query = _dados.AsQueryable();
        }

        public virtual T Find(params object[] keyValues)
        {
            throw new NotImplementedException("Derive a classe e este método para usar.");
        }
        public void Add(T item)
        {
            _dados.Add(item);
        }

        public void Remove(T item)
        {
            _dados.Remove(item);
        }

        public void Attach(T item)
        {
            _dados.Add(item);
        }
        public void Detach(T item)
        {
            _dados.Remove(item);
        }
        Type IQueryable.ElementType
        {
            get { return _query.ElementType; }
        }
        System.Linq.Expressions.Expression IQueryable.Expression
        {
            get { return _query.Expression; }
        }

        IQueryProvider IQueryable.Provider
        {
            get { return _query.Provider; }
        }
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return _dados.GetEnumerator();
        }
        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            return _dados.GetEnumerator();
        }

        T IDbSet<T>.Add(T entity)
        {
            throw new NotImplementedException("Derive a classe e este método para usar.");
        }

        T IDbSet<T>.Attach(T entity)
        {
            throw new NotImplementedException("Derive a classe e este método para usar.");
        }

        public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T
        {
            throw new NotImplementedException("Derive a classe e este método para usar.");
        }

        public T Create()
        {
            throw new NotImplementedException("Derive a classe e este método para usar.");
        }

        public System.Collections.ObjectModel.ObservableCollection<T> Local
        {
            get { throw new NotImplementedException("Derive a classe e este método para usar."); }
        }

        T IDbSet<T>.Remove(T entity)
        {
            throw new NotImplementedException("Derive a classe e este método para usar.");
        }
    }
}

Step 5: Create static classes in FakeDbSet to initialize their Models of lie

I'll give an example of how my configuration was:

namespace MeuPrjeto.Testes.Models
{
    public static class ColaboradoresConfiguration
    {
        public static IDbSet<Colaborador> MontarMockColaboradores()
        {
            return new FakeDbSet<Colaborador>
            {
                new Colaborador { ColaboradorId = Guid.NewGuid(), DataCriacao = DateTimeHelper.RandomDate(), DataNascimento = DateTimeHelper.RandomDate(), UltimaModificacao = DateTimeHelper.RandomDate() },
                new Colaborador { ColaboradorId = Guid.NewGuid(), DataCriacao = DateTimeHelper.RandomDate(), DataNascimento = DateTimeHelper.RandomDate(), UltimaModificacao = DateTimeHelper.RandomDate() },
                new Colaborador { ColaboradorId = Guid.NewGuid(), DataCriacao = DateTimeHelper.RandomDate(), DataNascimento = DateTimeHelper.RandomDate(), UltimaModificacao = DateTimeHelper.RandomDate() },
                new Colaborador { ColaboradorId = Guid.NewGuid(), DataCriacao = DateTimeHelper.RandomDate(), DataNascimento = DateTimeHelper.RandomDate(), UltimaModificacao = DateTimeHelper.RandomDate() },
                new Colaborador { ColaboradorId = Guid.NewGuid(), DataCriacao = DateTimeHelper.RandomDate(), DataNascimento = DateTimeHelper.RandomDate(), UltimaModificacao = DateTimeHelper.RandomDate() }
            };
        }
    }
}

In the lie Mock constructor, it would look like this:

    private MeuProjetoFakeContext()
    {
        Colaboradores = ColaboradoresConfiguration.MontarMockColaboradores();
    }

Step 6: Replace the original project contexts with interfaces

Here are several ways to do it. What I did was put the context in a Controller base as follows:

namespace MeuProjeto.Controllers
{
    public abstract class Controller : System.Web.Mvc.Controller
    {
        protected IMeuProjetoContext Context;

        protected Controller(IMeuProjetoContext _context = null)
        {
            // Aqui é o contexto de verdade mesmo que vai ser instanciado
            Context = _context ?? new MeuProjetoContext();
        }

        ...
    }
}

Step 6: Mount the Test Controllers

Ideally, test Controllers call the actual Controllers , but passing to the Controller this Mock we set. A test case would look like this:

namespace MeuProjeto.Testes.Controllers
{
    [TestClass]
    public class ColaboradoresControllerTest
    {
        [TestMethod]
        public void TestIndexView()
        {
            var fakeContext = new MeuProjetoFakeContext();

            var controller = new ColaboradoresController(fakeContext);
            var result = controller.Index("", null);
            // Testes sempre usam Assert.
            Assert.AreEqual("", result.ViewName);
        }
    }
}

This test is pretty silly. Just to show what you can do, and not leave the answer huge.

Method 2: Using a pre-existing database

This is much easier to do, but does not have unit test features. It consists of passing a connection string pointing to another base, identical to the default system base, and using it to perform tests, modify records, etc.

Conclusion

In doubt, I end up using both methods, but with preference for the first one at the beginning of development. For this I put two test projects per system, and if I am interested in performing the same tests using different bases, I derive a third project containing only the test classes.

    
10.08.2015 / 18:30