Test classes with dependencies (C# + Entity Framework)

Asked

Viewed 915 times

3

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

  • Mock frameworks require that we leave the methods we need "mock" as virtual, which I think is a bit risky to open this gap of being able to override a method where I see no sense of allowing this, other than by the mock’s own requirement.
  • Another way would be to create an interface for this "Budget" class to mock the interface instead of the class itself, but from what I’ve seen so far, Entity Framerwork cannot solve 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);
        }
    }
}
  • I don’t have much experience with this and I won’t answer but you’re absolutely right. This frameworks or even methodologies require doing terrible things with code to solve things they care about. For me, for what I understand and like, none of these solutions are acceptable. Complicating code to make it testable in established patterns is a bad idea. Actually, I’ve said a few times that you can test without doing these things. It just takes more work on the test. But I think it’s better than designing with the test in mind. Testability should be a consequence, not a requirement

  • @bigown, would you be referring indirectly to Design Patterns?

  • Eventually, but not primarily. Other techniques are more damaging. SOLID for example. Everything in it can be good if you need for the code to meet real needs. But I often see people applying because they’re told to, because they help with some of the secondary things, like to facilitate the test. Testing is good, but putting penduricalhos just to give the impression of low cost of testing, I am against.

  • Excellent question.

  • 1

    The way to test depends on what you want to test. Testing a class has no value - what we test are behaviours. If you can describe the classes involved and the purpose of the test (which behavior you want to test) it is easy to respond with an appropriate testing technique. I already tell you that hardly the solution will imply the use mocks. Mocks, mainly those coming in frameworks are highly complex and very little useful.

  • For what I see you don’t need Mocks but rather of Fakes. Therefore, create a manual Budget and a couple of Products is enough to make the tests.

  • @ramaral even if my Budget class is very complex and depending on other classes like products for example and this is also complex? Because today I do everything manually as you say, but I am in trouble when I need to modify these dependent classes and I need to update these fakes, even if the modification is not relevant to my Sell class and I thought mocks would be more appropriate.

  • When you just need data use Fakes. When the class under test depends on results of methods from other classes(dependencies) use Mocks. Note that to create a Mock you just need the Interface or it is possible to create Mocks without having the class implemented. If your classes have many dependencies (in-depth) consider using an Ioc Container.

  • Unit testing is a laborious thing, the cost/benefit ratio of doing it should always be considered. If you choose to do so I advise you to start by writing the test first and then the code (TDD).

  • That class Venda is a Model?

  • @Yes, it’s a model, I’m trying to follow the proposal of Domain-Driven Design

  • Bad idea. I think I already know where you got the initiative. Her author came to discuss with me these days. I recommend you start with the simplest, understand well the MVC and then diversify your application. Don’t put complexity in the system where you don’t need it. A Model MVC is not a classic POCO.

  • @Ciganomorrisonmendez the author of the book Domain-Driven Design, Eric Evans? I am looking to simplify my business rules as much as possible and to leave them independent of anything else (BD, Views), for organization and maintenance purposes and I thought it would match the proposal to develop thinking in the domain of the problem. And as far as I know, I can use MVC very well for this, being the View and Controller in a layer more external and the Model in its core (Onion Architeture), being able to focus still in my domains.

  • 1

    I wrote a text about it, which has not yet been published. I argue that the gain of implementing the DDD in MVC is zero. All this behavioral logic that you put in the class is done differently in MVC, and the supposed gain in organization and maintenance does not exist, even because MVC needs an organizational standard to function properly, and the model of abstraction makes you focus on what is important, which are the rules of business, and not on maintenance details. I said it before and I’ll say it again: it’s best to abandon this idea of DDD in MVC.

  • Interesting! Please send the link where your article will be published to read please @Ciganomorrisonmendez! Thanks for the clarification

  • @Ricardokenjiharasaki Out already: http://masterdesigners.com.br/quando-boa-pratica-vira-ma-pratica/

  • @Gypsy omorrisonmendez link broke, has how to update?

  • @Math I think the text is completely lost. I have another similar text here.

Show 13 more comments

1 answer

7

I will pass a way of my testing. I do not know if it is the best, but it can be a starting point for us to produce something more concise and coherent.

The testing method and Mocks gypsy

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

I’m going to take a series of steps to produce a test model quickly.

Method 1: Not using a pre-existing base

This method I find more interesting from the point of view of the test because it does not count with a ready base and its vices.

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

Extracting the context interface is simple:

  • In the class of your context, right-click on 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.

There’ll be something like that:

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

Replace now all occurrences of DbSet for IDbSet. This causes the system and the test project to use the same context, but with implementations of DbSet different.

I mean, 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

That part doesn’t have much of a secret:

Criar Projeto de Teste

Step 3: Create a Lie Context in Models (in the test project)

I am assuming that here you have already added the reference of the main project in the test project (right click on Reference > Add Reference...).

Also install the Entity Framework in the test project. Just add the reference to System.Entity.Data no use.

Mine was like this:

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 and all update on the interface you do will make you update the interface of the lie context with just one click.

Step 4: Implement a FakeDbSet generic

Mine was 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 Models to initialize your DbSets fake

Let me give you an example of how a configuration of mine:

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 builder of Mock fake, would be like this:

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

Step 6: Replace original project contexts with interfaces

Here are several ways to do it. What I did was put the context into a Controller basis 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: Assemble the Controllers test

Ideally, the Controllers test call the Controllers real, but passing to the Controller this Mock that we set up. 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 really silly. Just to show what you can do, and not leave the huge answer.

Method 2: Using a pre-existing base

This is much easier to do but has no unit test characteristics. It consists of passing to the context a Connection string pointing to another base, identical to the standard base of the system, and using it to perform tests, modify records, etc.

Completion

When in doubt, I end up using both methods, but with preference for the first at the beginning of development. For this I assemble two test projects per system, and if I want to carry out the same tests using different bases, I derive a third project containing only the test classes.

  • 1

    Very cool your method to be able to test my service layer without needing mock frameworks!

Browser other questions tagged

You are not signed in. Login or sign up in order to post.