2011-04-13 15 views
7

Estoy usando el patrón de repositorio en una aplicación MVC 3 que estoy desarrollando actualmente. Mi interfaz de repositorio se ve de la siguiente manera:NSubstitute - Prueba para una expresión de linq específica

public interface IRepository<TEntity> where TEntity : IdEntity 
{ 
    void Add(TEntity entity); 
    void Update(TEntity entity); 
    void Remove(TEntity entity); 
    TEntity GetById(int id); 
    IList<TEntity> GetAll(); 
    TEntity FindFirst(Expression<Func<TEntity, bool>> criteria); 
    IList<TEntity> Find(Expression<Func<TEntity, bool>> criteria); 
} 

En muchos casos, cuando los métodos de codificación en mis clases de servicio, estoy usando el FindFirst y encontrar métodos. Como puede ver, ambos toman una expresión linq como entrada. Lo que quiero saber es si existe una forma de que NSubstitute te permita especificar la expresión particular que deseas probar en tu código.

lo tanto, aquí es un ejemplo de un método de servicio que ilustra el uso de uno de los métodos del repositorio que he mencionado:

public IList<InvoiceDTO> GetUnprocessedInvoices() 
    { 
     try 
     { 
      var invoices = _invoiceRepository.Find(i => !i.IsProcessed && i.IsConfirmed); 
      var dtoInvoices = Mapper.Map<IList<Invoice>, IList<InvoiceDTO>>(invoices); 
      return dtoInvoices; 
     } 
     catch (Exception ex) 
     { 
      throw new Exception(string.Format("Failed to get unprocessed invoices: {0}", ex.Message), ex); 
     } 
    } 

Por lo tanto, hay una manera, usando Nsubtitute, que puedo probar la expresión específica de lamda: (i =>! i.IsProcessed & & i.IsConfirmed)?

Cualquier orientación será apreciada.

Respuesta

12

La respuesta muy corta es no, NSubstitute no tiene nada construido para facilitar la prueba de expresiones específicas.

La respuesta mucho más larga es que hay algunas opciones que puede probar, y la mayoría de ellas implican evitar el uso directo de LINQ en la clase bajo prueba. No estoy seguro si alguna de estas son buenas ideas ya que no conozco el contexto completo, pero espero que haya alguna información aquí que pueda usar. En los siguientes ejemplos, eliminé el paso de Mapper para hacer que las muestras del código sean un poco más pequeñas.

La primera opción es hacerlo para que pueda comprobar que la expresión es la misma referencia que está esperando, lo que significa que ya no puede crearla directamente en el código bajo prueba. Por ejemplo:

//Class under test uses: 
_invoiceRepository.Find(Queries.UnprocessedConfirmedOrders) 

[Test] 
public void TestUnprocessedInvoices() 
{ 
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>(); 
    _invoiceRepository.Find(Queries.UnprocessedConfirmedOrders).Returns(expectedResults); 
    Assert.That(_sut.GetUnprocessedInvoices(), Is.SameAs(expectedResults)); 
} 

He vertido la expresión de una clase de consultas estáticas, pero se puede usar una fábrica para encapsular mejor. Como tiene una referencia a la expresión real utilizada, puede establecer valores de retorno y verificar que las llamadas se recibieron de forma normal. También puedes probar la expresión de forma aislada.

La segunda opción lleva esto un poco más lejos mediante el uso de un patrón de especificación. Digamos que agregar el miembro siguiente a la interfaz IRepository e introducir un ISpecification:

public interface IRepository<TEntity> where TEntity : IdEntity 
{ 
    /* ...snip... */ 
    IList<TEntity> Find(ISpecification<TEntity> query); 
} 

public interface ISpecification<T> { bool Matches(T item); } 

entonces usted puede probar de esta manera:

//Class under test now uses: 
_invoiceRepository.Find(new UnprocessedConfirmedOrdersQuery()); 

[Test] 
public void TestUnprocessedInvoicesUsingSpecification() 
{ 
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>(); 
    _invoiceRepository.Find(Arg.Any<UnprocessedConfirmedOrdersQuery>()).Returns(expectedResults); 
    Assert.That(_sut.GetUnprocessedInvoices(), Is.SameAs(expectedResults)); 
} 

Una vez más, se puede probar esta consulta en forma aislada para asegurarse de que hace lo que piensas

La tercera opción es captar el argumento utilizado y probarlo directamente. Esto es un poco desordenado, pero funciona:

