Teste unitário com consultas definidas em methods de extensão

No meu projeto, estou usando a seguinte abordagem para consultar dados do database:

  1. Use um repository genérico que pode retornar qualquer tipo e não está vinculado a um tipo, ou seja, IRepository.Get vez de IRepository.Get . NHibernates ISession é um exemplo de tal repository.
  2. Use methods de extensão em IQueryable com um T específico para encapsular consultas recorrentes, por exemplo

     public static IQueryable ByInvoiceType(this IQueryable q, InvoiceType invoiceType) { return q.Where(x => x.InvoiceType == invoiceType); } 

O uso seria assim:

 var result = session.Query().ByInvoiceType(InvoiceType.NormalInvoice); 

Agora suponha que eu tenha um método público que eu queira testar que use essa consulta. Eu quero testar os três casos possíveis:

  1. A consulta retorna 0 faturas
  2. A consulta retorna 1 fatura
  3. A consulta retorna várias faturas

Meu problema agora é: o que zombar?

  • Eu não posso zombar ByInvoiceType porque é um método de extensão, ou posso?
  • Eu não posso nem zombar da Query pelo mesmo motivo.

Depois de mais algumas pesquisas e com base nas respostas aqui e nesses links , decidi reformular completamente minha API.

O conceito básico é proibir completamente as consultas personalizadas no código de negócios. Isso resolve dois problemas:

  1. A testabilidade é melhorada
  2. Os problemas descritos na postagem do blog de Mark não podem mais acontecer. A camada de negócios não precisa mais do conhecimento implícito sobre o armazenamento de dados usado para saber quais operações são permitidas no IQueryable e quais não são.

No código comercial, uma consulta agora é assim:

 IEnumerable inv = repository.Query .Invoices.ThatAre .Started() .Unfinished() .And.WithoutError(); // or IEnumerable inv = repository.Query.Invoices.ThatAre.Started(); // or Invoice inv = repository.Query.Invoices.ByInvoiceNumber(invoiceNumber); 

Na prática isso é implementado assim:

Como Vytautas Mackonis sugeriu em sua resposta , eu não estou mais dependendo diretamente da ISession do NHibernate, ao invés disso eu estou agora dependendo de um IRepository .

Essa interface possui uma propriedade chamada Query do tipo IQueries . Para cada entidade que a camada de negócios precisa consultar, existe uma propriedade no IQueries . Cada propriedade possui sua própria interface que define as consultas para a entidade. Cada interface de consulta implementa a interface genérica IQuery , a qual, por sua vez, implementa IEnumerable , levando à syntax semelhante a DSL, muito limpa, vista acima.

Algum código:

 public interface IRepository { IQueries Queries { get; } } public interface IQueries { IInvoiceQuery Invoices { get; } IUserQuery Users { get; } } public interface IQuery : IEnumerable { T Single(); T SingleOrDefault(); T First(); T FirstOrDefault(); } public interface IInvoiceQuery : IQuery { IInvoiceQuery Started(); IInvoiceQuery Unfinished(); IInvoiceQuery WithoutError(); Invoice ByInvoiceNumber(string invoiceNumber); } 

Essa syntax de consulta fluente permite que a camada de negócios combine as consultas fornecidas para aproveitar ao máximo os resources do ORM subjacente para permitir que o filtro do database seja o mais possível.

