¿Mejores prácticas para los métodos de prueba de unidad que usan mucho el caché?

17

Tengo varios métodos de lógica de negocios que almacenan y recuperan (con filtrado) objetos y listas de objetos del caché.

Considerar

IList<TObject> AllFromCache() { ... }

TObject FetchById(guid id) { ... }

IList<TObject> FilterByPropertry(int property) { ... }

Fetch..y Filter..llamaría a AllFromCachecuál llenaría el caché y regresaría si no estaba allí y solo regresaría si lo está.

Generalmente evito que la unidad los pruebe. ¿Cuáles son las mejores prácticas para las pruebas unitarias contra este tipo de estructura?

Pensé en llenar el caché en TestInitialize y eliminarlo en TestCleanup, pero eso no me parece correcto (aunque bien podría ser).

NikolaiDante
fuente

Respuestas:

18

Si desea pruebas unitarias verdaderas, debe simular el caché: escriba un objeto simulado que implemente la misma interfaz que el caché, pero en lugar de ser un caché, realiza un seguimiento de las llamadas que recibe y siempre devuelve lo real el caché debería regresar de acuerdo con el caso de prueba.

Por supuesto, el caché en sí también necesita pruebas unitarias, para lo cual debe burlarse de todo lo que depende, y así sucesivamente.

Lo que describe, usando el objeto de caché real pero inicializándolo a un estado conocido y limpiando después de la prueba, es más como una prueba de integración, porque está probando varias unidades en concierto.

tdammers
fuente
+1 este es definitivamente el mejor enfoque. Prueba de unidad para verificar la lógica y luego prueba de integración para verificar que la memoria caché funcione como espera.
Tom Squires
10

El principio de responsabilidad única es tu mejor amigo aquí.

En primer lugar, mueva AllFromCache () a una clase de repositorio y llámelo GetAll (). Lo que recupera de la memoria caché es un detalle de implementación del repositorio y no debe ser conocido por el código de llamada.

Esto hace que probar su clase de filtrado sea agradable y fácil. Ya no le importa de dónde lo obtengas.

En segundo lugar, ajuste la clase que obtiene los datos de la base de datos (o donde sea) en un contenedor de almacenamiento en caché.

AOP es una buena técnica para esto. Es una de las pocas cosas en las que es muy bueno.

Usando herramientas como PostSharp , puede configurarlo para que cualquier método marcado con un atributo elegido se almacene en caché. Sin embargo, si esto es lo único que está almacenando en caché, no necesita ir tan lejos como tener un marco de AOP. Solo tenga un Repositorio y un Contenedor de almacenamiento en caché que use la misma interfaz e inyecte eso en la clase de llamada.

p.ej.

public class ProductManager
{
    private IProductRepository ProductRepository { get; set; }

    public ProductManager
    {
        ProductRepository = productRepository;
    }

    Product FetchById(guid id) { ... }

    IList<Product> FilterByPropertry(int property) { ... }
}

public interface IProductRepository
{
    IList<Product> GetAll();
}

public class SqlProductRepository : IProductRepository
{
    public IList<Product> GetAll()
    {
        // DB Connection, fetch
    }
}

public class CachedProductRepository : IProductRepository
{
    private IProductRepository ProductRepository { get; set; }

    public CachedProductRepository (IProductRepository productRepository)
    {
        ProductRepository = productRepository;
    }

    public IList<Product> GetAll()
    {
        // Check cache, if exists then return, 
        // if not then call GetAll() on inner repository
    }
}

¿Ves cómo has eliminado el conocimiento de implementación del repositorio del ProductManager? Vea también cómo se ha adherido al Principio de responsabilidad única al tener una clase que maneja la extracción de datos, una clase que maneja la recuperación de datos y una clase que maneja el almacenamiento en caché.

Ahora puede crear una instancia del ProductManager con cualquiera de esos Repositorios y obtener el almacenamiento en caché ... o no. Esto es increíblemente útil más tarde cuando obtienes un error confuso que sospechas que es el resultado del caché.

