Como você está testando a unidade de pessoas com o Entity Framework 6, deve se preocupar?


170

Estou apenas começando com testes de unidade e TDD em geral. Eu já brinquei antes, mas agora estou determinado a adicioná-lo ao meu fluxo de trabalho e escrever um software melhor.

Ontem fiz uma pergunta que incluía isso, mas parece ser uma pergunta por si só. Sentei-me para começar a implementar uma classe de serviço que usarei para abstrair a lógica de negócios dos controladores e mapear modelos específicos e interações de dados usando o EF6.

O problema é que eu já me bloqueei porque não queria abstrair a EF em um repositório (ele ainda estará disponível fora dos serviços para consultas específicas, etc.) e gostaria de testar meus serviços (o Contexto da EF será usado) .

Aqui eu acho que é a pergunta, há um ponto para fazer isso? Em caso afirmativo, como as pessoas estão fazendo isso de maneira natural à luz das abstrações com vazamentos causadas pelo IQueryable e das muitas ótimas postagens de Ladislav Mrnka sobre o assunto de testes de unidade não serem diretas devido às diferenças nos provedores Linq ao trabalhar com uma memória implementação, ao contrário de um banco de dados específico.

O código que quero testar parece bastante simples. (este é apenas um código fictício para tentar entender o que estou fazendo, quero conduzir a criação usando TDD)

Contexto

public interface IContext
{
    IDbSet<Product> Products { get; set; }
    IDbSet<Category> Categories { get; set; }
    int SaveChanges();
}

public class DataContext : DbContext, IContext
{
    public IDbSet<Product> Products { get; set; }
    public IDbSet<Category> Categories { get; set; }

    public DataContext(string connectionString)
                : base(connectionString)
    {

    }
}

Serviço

public class ProductService : IProductService
{
    private IContext _context;

    public ProductService(IContext dbContext)
    {
        _context = dbContext;
    }

    public IEnumerable<Product> GetAll()
    {
        var query = from p in _context.Products
                    select p;

        return query;
    }
}

Atualmente, estou pensando em fazer algumas coisas:

  1. Zombando do EF com algo parecido com esta abordagem - Zombando do EF ao testar unidades ou usando diretamente uma estrutura de zombaria na interface como moq - sentindo a dor que os testes de unidade podem passar, mas não necessariamente trabalhar de ponta a ponta e fazer backup deles com testes de integração?
  2. Talvez usando algo como o esforço para zombar de EF - eu nunca o usei e não tenho certeza se alguém mais está usando isso na natureza?
  3. Não se preocupe em testar qualquer coisa que simplesmente chame de volta à EF - então, essencialmente, os métodos de serviço que chamam a EF diretamente (getAll etc.) não são testados em unidade, mas apenas testados em integração?

Alguém aí realmente está fazendo isso por aí sem um Repo e tendo sucesso?


Ei Modika, eu estava pensando sobre isso recentemente (por causa desta pergunta: stackoverflow.com/questions/25977388/… ) Nele, tento descrever um pouco mais formalmente como trabalho no momento, mas gostaria de saber como você está fazendo isso.
samy

Oi @ samy, a maneira como decidimos fazer não era o teste de unidade que tocava diretamente a EF. As consultas foram testadas, mas como teste de integração, não como testes de unidade. Zombar da EF parece um pouco sujo, mas esse projeto foi pequeno, então o impacto no desempenho de ter vários testes acessando um banco de dados não era realmente uma preocupação, para que pudéssemos ser um pouco mais pragmáticos. Ainda não tenho 100% de certeza de qual é a melhor abordagem para ser completamente sincero com você, em algum momento você atingirá a EF (e seu DB) e o teste de unidade não me parece correto aqui.
Modika

Respostas:


186

Este é um tópico no qual estou muito interessado. Há muitos puristas que dizem que você não deve testar tecnologias como EF e NHibernate. Eles estão certos, eles já foram rigorosamente testados e, como uma resposta anterior afirmou, muitas vezes não faz sentido gastar muito tempo testando o que você não possui.

No entanto, você possui o banco de dados abaixo! É aqui que, na minha opinião, essa abordagem é quebrada, não é necessário testar se a EF / NH está fazendo o trabalho corretamente. Você precisa testar se seus mapeamentos / implementações estão funcionando com seu banco de dados. Na minha opinião, essa é uma das partes mais importantes de um sistema que você pode testar.

A rigor, porém, estamos saindo do domínio do teste de unidade para o teste de integração, mas os princípios permanecem os mesmos.

A primeira coisa que você precisa fazer é poder zombar do seu DAL para que seu BLL possa ser testado independentemente do EF e do SQL. Estes são os seus testes de unidade. Em seguida, você precisa projetar seus testes de integração para provar seu DAL. Na minha opinião, eles são igualmente importantes.

