Como lidar com classes de utilitário estático ao projetar para testabilidade


63

Estamos tentando projetar nosso sistema para ser testável e, na maioria das partes, desenvolvido usando TDD. Atualmente, estamos tentando resolver o seguinte problema:

Em vários lugares, é necessário usar métodos auxiliares estáticos como ImageIO e URLEncoder (API Java padrão) e várias outras bibliotecas que consistem principalmente em métodos estáticos (como as bibliotecas Apache Commons). Mas é extremamente difícil testar os métodos que usam essas classes auxiliares estáticas.

Eu tenho várias idéias para resolver esse problema:

  1. Use uma estrutura simulada que pode simular classes estáticas (como o PowerMock). Esta pode ser a solução mais simples, mas, de alguma forma, parece desistir.
  2. Crie classes de invólucro instáveis ​​em torno de todos esses utilitários estáticos para que eles possam ser injetados nas classes que os utilizam. Parece uma solução relativamente limpa, mas receio que acabemos criando uma enorme quantidade dessas classes de wrapper.
  3. Extraia todas as chamadas para essas classes auxiliares estáticas em uma função que possa ser substituída e teste uma subclasse da classe que realmente quero testar.

Mas continuo pensando que isso só deve ser um problema que muitas pessoas precisam enfrentar ao fazer TDD - portanto, já deve haver soluções para esse problema.

Qual é a melhor estratégia para manter as classes que usam esses auxiliares estáticos testáveis?


Não sei ao certo o que você quer dizer com "fontes credíveis e / ou oficiais", mas concordo com o que o @berecursive escreveu em sua resposta. O PowerMock existe por um motivo e não deve parecer "desistir", especialmente se você não quiser escrever classes de wrapper. Métodos finais e estáticos são um problema quando se trata de testes de unidade (e TDD). Pessoalmente? Eu uso o método 2 que você descreveu.
Deco

"fontes credíveis e / ou oficiais" é apenas uma das opções que você pode selecionar ao iniciar um prêmio para uma pergunta. O que eu realmente quero dizer: experiências ou referências a artigos escritos por especialistas em TDD. Ou qualquer tipo de experiência por alguém que tem enfrentado o mesmo problema ...

Respostas:


34

(Receio que não haja fontes "oficiais" aqui - não é como se houvesse uma especificação de como testar bem. Apenas minhas opiniões, que espero que sejam úteis.)

Quando esses métodos estáticos representam dependências genuínas , crie wrappers. Então, para coisas como:

  • ImageIO
  • Clientes HTTP (ou qualquer outra coisa relacionada à rede)
  • O sistema de arquivos
  • Obtendo a hora atual (meu exemplo favorito de onde a injeção de dependência ajuda)

... faz sentido criar uma interface.

Mas muitos dos métodos no Apache Commons provavelmente não devem ser ridicularizados / falsificados. Por exemplo, use um método para juntar uma lista de cadeias, adicionando uma vírgula entre elas. Não há motivo para zombar deles - apenas deixe que a chamada estática faça seu trabalho normal. Você não quer ou precisa substituir o comportamento normal; você não está lidando com um recurso externo ou com algo difícil de trabalhar, são apenas dados. O resultado é previsível e você nunca desejaria que fosse outra coisa senão o que isso lhe dará de qualquer maneira.

Eu suspeito que, ao remover todas as chamadas estáticas, que realmente são métodos de conveniência com resultados previsíveis "puros" (como base64 ou codificação de URL), em vez de pontos de entrada em uma grande confusão de dependências lógicas (como HTTP), você descobrirá que é totalmente prático fazer a coisa certa com as dependências genuínas.


20

Esta é definitivamente uma pergunta / resposta opinativa, mas para o que vale a pena, pensei em gastar meus dois centavos. Em termos do estilo TDD, o método 2 é definitivamente a abordagem que segue a letra. O argumento para o método 2 é que, se você quiser substituir a implementação de uma dessas classes - digamos, uma ImageIObiblioteca equivalente -, poderá fazê-lo, mantendo a confiança nas classes que utilizam esse código.

No entanto, como você mencionou, se você usar muitos métodos estáticos, acabará escrevendo um monte de código de wrapper. Isso pode não ser uma coisa ruim a longo prazo. Em termos de manutenção, certamente existem argumentos para isso. Pessoalmente, eu preferiria essa abordagem.

