in ASP.NET

Testes com Moq e Entity Framework (EF6)

Recentemente estava implementando um projeto utilizando TDD, onde as principais regras de negócios se baseavam em consultas e filtros aplicados ao banco de dados, por exemplo: “retorna todos os descontos ativos (select * from descontos where status=1 and datafim>getdate()”.

Pensando em uma abordagem para garantir a implementação destas diversas regras, acabei optando por utilizar o Entity Framework (EF6) para o acesso aos dados e o Moq como framework de mock.

Moq

O Moq é um framework para mock extremamente poderoso, que ajuda a simular o comportamento e ações de objetos de uma maneira controlada. Com ele é possível simular o retorno de um método de um objeto, e ainda realizar diversos testes como quantidade de get ou set em uma determinada propriedade, ou até mesmo verificar quantas vezes determinado método do objeto foi chamado.

https://github.com/Moq/moq4

https://github.com/Moq/moq4/wiki/Quickstart

PM> Install-Package Moq

Entity Framework (EF6)

Para este projeto vou utilizar o Entity Framework (a partir da versão 6), ele é um ORM que torna possível utilizar e fazer o mapeamento do banco de dados para os objetos de domínio de nossa aplicação, podendo ser utilizado com Code First e EF Design.

No exemplo que irei mostrar utilizaremos o Code First, porém com algumas mudanças é possível utilizar a mesma abordagem para testes no EF Design.

http://www.asp.net/entity-framework

PM> Install-Package EntityFramework

O Projeto

Neste exemplo, vou criar um projeto de um site que permite listar e adicionar livros. A única regra que irei implementar de consulta é retornar os livros que estão ativos, no meu caso um livro ativo é um livro que possui a flag de ativo.

Para isso criei dois projetos, o ProjetoLivro.BL (class library) e o ProjetoLivro.Web (ASP.NET MVC).

No ProjetoLivro.BL eu adicionei 3 classes e dei um adicionei o EntityFramework via nuget: “Install-Package EntityFramework”.

image

Livro.cs

namespace ProjetoLivro.BL
{
    public class Livro
    {
        public int Id { get; set; }
        public string Nome { get; set; }
        public bool Status { get; set; }

    }
}

LivroContext.cs

using System.Data.Entity;

namespace ProjetoLivro.BL
{
    public class LivroContext : DbContext
    {
        public virtual DbSet<Livro> Livros { get; set; }
    }
}

LivroService.cs

using System.Collections.Generic;
using System.Linq;

namespace ProjetoLivro.BL
{
    public class LivroService
    {
        private LivroContext _context { get; set; }
        public LivroService(LivroContext context)
        {
            _context = context;
        }

        public List<Livro> GetLivrosAtivos()
        {
            return _context.Livros.Where(a => a.Status == true).ToList();
        }

        public void Salvar(Livro livro)
        {
            _context.Livros.Add(livro);
            _context.SaveChanges();
        }
    }
}

Após adicionar estas classe, no projeto ProjetoLivro.Web irei adicionar o LivroController e as Views responsáveis pela apresentação dos livros.

LivroController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using ProjetoLivro.BL;

namespace ProjetoLivro.Web.Controllers
{
    public class LivroController : Controller
    {
        //Nao façam isso!!!
        private LivroService livroService = new LivroService(new LivroContext());

        public ActionResult Index()
        {
            return View(livroService.GetLivrosAtivos());
        }

        // GET: /Livro/Create
        public ActionResult Create()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create([Bind(Include = "Id,Nome,Status")] Livro livro)
        {
            if (ModelState.IsValid)
            {
                livroService.Salvar(livro);

                return RedirectToAction("Index");
            }

            return View(livro);
        }
    }
}

Views/Livro/Index.cshtml

@model IEnumerable<ProjetoLivro.BL.Livro>

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Nome)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Status)
        </th>
        <th></th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Nome)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Status)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
            @Html.ActionLink("Details", "Details", new { id=item.Id }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.Id })
        </td>
    </tr>
}

