with unit test net unit-testing nsubstitute

unit testing - unit - NSubstitute-Prueba para una expresión linq específica



nsubstitute xunit (3)

Era reacio a renunciar al uso de Expression<Func<T,bool>> en mi interfaz de repositorio, así que como alternativa a programar este simulacro en particular (ya que NSubstitute no lo soportaba), simplemente creé una clase privada dentro de mi dispositivo 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(); } } }

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:

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, al codificar métodos en mis clases de servicio, estoy usando los métodos FindFirst y Find. 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.

Entonces, aquí hay un ejemplo de un método de servicio que ilustra el uso de uno de los métodos de 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); } }

Entonces, ¿hay alguna manera, usando Nsubtitute, de que pueda probar la expresión específica de lamda: (i =>! I.IsProcessed && i.IsConfirmed)?

Cualquier orientación será apreciada.


La muy breve respuesta 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 verificar 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)); }

Descarté la expresión en una clase estática de Consultas, pero podrías usar una fábrica para encapsularla mejor. Como tiene una referencia a la expresión real utilizada, puede establecer valores de retorno y verificar que las llamadas se recibieron como de costumbre. También puedes probar la expresión de forma aislada.

La segunda opción lleva esto un poco más allá al usar un patrón de especificación. Supongamos que agrega el siguiente miembro a la interfaz de IRepository e introduce una especificación de IS:

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

Luego puedes probarlo así:

//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)); }

De nuevo, puede probar esta consulta aisladamente para asegurarse de que haga lo que piensa.

La tercera opción es capturar el argumento utilizado y probarlo directamente. Esto es un poco sucio 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 }); }

(Es de esperar que este sea un poco más fácil en futuras versiones NSubstitute)

La cuarta opción sería encontrar / tomar prestado / escribir / robar algún código que pueda comparar árboles de expresión, y usar Arg.Is (...) de NSubstitute que tome 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 analizar exactamente lo que necesita probar y cómo puede escribir mejor y con mayor 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 le gustaría tener, luego ver qué diseño puede expulsar para respaldar esa capacidad de prueba.

Espero que esto ayude.


Me encontré con esta pregunta cuando estaba tratando de descubrir cómo devolver un valor específico usando una expresión lambda en NSubstitute. Sin embargo, para mi caso de uso, no me importa lo que realmente 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.

Entonces usando el ejemplo de arriba

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