Há algumas coisas a considerar:

  1. Seu banco de dados precisa estar em um estado conhecido a cada teste. A maioria dos sistemas usa um backup ou cria scripts para isso.
  2. Cada teste deve ser repetível
  3. Cada teste deve ser atômico

Existem duas abordagens principais para configurar seu banco de dados, a primeira é executar um script de criação de banco de dados do UnitTest. Isso garante que seu banco de dados de teste de unidade esteja sempre no mesmo estado no início de cada teste (você pode redefinir isso ou executar cada teste em uma transação para garantir isso).

Sua outra opção é o que eu faço, execute configurações específicas para cada teste individual. Eu acredito que esta é a melhor abordagem por duas razões principais:

  • Seu banco de dados é mais simples, você não precisa de um esquema inteiro para cada teste
  • Cada teste é mais seguro, se você alterar um valor no script de criação, ele não invalidará dezenas de outros testes.

Infelizmente, seu compromisso aqui é a velocidade. Leva tempo para executar todos esses testes, para executar todos esses scripts de configuração / desmontagem.

Um ponto final, pode ser um trabalho muito difícil escrever uma quantidade tão grande de SQL para testar seu ORM. É aqui que tomo uma abordagem muito desagradável (os puristas aqui vão discordar de mim). Eu uso meu ORM para criar meu teste! Em vez de ter um script separado para cada teste de DAL no meu sistema, tenho uma fase de configuração de teste que cria os objetos, os anexa ao contexto e os salva. Eu então executo meu teste.

Isso está longe de ser a solução ideal, no entanto, na prática, acho muito mais fácil gerenciar (especialmente quando você tem vários milhares de testes); caso contrário, você está criando um grande número de scripts. Praticidade sobre pureza.

Sem dúvida, analisarei essa resposta em alguns anos (meses / dias) e discordarei de mim mesmo à medida que minhas abordagens mudaram - no entanto, essa é minha abordagem atual.

Para tentar resumir tudo o que eu disse acima, este é meu teste de integração de banco de dados típico:

[Test]
public void LoadUser()
{
  this.RunTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    return user.UserID;
  }, id => // the ID of the entity we need to load
  {
     var user = LoadMyUser(id); // load the entity
     Assert.AreEqual("Mr", user.Title); // test your properties
     Assert.AreEqual("Joe", user.Firstname);
     Assert.AreEqual("Bloggs", user.Lastname);
  }
}

O principal a notar aqui é que as sessões dos dois loops são completamente independentes. Na sua implementação do RunTest, você deve garantir que o contexto seja confirmado e destruído e seus dados só possam vir do seu banco de dados para a segunda parte.

Editar 13/10/2014

Eu disse que provavelmente revisaria esse modelo nos próximos meses. Embora eu mantenha a abordagem que defendi acima, atualizei levemente meu mecanismo de teste. Agora, tenho a tendência de criar as entidades no TestSetup e TestTearDown.

[SetUp]
public void Setup()
{
  this.SetupTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    this.UserID =  user.UserID;
  });
}

[TearDown]
public void TearDown()
{
   this.TearDownDatabase();
}

Em seguida, teste cada propriedade individualmente

[Test]
public void TestTitle()
{
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Mr", user.Title);
}

[Test]
public void TestFirstname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Joe", user.Firstname);
}

[Test]
public void TestLastname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Bloggs", user.Lastname);
}

Existem várias razões para essa abordagem:

  • Não há chamadas adicionais ao banco de dados (uma configuração, uma desmontagem)
  • Os testes são muito mais granulares, cada teste verifica uma propriedade
  • A lógica Setup / TearDown é removida dos próprios métodos de teste

Eu sinto que isso torna a classe de teste mais simples e os testes mais granulares ( declarações simples são boas )

Editar 03/05/2015

Outra revisão sobre essa abordagem. Embora as configurações em nível de classe sejam muito úteis para testes, como carregar propriedades, elas são menos úteis quando são necessárias diferentes configurações. Nesse caso, a criação de uma nova classe para cada caso é um exagero.

Para ajudar com isso, agora tenho duas classes base SetupPerTeste SingleSetup. Essas duas classes expõem a estrutura conforme necessário.

No SingleSetuptemos um mecanismo muito semelhante ao descrito na minha primeira edição. Um exemplo seria

public TestProperties : SingleSetup
{
  public int UserID {get;set;}

  public override DoSetup(ISession session)
  {
    var user = new User("Joe", "Bloggs");
    session.Save(user);
    this.UserID = user.UserID;
  }

  [Test]
  public void TestLastname()
  {
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Bloggs", user.Lastname);
  }

  [Test]
  public void TestFirstname()
  {
       var user = LoadMyUser(this.UserID);
       Assert.AreEqual("Joe", user.Firstname);
  }
}