</table>

/Views/Livro/Create.cshtml

@model ProjetoLivro.BL.Livro

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>


@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <h4>Livro</h4>
        <hr />
        @Html.ValidationSummary(true)

        <div class="form-group">
            @Html.LabelFor(model => model.Nome, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Nome)
                @Html.ValidationMessageFor(model => model.Nome)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Status, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Status)
                @Html.ValidationMessageFor(model => model.Status)
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Com isso temos o projeto funcionando, permitindo inserir e listar livros.

1, 2, 3 Testando

Agora vamos começar a testar (claro que eu poderia ter implementar utilizando TDD, como foi no caso do projeto que estava desenvolvendo), mas como o intuito deste post é mostrar como implementar testes em uma camada que está acessando os dados, vamos focar nisto.

Basicamente implementarei dois testes no LivroService.cs, que é a classe onde tenho duas ações que quero garantir que estão corretas:

  • Ao criar um livro o Context está sendo salvo (NoQuery Test).
  • GetLivrosAtivos retorna somente livros que estão com o status de ativo (Query Test).

Para isto, vou criar um projeto de teste do tipo Unit Test Project.

image

Neste projeto vou adicionar o Moq via nuget PM> Install-Package Moq

e também será necessário adicionar o EntityFramework PM> Install-Package EntityFramework

além disso vou referenciar o projeto ProjetoLivro.BL (o qual iremos testar).

No Query Test

Para testar o cenário de adicionar um livro eu quero garantir que ao chamar o método Salvar do LivroService, internamente ele chame exatamente 1 vez o método SaveChanges do Context.

Neste caso, utilizarei o Moq para criar dois objetos controlados: um DbSet<Livro> (moskSet)  e o próprio LivroContext (mockContext). Após criar estes dois objetos eu configuro o mockContext utilizando o método Setup para que ao acessarem a propriedade Livros, seja retornado o objeto controlado mockSet (linha 18).

Depois de criar e configurar o comportamento destes objetos eu passo o objeto do mockContext para o LivroService (linha 20).

Com o LivroService ok, podemos chamar o método Salvar, passando um novo livro como parâmetro. E por fim, validar no mockSet se um Livro foi adicionado uma única vez (linha 23) e verificar por ultimo, se o método SaveChages do mockContext foi chamado uma única vez (linha 24).

LivroServiceTest.cs

using System.Data.Entity;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using ProjetoLivro.BL;

namespace ProjetoLivro.Test
{
    [TestClass]
   public class LivroServiceTest
    {
        ////noQuery test
        [TestMethod]
        public void CriarLivroSalvaOContext()
        {
            var mockSet = new Mock<DbSet<Livro>>();

            var mockContext = new Mock<LivroContext>();
            mockContext.Setup(m => m.Livros).Returns(mockSet.Object);

            var service = new LivroService(mockContext.Object);
            service.Salvar(new Livro() { Nome = "Livro de teste", Status = true });

            mockSet.Verify(m => m.Add(It.IsAny<Livro>()), Times.Once());
            mockContext.Verify(m => m.SaveChanges(), Times.Once());
        }
    }
}

Com isso nós temos nosso teste funcionando.

image

Imaginem que durante uma manutenção, um desenvolvedor alterou o método que salva um Livro, e por engano duplicou o Add, no momento que os testes forem executados, este bug será diagnosticado, evitando assim um problema de duplicação em produção. O mesmo ocorre caso ele deixe de chamar o SaveChanges.

image

Query Test

Para testar o segundo cenário (a consulta pro livros ativos) vamos começar criando uma massa de dados em um objeto com o nome data (lista de livros), e configurar o nosso mockSet para utilizar esta massa de dados como retorno, isto server para simular em memória os objetos que teríamos na nossa tabela do banco de dados (linhas 5-16).