[Test] 
public void TestUnprocessedInvoicesByCatchingExpression() 
{ 
    Expression<Func<InvoiceDTO, bool>> queryUsed = null; 
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>(); 
    _invoiceRepository 
     .Find(i => true) 
     .ReturnsForAnyArgs(x => 
     { 
      queryUsed = (Expression<Func<InvoiceDTO, bool>>)x[0]; 
      return expectedResults; 
     }); 

    Assert.That(_sut.GetUnprocessedInvoices(), Is.SameAs(expectedResults)); 
    AssertQueryPassesFor(queryUsed, new InvoiceDTO { IsProcessed = false, IsConfirmed = true }); 
    AssertQueryFailsFor(queryUsed, new InvoiceDTO { IsProcessed = true, IsConfirmed = true }); 
} 

(Esto se espera que sea cada vez un poco más fácil en futuras versiones NSubstitute)

cuarta opción sería encontrar/prestado/escritura/robar algo de código que se puede comparar árboles de expresión, y usan Arg.Is (...) de NSubstitute que toma un predicado para comparar los árboles de expresión allí.

La quinta opción es no probarla en ese grado, y solo la prueba de integración usando un FacturaRepositorio real. En lugar de preocuparse por la mecánica de lo que está sucediendo, intente verificar el comportamiento real que necesita.

Mi consejo general sería ver exactamente lo que necesita probar y cómo puede escribir mejor y con más facilidad esas pruebas. Recuerde que tanto la expresión como el hecho de que se transmite deben ser probados de alguna manera, y la prueba no necesita ser una prueba unitaria. También puede valer la pena considerar si la interfaz actual de IRepository te está haciendo la vida más fácil. Podría intentar escribir las pruebas que tendría , como, y luego ver qué diseño puede extraer para respaldar esa capacidad de prueba.

Espero que esto ayude.

+0

He estado luchando con algo similar - Me encanta ser capaz de probar todo mi LINQ, pero se me hace difícil cuando se introduce cosas como SqlFunctions que no se pueden ejecutar. http://stackoverflow.com/questions/21495980/testing-sqlfunctions-in-linq-statements - Creo que voy a ir para las pruebas de integración en su lugar – Neil

3

Me encontré con esta pregunta cuando estaba tratando de encontrar la forma de devolver un valor específico utilizando una expresión lambda en NSubstitute. Sin embargo, para mi caso de uso no me importa lo que en realidad se pasa a la consulta linq, y quería compartir cómo devolver valores para las consultas de linq en las interfaces simuladas en NSubstitute.

Así, utilizando el ejemplo de arriba

[Test] 
public void TestUnprocessedInvoices() 
{ 
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>(); 
    _invoiceRepository.Find(Arg.Any<Expression<Func<Invoice, bool>>>()).Returns(expectedResults); 
} 
3

yo era reacio a renunciar a la utilización de Expression<Func<T,bool>> en mi interfaz de repositorio, por lo que, como alternativa a la programación de éste simulacro en particular (ya NSubstitute no apoyó), simplemente creé una clase privada dentro de mi accesorio de prueba que implementó mi interfaz de repositorio y solo el método relacionado con expresiones que usaría la prueba. Pude continuar usando NSubstitute para simular todas las demás dependencias, como de costumbre, pero podría usar este mismo repositorio para varias pruebas diferentes y obtener diferentes resultados de diferentes entradas.

public class SomeFixture 
{ 
    private readonly IRepository<SomeEntity> entityRepository; 
    private readonly IRepository<SomeThing> thingRepository; 

    public SomeFixture() 
    { 
     var entities = new List<SomeEntity> 
     { 
      BuildEntityForThing(1), 
      BuildEntityForThing(1), 
      BuildEntityForThing(1), 
      BuildEntityForThing(2), 
     }; 
     entityRepository = new FakeRepository(entities); 

     thingRepository = Substitute.For<IRepository<SomeThing>>(); 
     thingRepository.GetById(1).Returns(BuildThing(1)); 
     thingRepository.GetById(2).Returns(BuildThing(2)); 
    } 

    public void SomeTest() 
    { 
     var classUnderTest = new SomeClass(thingRepository, entityRepository); 

     Assert.AreEqual(classUnderTest.FetchEntitiesForThing(1).Count, 3); 
    } 

    private void SomeOtherTest() 
    { 
     var classUnderTest = new SomeClass(thingRepository, entityRepository); 

     Assert.AreEqual(classUnderTest.FetchEntitiesForThing(2).Count, 1); 
    } 

    private class FakeRepository : IRepository<SomeEntity> 
    { 
     private readonly List<SomeEntity> items; 

     public FakeRepository(List<SomeEntity> items) 
     { 
      this.items = items; 
     } 

     IList<TEntity> Find(Expression<Func<SomeEntity, bool>> criteria) 
     { 
      // For these purposes, ignore possible inconsistencies 
      // between Linq and SQL when executing expressions 
      return items.Where(criteria.Compile()).ToList(); 
     } 

     // Other unimplemented methods from IRepository ... 
     void Add(SomeEntity entity) 
     { 
      throw new NotImplementedException(); 
     } 
    } 
} 
Cuestiones relacionadas