No entanto, referências que garantem que apenas as entidades corretas sejam carregadas podem usar uma abordagem SetupPerTest

public TestProperties : SetupPerTest
{
   [Test]
   public void EnsureCorrectReferenceIsLoaded()
   {
      int friendID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriend();
         session.Save(user);
         friendID = user.Friends.Single().FriendID;
      } () =>
      {
         var user = GetUser();
         Assert.AreEqual(friendID, user.Friends.Single().FriendID);
      });
   }
   [Test]
   public void EnsureOnlyCorrectFriendsAreLoaded()
   {
      int userID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriends(2);
         var user2 = CreateUserWithFriends(5);
         session.Save(user);
         session.Save(user2);
         userID = user.UserID;
      } () =>
      {
         var user = GetUser(userID);
         Assert.AreEqual(2, user.Friends.Count());
      });
   }
}

Em resumo, ambas as abordagens funcionam dependendo do que você está tentando testar.


2
Aqui está uma abordagem diferente para testes de integração. TL; DR - Use o próprio aplicativo para configurar dados de teste, reverter uma transação por teste.
Gert Arnold

3
@ Liath, ótima resposta. Você confirmou minhas suspeitas sobre o teste de EF. Minha pergunta é essa; seu exemplo é para um caso muito concreto, o que é bom. No entanto, como você observou, pode ser necessário testar centenas de entidades. De acordo com o princípio DRY (Não se repita), como você escala sua solução, sem repetir o mesmo padrão de código básico todas as vezes?
Jeffrey A. Gochin

4
Eu tenho que discordar disso, porque evita completamente o problema. Teste de unidade é sobre testar a lógica da função. No exemplo do OP, a lógica depende de um armazenamento de dados. Você está certo quando diz para não testar a EF, mas esse não é o problema. O problema está testando seu código isoladamente do armazenamento de dados. Testar seu mapeamento é um tópico totalmente diferente. Para testar se a lógica está interagindo com os dados corretamente, você precisa controlar o armazenamento.
Sinaesthetic 08/08/16

7
Ninguém está em dúvida sobre se você deve testar o Entity Framework por si só. O que acontece é que você precisa testar algum método que faz algumas coisas e também faz uma chamada EF para o banco de dados. O objetivo é zombar do EF para que você possa testar esse método sem precisar de um banco de dados no servidor de construção.
O homem de muffin

4
Eu realmente gosto da jornada. Obrigado por adicionar edições ao longo do tempo - é como ler o controle da fonte e entender como o seu pensamento evoluiu. Eu realmente aprecio a distinção funcional (com EF) e de unidade (EF zombado) também.
precisa

21

Feedback da experiência do esforço aqui

Depois de muita leitura, tenho usado o Effort em meus testes: durante os testes, o Contexto é construído por uma fábrica que retorna uma versão em memória, o que me permite testar contra uma folha em branco a cada vez. Fora dos testes, a fábrica é resolvida para uma que retorne todo o contexto.

No entanto, tenho a sensação de que o teste contra uma simulação completa do banco de dados tende a arrastar os testes para baixo; você percebe que precisa cuidar de configurar um monte de dependências para testar uma parte do sistema. Você também tende a organizar testes juntos que podem não estar relacionados, apenas porque existe apenas um objeto enorme que lida com tudo. Se você não prestar atenção, poderá fazer testes de integração em vez de testes de unidade

Eu preferiria testar contra algo mais abstrato do que um grande DBContext, mas não consegui encontrar o ponto ideal entre testes significativos e testes simples. Giz até minha inexperiência.

Então, acho o esforço interessante; se você precisar entrar em ação, é uma boa ferramenta para começar rapidamente e obter resultados. No entanto, acho que algo um pouco mais elegante e abstrato deve ser o próximo passo e é isso que vou investigar a seguir. Favoritar este post para ver para onde ele vai a seguir :)

Edite para adicionar : o esforço leva algum tempo para se aquecer; você está olhando aprox. 5 segundos no início do teste. Isso pode ser um problema para você, se você precisar que seu conjunto de testes seja muito eficiente.


Editado para esclarecimentos:

Eu usei o Effort para testar um aplicativo de serviço da web. Cada mensagem M que entra é encaminhada para uma IHandlerOf<M>via Windsor. Castle.Windsor resolve o IHandlerOf<M>que resolve novamente as dependências do componente. Uma dessas dependências é aDataContextFactory , que permite que o manipulador solicite a fábrica

Nos meus testes, instancio o componente IHandlerOf diretamente, zomba de todos os subcomponentes do SUT e lida com o envoltório de esforço DataContextFactorydo manipulador.