A implementação para o NHibernate seria algo como isto:

 public class NHibernateInvoiceQuery : IInvoiceQuery { IQueryable _query; public NHibernateInvoiceQuery(ISession session) { _query = session.Query(); } public IInvoiceQuery Started() { _query = _query.Where(x => x.IsStarted); return this; } public IInvoiceQuery WithoutError() { _query = _query.Where(x => !x.HasError); return this; } public Invoice ByInvoiceNumber(string invoiceNumber) { return _query.SingleOrDefault(x => x.InvoiceNumber == invoiceNumber); } public IEnumerator GetEnumerator() { return _query.GetEnumerator(); } // ... } 

Na minha implementação real, extraí a maior parte do código de infraestrutura em uma class base, para que seja muito fácil criar um novo object de consulta para uma nova entidade. Adicionar uma nova consulta a uma entidade existente também é muito simples.

O bom disso é que a camada de negócios está completamente livre da lógica de consulta e, portanto, o armazenamento de dados pode ser alternado facilmente. Ou pode-se implementar uma das consultas usando a API de critérios ou obter os dados de outra fonte de dados. A camada de negócios estaria alheia a esses detalhes.

ISession seria a coisa que você deveria zombar neste caso. Mas o verdadeiro problema é que você não deve ter isso como uma dependência direta. Ele mata testabilidade da mesma maneira que ter SqlConnection na class – você teria que “simular” o próprio database.

Enrole ISession com alguma interface e tudo fica fácil:

 public interface IDataStore { IQueryable Query(); } public class NHibernateDataStore : IDataStore { private readonly ISession _session; public NHibernateDataStore(ISession session) { _session = session; } public IQueryable Query() { return _session.Query(); } } 

Então você poderia zombar de IDataStore, retornando uma lista simples.

Para isolar o teste apenas para o método de extensão, eu não iria zombar de nada. Crie uma lista de Faturas em uma Lista () com valores predefinidos para cada um dos três testes e invoque o método de extensão no fakeInvoiceList.AsQueryable () e teste os resultados.

Crie entidades na memory em uma lista falsa.

 var testList = new List(); testList.Add(new Invoice {...}); var result = testList().AsQueryable().ByInvoiceType(enumValue).ToList(); // test results 

dependendo da sua implementação do Repository.Get, você poderia simular a isenção do NHibernate.

Se for adequado às suas condições, você pode seqüestrar genéricos para sobrecarregar os methods de extensão. Vamos dar o seguinte exemplo:

 interface ISession { // session members } class FakeSession : ISession { public void Query() { Console.WriteLine("fake implementation"); } } static class ISessionExtensions { public static void Query(this ISession test) { Console.WriteLine("real implementation"); } } static void Stub1(ISession test) { test.Query(); // calls the real method } static void Stub2(TTest test) where TTest : FakeSession { test.Query(); // calls the fake method } 

Eu vejo seu IRepository como um “UnitOfWork” e seu IQueries como um “Repositório” (Talvez um repository fluente!). Então, basta seguir o padrão UnitOfWork and Repository. Esta é uma boa prática para a EF, mas você pode facilmente implementar a sua própria.

Eu sei que isso tem sido respondido há muito tempo e eu gosto da resposta aceita, mas para qualquer um que se deparasse com um problema semelhante, eu recomendaria investigar a implementação do padrão de especificação como descrito aqui .

Temos feito isso em nosso projeto atual há mais de um ano e todo mundo gosta disso. Na maioria dos casos, seus repositorys só precisam de um método como

 IEnumerable GetBySpecification(ISpecification spec) 

E isso é muito fácil de zombar.

Editar:

A chave para usar o padrão com um OR-Mapper como o NHibernate é ter suas especificações expondo uma tree de expressão, que o provedor Linq do ORM pode analisar. Por favor, siga o link para o artigo que eu mencionei acima para mais detalhes.

 public interface ISpecification { Expression> SpecExpression { get; } bool IsSatisfiedBy(T obj); } 

A resposta é (IMO): você deve simular a Query() .

A ressalva é: eu digo isso em total ignorância de como Query é definida aqui – eu nem conhecia o NHibernate, e se ele é definido como virtual.

Mas isso provavelmente não importa! Basicamente, o que eu faria seria:

-Mock Query para retornar uma simulação IQueryable. (Se você não pode imitar a Query porque ela não é virtual, crie sua própria ISession de interface, que expõe uma consulta simulada, e assim por diante.) -O IQueryable falso realmente não analisa a consulta passada, apenas retorna alguns resultados predeterminados que você especifica quando cria a simulação.

Todos juntos isso basicamente permite que você escaneie o seu método de extensão sempre que quiser.

Para saber mais sobre a ideia geral de fazer consultas de método de extensão e uma implementação simulada de IQueryable simples, veja aqui:

http://blogs.msdn.com/b/tilovell/archive/2014/09/12/how-to-make-your-ef-queries-testable.aspx