Práticas recomendadas para métodos de teste de unidade que usam muito o cache?

17

Eu tenho vários métodos de lógica de negócios que armazenam e recuperam (com filtragem) objetos e listas de objetos do cache.

Considerar

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

TObject FetchById(guid id) { ... }

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

Fetch..e Filter..chamaria o AllFromCacheque preencheria o cache e retornaria se não estiver lá e retornaria apenas se estiver.

Eu geralmente evito testar essas unidades. Quais são as práticas recomendadas para o teste de unidade nesse tipo de estrutura?

Eu considerei preencher o cache no TestInitialize e removê-lo no TestCleanup, mas isso não parece certo para mim (embora possa ser).

NikolaiDante
fonte

Respostas:

18

Se você quiser testes de unidade verdadeiros, precisará zombar do cache: escreva um objeto falso que implemente a mesma interface que o cache, mas em vez de ser um cache, ele acompanha as chamadas recebidas e sempre retorna o que é real. o cache deve retornar de acordo com o caso de teste.

É claro que o próprio cache também precisa de testes de unidade, dos quais você precisa zombar de tudo o que depende, e assim por diante.

O que você descreve, usando o objeto de cache real, mas inicializando-o para um estado conhecido e limpando após o teste, é mais como um teste de integração, porque você está testando várias unidades em conjunto.

tdammers
fonte
+1 é, sem dúvida, a melhor abordagem. Teste de unidade para verificar a lógica e teste de integração para realmente verificar se o cache funciona conforme o esperado.
Tom Squires
10

O Princípio da Responsabilidade Única é seu melhor amigo aqui.

Primeiro, mova AllFromCache () para uma classe de repositório e chame-a GetAll (). A recuperação do cache é um detalhe de implementação do repositório e não deve ser conhecido pelo código de chamada.

Isso torna fácil e fácil testar sua classe de filtragem. Não se importa mais de onde você está conseguindo.

Segundo, agrupe a classe que obtém os dados do banco de dados (ou qualquer outro local) em um wrapper de cache.

AOP é uma boa técnica para isso. É uma das poucas coisas em que é muito bom.

Usando ferramentas como o PostSharp , você pode configurá-lo para que qualquer método marcado com um atributo escolhido seja armazenado em cache. No entanto, se essa é a única coisa que você está armazenando em cache, não precisa ir tão longe quanto ter uma estrutura de AOP. Apenas tenha um Repositório e um Caching Wrapper que usem a mesma interface e os injetem na classe de chamada.

por exemplo.

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
    }
}

Veja como você removeu o conhecimento de implementação do repositório do ProductManager? Veja também como você aderiu ao Princípio de Responsabilidade Única ao ter uma classe que lida com extração de dados, uma classe que lida com recuperação de dados e uma classe que lida com cache.

Agora você pode instanciar o ProductManager com um desses repositórios e obter o cache ... ou não. Isso é incrivelmente útil mais tarde, quando você recebe um bug confuso que suspeita ser resultado do cache.

productManager = new ProductManager(
                         new SqlProductRepository()
                         );

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

(Se você estiver usando um contêiner de COI, melhor ainda. Deve ser óbvio como se adaptar.)

E, em seus testes do ProductManager

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

Não há necessidade de testar o cache.

Agora a pergunta é: devo testar esse CachedProductRepository? Eu sugiro que não. O cache é bastante indeterminado. A estrutura faz coisas que estão fora de seu controle. Por exemplo, basta remover coisas dele quando estiver muito cheia, por exemplo. Você terminará com testes que falharão uma vez na lua azul e nunca entenderá realmente o porquê.

E, tendo feito as alterações sugeridas acima, não há realmente muita lógica para testar lá. O teste realmente importante, o método de filtragem, estará lá e completamente abstraído dos detalhes de GetAll (). GetAll () apenas ... recebe tudo. De algum lugar.

pdr
fonte
O que você faz se estiver usando CachedProductRepository no ProductManager, mas quiser usar métodos que estão no SQLProductRepository?
Jonathan
@ Jonathan: "Basta ter um repositório e um wrapper de cache que usam a mesma interface" - se eles tiverem a mesma interface, você poderá usar os mesmos métodos. O código de chamada não precisa saber nada sobre implementação.
PDR
3

Sua abordagem sugerida é o que eu faria. De acordo com sua descrição, o resultado do método deve ser o mesmo, independentemente de o objeto estar presente no cache ou não: você ainda deve obter o mesmo resultado. É fácil testar, configurando o cache de uma maneira específica antes de cada teste. Provavelmente existem alguns casos adicionais, como se o guid é nullou nenhum objeto possui a propriedade solicitada; esses também podem ser testados.

Além disso, você pode considerar que o objeto esteja presente no cache após o retorno do método, independentemente de ele estar no cache em primeiro lugar. Isso é controverso, pois algumas pessoas (inclusive eu) argumentam que você se preocupa com o que recebe da sua interface, não como obtê-lo (ou seja, com o teste de que a interface funciona conforme o esperado, e não com uma implementação específica). Se você considerar importante, terá a oportunidade de testar isso.


fonte
1

Considerei preencher o cache no TestInitialize e removê-lo no TestCleanup, mas isso não parece certo para mim

Na verdade, essa é a única maneira correta de fazer. É para isso que essas duas funções existem: definir as pré-condições e limpar. Se as pré-condições não forem atendidas, seu programa poderá não funcionar.

BЈовић
fonte
0

Eu estava trabalhando em alguns testes que usam cache recentemente. Criei um wrapper em torno da classe que funciona com o cache e, em seguida, tinha afirmações de que esse wrapper estava sendo chamado.

Fiz isso principalmente porque a classe existente que trabalha com cache era estática.

Daniel Hollinrake
fonte
0

Parece que você deseja testar a lógica de armazenamento em cache, mas não a lógica de preenchimento. Então, eu sugiro que você zombe do que não precisa testar - preenchendo.

Seu AllFromCache()método cuida de preencher o cache e isso deve ser delegado para outra coisa, como um fornecedor de valores. Portanto, seu código seria semelhante

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

Agora você pode zombar do fornecedor para o teste, para retornar alguns valores predefinidos. Dessa forma, você pode testar sua filtragem e busca reais e não carregar objetos.

jmruc
fonte