Após isso vou configurar o mockContext para utilizar este mockSet para o retorno da propriedade Livros (linha 19), com estas configurações, podemos instanciar o LivroService, passando o nosso mockContext, e executar o método GetLivrosAtivos, o qual retorna uma coleção com os livros ativos.

Como teste verificarei o total de livros retornados, notem que eu simulei uma tabela com 3 livros, onde somente 2 eram ativos. No teste eu valido a quantidade e os nomes dos livros retornados (linha 24-27), a fim de garantir que a regra de trazer livros ativos está correta.

////Query test
[TestMethod]
public void RetornaSomenteLivrosAtivos()
{
    var data = new List<Livro>
    {
        new Livro { Nome = "Livro A", Status=true },
        new Livro { Nome = "Livro B", Status=false },
        new Livro { Nome = "Livro C", Status=true },
    }.AsQueryable();

    var mockSet = new Mock<DbSet<Livro>>();
    mockSet.As<IQueryable<Livro>>().Setup(m => m.Provider).Returns(data.Provider);
    mockSet.As<IQueryable<Livro>>().Setup(m => m.Expression).Returns(data.Expression);
    mockSet.As<IQueryable<Livro>>().Setup(m => m.ElementType).Returns(data.ElementType);
    mockSet.As<IQueryable<Livro>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
 
    var mockContext = new Mock<LivroContext>();
    mockContext.Setup(c => c.Livros).Returns(mockSet.Object);

    var service = new LivroService(mockContext.Object);
    var livros = service.GetLivrosAtivos();

    Assert.AreEqual(2, livros.Count);
    Assert.AreEqual("Livro A", livros[0].Nome);
    Assert.AreEqual("Livro C", livros[1].Nome);

}

Agora temos em nosso projeto dois testes executando com sucesso \o/.

image

Da mesma maneira do teste anterior, imaginando um cenário de manutenção ou mudança de código, caso alguma regra seja alterada no método que retorna os livros ativos, por exemplo, retirada a validação de status do Livro, ao executar os testes teremos o problema facilmente identificado.

image

Próximos passos

Primeiro queria lembrar que a ideia deste post é somente demonstrar um caminho para implementar este tipo de testes, implementar testes ou desenvolver com testes envolve muitos conceitos importantes de design de código, injeção de dependência, padrões de projetos e muitas outras coisas. Abaixo deixo alguns links para estudo Alegre.

O projeto que criei como exemplo está disponivel no GitHub:

https://github.com/rodolfofadino/TestesComMoqEEntityFramework

Espero que este post seja útil, estou a disposição para qualquer dúvida, critica ou sugestão.

abs

Rodolfo

  • Anonymous

    Muito bom! Desse jeito, você consegue pegar erros com o .Include (relacionados ao LazyLoading)?

  • http://www.rodolfofadino.com.br/ Rodolfo Fadino

    Luis eu tentei simular alguns cenários para testar aqui e não consegui “testar” este comportamento. Vou dar uma estudada e te falo, se você achar alguma coisa posta aqui também 🙂

  • Anonymous

    Na verdade, eu fiz um exemplo utilizando o effort (http://effort.codeploex.com) – https://github.com/luisrudge/nancy-ef-effort-integration-test

    Nesse exemplo, não tem Mock. O Effort cria um Provider para o entity framework que roda inteiramenta na memória.

    Dessa maneira, você pode testar sem código dando asserts diretamente no contexto:

    Db.Users.Count().ShouldEqual(0);

  • http://www.rodolfofadino.com.br/ Rodolfo Fadino

    Não conhecia o Effort, bem legal, claro que utilizar o Moq tem alguns beneficios de tracking para implementar os testes (como por exemplo quantas vezes determinado método foi chamado), vou testar e estudar o Effort também 🙂

  • Daniel Ramos

    Muito bom amigo! Exatamente o que eu procurava. Recomenda alguma leitura sobre Moq?

    Abraços!