Isso significa que eu não teste unitário em sentido estrito, pois o banco de dados é atingido pelos meus testes. No entanto, como eu disse acima, deixe-me bater no chão correndo e eu poderia testar rapidamente alguns pontos no aplicativo


Obrigado pela contribuição, o que posso fazer para executar esse projeto, pois é um trabalho pago genuíno, é começar com alguns repositórios e ver como eu entro, mas o esforço é muito interessante. Não interessa em que camada você tem usado esforços em seus aplicativos?
Modika 27/03

2
somente se o Effort tivesse suportado transações corretamente #
Sedat Kapanoglu

e esforço tem um bug para strings com o carregador csv, quando usamos '' em vez de null em strings.
Sam

13

Se você deseja testar o código da unidade , é necessário isolar o código que deseja testar (neste caso, o seu serviço) de recursos externos (por exemplo, bancos de dados). Você provavelmente poderia fazer isso com algum tipo de provedor EF na memória , no entanto, uma maneira muito mais comum é abstrair sua implementação EF, por exemplo, com algum tipo de padrão de repositório. Sem esse isolamento, qualquer teste que você escrever será de integração, não de unidade.

Quanto ao teste do código EF - escrevo testes de integração automatizados para meus repositórios que gravam várias linhas no banco de dados durante sua inicialização e, em seguida, chamo minhas implementações de repositório para garantir que elas se comportem conforme o esperado (por exemplo, para garantir que os resultados sejam filtrados corretamente ou que eles estão classificados na ordem correta).

Esses são testes de integração e não testes de unidade, pois os testes contam com a presença de uma conexão com o banco de dados e o banco de dados de destino já possui o esquema atualizado mais recente.


Obrigado @justin eu sei sobre o padrão do repositório, mas ler coisas como ayende.com/blog/4784/… e lostechies.com/jimmybogard/2009/09/11/wither-the-repository entre outros me fez pensar que eu não não quero essa camada de abstração, mas, novamente, eles falam mais sobre uma abordagem de consulta, que fica muito confusa.
Modika 27/03

7
O @Modika Ayende escolheu uma implementação ruim do padrão de repositório para criticar e, como resultado, é 100% correto - seu excesso de engenharia e não oferece nenhum benefício. Uma boa implementação isola as partes testáveis ​​em unidade do seu código da implementação do DAL. O uso do NHibernate e do EF diretamente torna o código difícil (se não impossível) para o teste de unidade e leva a uma base de código monolítica rígida. Ainda sou um pouco cético em relação ao padrão do repositório, no entanto, estou 100% convencido de que você precisa isolar sua implementação do DAL de alguma forma e o repositório é a melhor coisa que encontrei até agora.
27714 Justin

2
@Modika Leia o segundo artigo novamente. "Eu não quero essa camada de abstração" não é o que ele diz. Além disso, leia sobre o padrão original do repositório em Fowler ( martinfowler.com/eaaCatalog/repository.html ) ou DDD ( dddcommunity.org/resources/ddd_terms ). Não acredite em pessimistas sem entender completamente o conceito original. O que eles realmente criticam é um uso indevido recente do padrão, não do próprio padrão (embora provavelmente não saibam disso).
precisa

1
@ guillaume31 Eu não sou contra o padrão de repositório (eu o entendo), estou simplesmente tentando descobrir se preciso abstrair o que já é uma abstração nesse nível e se posso omitir e testar diretamente a EF, zombando e usá-lo em meus testes em uma camada mais alta no meu aplicativo. Além disso, se eu não usar um repositório, obterá o benefício do conjunto de recursos estendidos da EF, com um repositório talvez não seja possível.
Modika 27/03

Depois de isolar o DAL com um repositório, preciso de alguma forma "zombar" do banco de dados (EF). Até agora, zombar do contexto e várias extensões assíncronas (ToListAsync (), FirstOrDefaultAsync () etc.) resultaram em frustração para mim.
Kevin Burton

9

Então, o Entity Framework é uma implementação. Apesar de abstrair a complexidade da interação do banco de dados, interagir diretamente ainda é um acoplamento rígido e é por isso que é confuso testar.

O teste de unidade consiste em testar a lógica de uma função e cada um de seus possíveis resultados isoladamente de quaisquer dependências externas, que neste caso são o armazenamento de dados. Para fazer isso, você precisa poder controlar o comportamento do armazenamento de dados. Por exemplo, se você deseja afirmar que sua função retorna false se o usuário buscado não atender a algum conjunto de critérios, seu armazenamento de dados [ridicularizado] deve ser configurado para sempre retornar um usuário que não atenda aos critérios e vice-versa. versa para a afirmação oposta.

Com isso dito, e aceitando o fato de que a EF é uma implementação, eu provavelmente favoreceria a idéia de abstrair um repositório. Parece um pouco redundante? Não é, porque você está resolvendo um problema que está isolando seu código da implementação de dados.