Dito isto, o PowerMock existe por um motivo. É um problema bastante conhecido que testar quando métodos estáticos estão envolvidos é muito doloroso, daí o início do PowerMock. Eu acho que você precisa avaliar suas opções em termos de quanto trabalho será para agrupar todas as suas classes auxiliares versus o PowerMock. Eu não acho que está 'desistindo' de usar o PowerMock - apenas acho que agrupar as classes permite mais flexibilidade em um projeto grande. Quanto mais contratos públicos (interfaces) você puder, mais limpa será a separação entre intenção e implementação.


11
Um problema adicional sobre o qual não tenho muita certeza: Ao implementar os wrappers, você implementaria todos os métodos da classe embrulhada ou apenas os que são necessários no momento?

3
Ao seguir as idéias ágeis, você deve fazer a coisa mais simples que funciona e evitar fazer o trabalho que não precisa. Portanto, você deve expor apenas os métodos que realmente precisa.
Assaf Pedra

@AssafStone concordou

Cuidado com o PowerMock, toda a manipulação de classe que é necessária para zombar dos métodos vem com muita sobrecarga. Seus testes serão muito mais lentos se você o usar extensivamente.
22812 bcarlso

Você realmente precisa escrever muito wrapper para combinar seus testes / migrações com a adoção de uma biblioteca de DI / IoC?

4

Como referência para todos os que também estão lidando com esse problema e se deparam com essa pergunta, vou descrever como decidimos resolver o problema:

Estamos basicamente seguindo o caminho descrito como # 2 (classes de wrapper para utilitários estáticos). Mas nós os usamos apenas quando é muito complexo para fornecer ao utilitário os dados necessários para produzir a saída desejada (ou seja, quando é absolutamente necessário zombar do método).

Isso significa que não precisamos escrever um wrapper para um utilitário simples como o Apache Commons StringEscapeUtils(porque as strings que eles precisam podem ser fornecidas com facilidade) e não usamos zombarias para métodos estáticos (se acharmos que é hora de escrever) uma classe de invólucro e, em seguida, simule uma instância do invólucro).



1

Eu trabalho para uma grande companhia de seguros e nosso código-fonte chega a 400 MB de arquivos java puros. Desenvolvemos o aplicativo inteiro sem pensar no TDD. A partir de janeiro deste ano, começamos com testes de junção para cada componente individual.

A melhor solução em nosso departamento foi usar objetos Mock em alguns métodos JNI confiáveis ​​do sistema (escritos em C) e, como tal, não era possível estimar exatamente os resultados sempre em todos os sistemas operacionais. Não tínhamos outra opção senão usar classes simuladas e implementações específicas de métodos JNI especificamente com o objetivo de testar cada módulo individual do aplicativo para cada sistema operacional que suportamos.

Mas foi muito rápido e tem funcionado muito bem até agora. Eu recomendo - http://www.easymock.org/


1

Os objetos interagem entre si para atingir uma meta, quando você tem um objeto difícil de testar por causa do ambiente (um ponto de extremidade de serviço da web, camada dao acessando o banco de dados, controladores que manipulam parâmetros de solicitação http) ou você deseja testar seu objeto isoladamente e, em seguida, você zomba desses objetos.

a necessidade de zombar de métodos estáticos é um mau cheiro, você precisa projetar seu aplicativo mais Orientado a Objetos, e os métodos estáticos do utilitário de teste de unidade não agregam muito valor ao projeto, a classe wrapper é uma boa abordagem, dependendo da situação, mas tente para testar os objetos que usam os métodos estáticos.


1

Às vezes eu uso a opção 4

  1. Use o padrão de estratégia. Crie uma classe de utilitário com métodos estáticos que delegam a implementação em uma instância da interface conectável. Codifique um inicializador estático que se conecta a uma implementação concreta. Conecte uma implementação simulada para teste.

Algo assim.

public class DateUtil {
    public interface ITimestampGenerator {
        long getUtcNow();
    }

    class ConcreteTimestampGenerator implements ITimestampGenerator {
        public long getUtcNow() { return System.currentTimeMillis(); }
    }

    private static ITimestampGenerator timestampGenerator;

    static {
        timestampGenerator = new ConcreteTimeStampGenerator;
    }

    public static DateTime utcNow() {
        return new DateTime(timestampGenerator.getUtcNow(), DateTimeZone.UTC);
    }

    public static void setTimestampGenerator(ITimestampGenerator t) {...}

    // plus other util routines, which may or may not use the timestamp generator 
}

O que eu gosto nessa abordagem é que ela mantém os métodos utilitários estáticos, o que me parece correto quando estou tentando usar a classe em todo o código.

Math.sum(17, 29, 42);
// vs
new Math().sum(17, 29, 42);
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.