productManager = new ProductManager(
                         new SqlProductRepository()
                         );

productManager = new ProductManager(
                         new CachedProductRepository(new SqlProductRepository())
                         );

(Si está utilizando un contenedor de COI, aún mejor. Debería ser obvio cómo adaptarse).

Y, en sus pruebas de ProductManager

IProductRepository repo = MockRepository.GenerateStrictMock<IProductRepository>();

No es necesario probar el caché en absoluto.

Ahora la pregunta es: ¿Debo probar ese CachedProductRepository? Sugiero que no. El caché es bastante indeterminado. El marco hace cosas que están fuera de su control. Por ejemplo, simplemente eliminando cosas cuando se llena demasiado, por ejemplo. Vas a terminar con pruebas que fallan una vez en una luna azul y nunca entenderás realmente por qué.

Y, habiendo hecho los cambios que he sugerido anteriormente, realmente no hay tanta lógica para probar allí. La prueba realmente importante, el método de filtrado, estará allí y completamente abstraída de los detalles de GetAll (). GetAll () solo ... obtiene todo. Desde algún lugar.

pdr
fuente
¿Qué haces si estás usando CachedProductRepository en ProductManager pero quieres usar métodos que están en SQLProductRepository?
Jonathan
@ Jonathan: "Solo tenga un repositorio y un contenedor de almacenamiento en caché que utilicen la misma interfaz". Si tienen la misma interfaz, puede utilizar los mismos métodos. El código de llamada no necesita saber nada sobre la implementación.
pdr
3

Su enfoque sugerido es lo que haría. Dada su descripción, el resultado del método debería ser el mismo si el objeto está presente en la memoria caché o no: aún debe obtener el mismo resultado. Eso es fácil de probar configurando el caché de una manera particular antes de cada prueba. Probablemente hay algunos casos adicionales, como si el guid es nullo ningún objeto tiene la propiedad solicitada; esos pueden ser probados también.

Además, se puede considerar que se espera que el objeto esté presente en la memoria caché después de que regrese su método, independientemente de si estaba en la memoria caché en primer lugar. Esto es polémico, ya que algunas personas (incluido yo mismo) argumentan que le importa lo que obtiene de su interfaz, no cómo lo obtiene (es decir, su prueba de que la interfaz funciona como se esperaba, no de que tenga una implementación específica). Si lo considera importante, tiene la oportunidad de probarlo.


fuente
1

Pensé en llenar el caché en TestInitialize y eliminarlo en TestCleanup, pero eso no me parece correcto

En realidad, esa es la única forma correcta de hacerlo. Para eso están esas dos funciones: establecer las condiciones previas y limpiar. Si no se cumplen las condiciones previas, su programa podría no funcionar.

BЈовић
fuente
0

Estaba trabajando en algunas pruebas que usan el almacenamiento en caché recientemente. Creé un contenedor alrededor de la clase que funciona con el caché, y luego tuve afirmaciones de que se estaba llamando a este contenedor.

Lo hice principalmente porque la clase existente que funciona con caché era estática.

Daniel Hollinrake
fuente
0

Parece que quiere probar la lógica de almacenamiento en caché, pero no la lógica de relleno. Por lo tanto, te sugiero que te burles de lo que no necesitas probar: llenar.

Su AllFromCache()método se ocupa de llenar el caché, y eso debería delegarse en otra cosa, como un proveedor de valores. Entonces su código se vería así

private Supplier<TObject> supplier;

IList<TObject> AllFromCache() {
    if (!cacheInitialized) {
        //whatever logic needed to fill the cache
        cache.putAll(supplier.getValues());
        cacheInitialized = true;
    }

    return  cache.getAll();
}

Ahora puede burlarse del proveedor para la prueba, para devolver algunos valores predefinidos. De esa manera, puede probar su filtrado y recuperación reales, y no cargar objetos.

jmruc
fuente