No DDD, os repositórios sempre retornam raízes agregadas, não o DAO. Dessa forma, o consumidor do repositório nunca precisa saber sobre a implementação de dados (como não deveria) e podemos usá-lo como um exemplo de como resolver esse problema. Nesse caso, o objeto gerado pelo EF é um DAO e, como tal, deve estar oculto do seu aplicativo. Esse é outro benefício do repositório que você define. Você pode definir um objeto de negócios como seu tipo de retorno em vez do objeto EF. Agora, o que o repo faz é ocultar as chamadas para o EF e mapeia a resposta do EF para esse objeto de negócios definido na assinatura do repos. Agora você pode usar esse repositório no lugar da dependência do DbContext que você injeta em suas classes e, consequentemente, agora pode simular essa interface para fornecer o controle necessário para testar seu código isoladamente.

É um pouco mais trabalhoso e muitos dão o que falar, mas resolve um problema real. Existe um provedor na memória mencionado em uma resposta diferente que pode ser uma opção (eu ainda não tentei), e sua própria existência é uma evidência da necessidade da prática.

Eu discordo completamente da resposta principal, porque ela contorna o problema real que está isolando seu código e, em seguida, continua tangente ao testar seu mapeamento. Teste sempre o seu mapeamento, se quiser, mas resolva o problema real aqui e obtenha uma cobertura de código real.


8

Eu não código de teste de unidade que não possuo. O que você está testando aqui, que o compilador MSFT funciona?

Dito isso, para tornar esse código testável, você quase precisa separar sua camada de acesso a dados do código lógico de negócios. O que faço é pegar todo o meu material EF e colocá-lo em uma (ou várias) classes DAO ou DAL, que também possui uma interface correspondente. Em seguida, escrevo meu serviço, que terá o objeto DAO ou DAL injetado como uma dependência (preferencialmente a injeção do construtor) referenciada como interface. Agora, a parte que precisa ser testada (seu código) pode ser facilmente testada zombando da interface do DAO e injetando-a na sua instância de serviço dentro do seu teste de unidade.

//this is testable just inject a mock of IProductDAO during unit testing
public class ProductService : IProductService
{
    private IProductDAO _productDAO;

    public ProductService(IProductDAO productDAO)
    {
        _productDAO = productDAO;
    }

    public List<Product> GetAllProducts()
    {
        return _productDAO.GetAll();
    }

    ...
}

Eu consideraria as Camadas de Acesso a Dados ao vivo como parte dos testes de integração, não dos testes de unidade. Eu já vi caras verificando quantas viagens ao hibernate de banco de dados faz antes, mas eles estavam em um projeto que envolvia bilhões de registros em seu armazenamento de dados e essas viagens extras realmente importavam.


1
Obrigado pela resposta, mas qual seria a diferença disso para dizer um Repositório em que você está escondendo os internos da EF por trás desse nível? Eu realmente não quero abstrair o EF, embora eu ainda possa estar fazendo isso com a interface do IContext? Eu sou novo para isso, seja gentil :)
Modika

3
@Modika A Repo também está bem. Qualquer padrão que você quiser. "Eu realmente não quero abstrair a EF" Você quer código testável ou não?
Jonathan Henson

1
@Modika Meu argumento é que você não terá QUALQUER código testável se não separar suas preocupações. O acesso a dados e a lógica de negócios DEVEM estar em camadas separadas para realizar bons testes de manutenção.
Jonathan Henson

2
Eu simplesmente não senti a necessidade de agrupar EF em uma abstração de repositório, pois essencialmente os IDbSets são repositórios e o contexto é UOW, atualizarei minha pergunta um pouco, pois isso pode ser enganoso. O problema vem com qualquer abstração e o ponto principal é o que exatamente estou testando, porque minhas filas não serão executadas nos mesmos limites (linq-para-entidades vs linq-para-objetos), portanto, se estiver testando apenas se meu serviço faz um chamada que parece um pouco desperdício ou estou bem aqui?
Modika 27/03

1
Embora eu concorde com seus pontos gerais, o DbContext é uma unidade de trabalho e o IDbSets é definitivamente para a implementação do repositório, e não sou o único a pensar isso. Posso zombar da EF e, em alguma camada, precisarei executar testes de integração, isso realmente importa se eu fizer isso em um Repositório ou mais adiante em um Serviço? Estar fortemente acoplado a um banco de dados não é realmente uma preocupação, tenho certeza que isso acontece, mas não planejarei algo que possa não ocorrer.
Modika 27/03

8

Eu me atrapalhei em algum momento para chegar a estas considerações:

1- Se meu aplicativo acessa o banco de dados, por que o teste não deve? E se houver algo errado com o acesso a dados? Os testes devem conhecê-lo de antemão e me alertar sobre o problema.

2- O Padrão do Repositório é um tanto difícil e demorado.

Então, criei essa abordagem, que não acho a melhor, mas atendi minhas expectativas:

Use TransactionScope in the tests methods to avoid changes in the database.

Para isso é necessário:

1- Instale o EntityFramework no projeto de teste. 2- Coloque a string de conexão no arquivo app.config do Test Project. 3- Faça referência à dll System.Transactions no projeto de teste.

O efeito colateral exclusivo é que a semente de identidade será incrementada ao tentar inserir, mesmo quando a transação for abortada. Mas como os testes são feitos em um banco de dados de desenvolvimento, isso não deve ser problema.

Código de amostra:

[TestClass]
public class NameValueTest
{
    [TestMethod]
    public void Edit()
    {
        NameValueController controller = new NameValueController();

        using(var ts = new TransactionScope()) {
            Assert.IsNotNull(controller.Edit(new Models.NameValue()
            {
                NameValueId = 1,
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }

    [TestMethod]
    public void Create()
    {
        NameValueController controller = new NameValueController();

        using (var ts = new TransactionScope())
        {
            Assert.IsNotNull(controller.Create(new Models.NameValue()
            {
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }
}

1
Na verdade, eu gosto muito desta solução. Super simples de implementar e cenários de teste mais realistas. Obrigado!
slopapa

1
com o EF 6, você usaria DbContext.Database.BeginTransaction, não usaria?
SwissCoder 12/09

5

Em resumo, eu diria que não, o suco não vale a pena testar um método de serviço com uma única linha que recupera dados do modelo. Na minha experiência, as pessoas que são novas no TDD querem testar absolutamente tudo. A velha castanha de abstrair uma fachada para uma estrutura de terceiros apenas para que você possa criar uma simulação dessa API de estruturas com a qual você bastardiza / estende para poder injetar dados fictícios tem pouco valor em minha mente. Todo mundo tem uma visão diferente de quanto é o melhor teste de unidade. Atualmente, sou mais pragmático e me pergunto se meu teste está realmente agregando valor ao produto final e a que custo.


1
Sim ao pragmatismo. Ainda argumento que a qualidade dos seus testes de unidade é inferior à qualidade do seu código original. É claro que há valor no uso do TDD para melhorar sua prática de codificação e também para melhorar a capacidade de manutenção, mas o TDD pode ter um valor decrescente. Executamos todos os nossos testes no banco de dados, porque isso nos dá confiança de que nosso uso do EF e das próprias tabelas é consistente. Os testes levam mais tempo para serem executados, mas são mais confiáveis.
Savage

3

Quero compartilhar uma abordagem comentada e discutida brevemente, mas mostrar um exemplo real que estou usando atualmente para ajudar a testar os serviços baseados em EF.

Primeiro, eu adoraria usar o provedor de memória do EF Core, mas isso é sobre o EF 6. Além disso, para outros sistemas de armazenamento como o RavenDB, eu também seria um proponente dos testes por meio do provedor de banco de dados na memória. Novamente - isso é especificamente para ajudar a testar o código baseado em EF sem muita cerimônia .

Aqui estão os objetivos que eu tinha ao criar um padrão:

  • Deve ser simples para outros desenvolvedores da equipe entenderem
  • Ele deve isolar o código EF no nível mais baixo possível
  • Não deve envolver a criação de interfaces estranhas de múltiplas responsabilidades (como um padrão de repositório "genérico" ou "típico")
  • Deve ser fácil de configurar e configurar em um teste de unidade

Concordo com as afirmações anteriores de que a EF ainda é um detalhe de implementação e não há problema em sentir que você precisa abstraí-lo para fazer um teste de unidade "puro". Também concordo que, idealmente, eu gostaria de garantir que o próprio código EF funcione - mas isso envolve um banco de dados sandbox, um provedor de memória etc. Minha abordagem resolve os dois problemas - você pode testar com segurança o código dependente de EF e criar testes de integração para testar seu código EF especificamente.

A maneira como consegui isso foi simplesmente encapsular o código EF em classes dedicadas de consulta e comando. A ideia é simples: basta agrupar qualquer código EF em uma classe e depender de uma interface nas classes que o teriam usado originalmente. O principal problema que precisei resolver foi evitar adicionar inúmeras dependências às classes e configurar muito código nos meus testes.

É aqui que entra uma biblioteca útil e simples: Mediatr . Ele permite mensagens simples no processo e faz isso dissociando "solicitações" dos manipuladores que implementam o código. Isso tem um benefício adicional de dissociar o "quê" do "como". Por exemplo, ao encapsular o código EF em pequenos blocos, você pode substituir as implementações por outro provedor ou mecanismo totalmente diferente, porque tudo o que você está fazendo é enviar uma solicitação para executar uma ação.

Utilizando injeção de dependência (com ou sem uma estrutura - sua preferência), podemos facilmente zombar do mediador e controlar os mecanismos de solicitação / resposta para ativar o código EF de teste de unidade.

Primeiro, digamos que temos um serviço com lógica de negócios que precisamos testar:

public class FeatureService {

  private readonly IMediator _mediator;

  public FeatureService(IMediator mediator) {
    _mediator = mediator;
  }

  public async Task ComplexBusinessLogic() {
    // retrieve relevant objects

    var results = await _mediator.Send(new GetRelevantDbObjectsQuery());
    // normally, this would have looked like...
    // var results = _myDbContext.DbObjects.Where(x => foo).ToList();

    // perform business logic
    // ...    
  }
}

Você começa a ver os benefícios dessa abordagem? Você não é apenas explicitamente encapsula todo o código relacionado ao EF em classes descritivas, como também permite extensibilidade, removendo a preocupação de implementação de "como" essa solicitação é manipulada - essa classe não se importa se os objetos relevantes vêm do EF, MongoDB, ou um arquivo de texto.

Agora, para a solicitação e o manipulador, via MediatR:

public class GetRelevantDbObjectsQuery : IRequest<DbObject[]> {
  // no input needed for this particular request,
  // but you would simply add plain properties here if needed
}

public class GetRelevantDbObjectsEFQueryHandler : IRequestHandler<GetRelevantDbObjectsQuery, DbObject[]> {
  private readonly IDbContext _db;

  public GetRelevantDbObjectsEFQueryHandler(IDbContext db) {
    _db = db;
  }

  public DbObject[] Handle(GetRelevantDbObjectsQuery message) {
    return _db.DbObjects.Where(foo => bar).ToList();
  }
}

Como você pode ver, a abstração é simples e encapsulada. Também é absolutamente testável porque, em um teste de integração, você pode testar essa classe individualmente - não há preocupações comerciais misturadas aqui.

Então, como é um teste de unidade do nosso serviço de recursos? É muito simples. Nesse caso, estou usando o Moq para fazer zombaria (use o que te faz feliz):

[TestClass]
public class FeatureServiceTests {

  // mock of Mediator to handle request/responses
  private Mock<IMediator> _mediator;

  // subject under test
  private FeatureService _sut;

  [TestInitialize]
  public void Setup() {

    // set up Mediator mock
    _mediator = new Mock<IMediator>(MockBehavior.Strict);

    // inject mock as dependency
    _sut = new FeatureService(_mediator.Object);
  }

  [TestCleanup]
  public void Teardown() {

    // ensure we have called or expected all calls to Mediator
    _mediator.VerifyAll();
  }

  [TestMethod]
  public void ComplexBusinessLogic_Does_What_I_Expect() {
    var dbObjects = new List<DbObject>() {
      // set up any test objects
      new DbObject() { }
    };

    // arrange

    // setup Mediator to return our fake objects when it receives a message to perform our query
    // in practice, I find it better to create an extension method that encapsulates this setup here
    _mediator.Setup(x => x.Send(It.IsAny<GetRelevantDbObjectsQuery>(), default(CancellationToken)).ReturnsAsync(dbObjects.ToArray()).Callback(
    (GetRelevantDbObjectsQuery message, CancellationToken token) => {
       // using Moq Callback functionality, you can make assertions
       // on expected request being passed in
       Assert.IsNotNull(message);
    });

    // act
    _sut.ComplexBusinessLogic();

    // assertions
  }

}

Você pode ver tudo o que precisamos é de uma configuração única e nem precisamos configurar nada extra - é um teste de unidade muito simples. Sejamos claros: isso é totalmente possível sem algo como o Mediatr (você simplesmente implementaria uma interface e a zombaria para testes, por exemplo IGetRelevantDbObjectsQuery), mas na prática para uma grande base de código com muitos recursos e consultas / comandos, eu amo o encapsulamento e suporte de DI inato que o Mediatr oferece.

Se você está se perguntando como eu organizo essas aulas, é bem simples:

- MyProject
  - Features
    - MyFeature
      - Queries
      - Commands
      - Services
      - DependencyConfig.cs (Ninject feature modules)

A organização por fatias de recursos não vem ao caso, mas isso mantém todos os códigos relevantes / dependentes juntos e facilmente detectáveis. Mais importante, eu separo as consultas versus os comandos - seguindo o princípio de separação de comandos / consultas .

Isso atende a todos os meus critérios: é de cerimônia baixa, é fácil de entender e há benefícios ocultos extras. Por exemplo, como você lida com salvar alterações? Agora você pode simplificar seu contexto de banco de dados usando uma interface de função (IUnitOfWork.SaveChangesAsync()) e chamadas simuladas para a interface de função única ou você pode encapsular a confirmação / reversão dentro de seus RequestHandlers - no entanto, você prefere fazer isso com você, desde que seja sustentável. Por exemplo, fiquei tentado a criar uma única solicitação / manipulador genérico onde você passaria um objeto EF e o salvaria / atualizaria / removeria - mas é necessário perguntar qual é a sua intenção e lembrar que, se você quiser Para trocar o manipulador por outro provedor / implementação de armazenamento, você provavelmente deve criar comandos / consultas explícitos que representam o que você pretende fazer. Na maioria das vezes, um único serviço ou recurso precisará de algo específico - não crie itens genéricos antes que você precise.

Obviamente, existem advertências para esse padrão - você pode ir longe demais com um simples mecanismo de publicação / publicação. Limitei minha implementação a apenas abstrair códigos relacionados à EF, mas desenvolvedores aventureiros poderiam começar a usar o MediatR para exagerar e colocar mensagens em itens - algo que boas práticas de revisão de código e revisões por pares devem capturar. Esse é um problema de processo, não do MediatR, portanto, fique ciente de como você está usando esse padrão.

Você queria um exemplo concreto de como as pessoas estão testando / zombando da EF e esta é uma abordagem que está funcionando com sucesso para nós em nosso projeto - e a equipe está super feliz com a facilidade de adoção. Eu espero que isso ajude! Como em todas as coisas da programação, existem várias abordagens e tudo depende do que você deseja alcançar. Valorizo ​​a simplicidade, a facilidade de uso, a capacidade de manutenção e a descoberta - e essa solução atende a todas essas demandas.


Obrigado pela resposta, é uma ótima descrição do QueryObject Pattern usando um mediador e algo que estou começando a incluir nos meus projetos também. Talvez eu precise atualizar a pergunta, mas eu não estou mais testando a unidade EF, as abstrações estão muito vazias (o SqlLite pode estar ok), então apenas teste de integração minhas coisas que consultam as regras de negócios do banco de dados e do teste de unidade e outra lógica.
Modika

3

Existe um esforço que é um provedor de banco de dados da estrutura da entidade na memória. Na verdade, eu ainda não tentei ... Haa acabou de descobrir que isso foi mencionado na pergunta!

Como alternativa, você pode alternar para EntityFrameworkCore, que possui um provedor de banco de dados de memória embutido.

https://blog.goyello.com/2016/07/14/save-time-mocking-use-your-real-entity-framework-dbcontext-in-unit-tests/

https://github.com/tamasflamich/effort

Eu usei uma fábrica para obter um contexto, para que eu possa criar o contexto próximo ao seu uso. Isso parece funcionar localmente no visual studio, mas não no servidor de criação do TeamCity, ainda não sei por que.

return new MyContext(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");

Olá, Andrew, o problema nunca foi entender o contexto. Você pode criar o contexto que estávamos fazendo, abstraindo o contexto e construindo-o pela fábrica. O maior problema foi a consistência do que estava na memória versus o que o Linq4Entities faz; eles não são os mesmos, o que pode levar a testes enganosos. Atualmente, apenas integramos as coisas do banco de dados de teste, talvez não seja o melhor processo para todos.
Modika

Esse auxiliar do Moq funciona ( codeproject.com/Tips/1045590/… ) se você tiver um contexto para zombar. Se o backup do contexto simulado com uma lista não se comportar como um contexto suportado por um banco de dados sql.
precisa

2

Gosto de separar meus filtros de outras partes do código e testá-los conforme descrito em meu blog aqui http://coding.grax.com/2013/08/testing-custom-linq-filter-operators.html

Dito isto, a lógica do filtro que está sendo testada não é idêntica à lógica do filtro executada quando o programa é executado devido à conversão entre a expressão LINQ e a linguagem de consulta subjacente, como o T-SQL. Ainda assim, isso me permite validar a lógica do filtro. Não me preocupo muito com as traduções que acontecem e coisas como diferenciação entre maiúsculas e minúsculas até testar a integração entre as camadas.


0

É importante testar o que você espera que a estrutura da entidade faça (ou seja, validar suas expectativas). Uma maneira de fazer isso que usei com êxito é usar o moq, como mostrado neste exemplo (por muito tempo para copiar nesta resposta):

https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking

No entanto, tenha cuidado ... Não é garantido que um contexto SQL retorne as coisas em uma ordem específica, a menos que você tenha um "OrderBy" apropriado em sua consulta linq, portanto, é possível escrever as coisas que passam quando você testa usando uma lista na memória ( linq para entidades), mas falha em seu ambiente uat / live quando (linq para sql) é usado.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.