Como lidar com classes de utilitários estáticos ao projetar para testabilidade

62

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

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

Tenho várias ideias para resolver este problema:

  1. Use um framework simulado que pode zombar de classes estáticas (como o PowerMock). Esta pode ser a solução mais simples, mas de alguma forma parece desistir.
  2. Crie classes wrapper instanciáveis em torno de todos esses utilitários estáticos para que possam ser injetados nas classes que os utilizam. Isso parece uma solução relativamente limpa, mas temo que acabemos criando muitas dessas classes de invólucros.
  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 eu continuo pensando que isso só tem que ser um problema que muitas pessoas têm que enfrentar ao fazer TDD - então já deve haver soluções para esse problema.

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

    
por Benedikt 27.04.2012 / 10:48
fonte

7 respostas

33

(Não há fontes "oficiais" aqui, receio - não é como se houvesse uma especificação de como testar bem. Apenas as minhas opiniões, que esperamos que sejam úteis.)

Quando esses métodos estáticos representam as 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 unir uma lista de strings, adicionando uma vírgula entre elas. Não há ponto em zombar deles - apenas deixe a chamada estática fazer seu trabalho normal. Você não quer ou precisa substituir o comportamento normal; você não está lidando com um recurso externo ou algo difícil de se trabalhar, são apenas dados. O resultado é previsível e você nunca desejaria que fosse qualquer coisa outra do que o que você terá de qualquer maneira.

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

    
por 09.05.2012 / 08:06
fonte
20

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

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

Dito isto, o PowerMock existe por um motivo. É um problema bastante conhecido que testar quando os métodos estáticos estão envolvidos é seriamente doloroso, daí a criação do PowerMock. Eu acho que você precisa pesar suas opções em termos de quanto trabalho será para envolver todas as suas classes auxiliares versus usando PowerMock. Eu não acho que é desistir de usar o PowerMock - eu sinto que envolver as classes permite mais flexibilidade em um grande projeto. Quanto mais contratos públicos (interfaces) você puder fornecer ao limpador, a separação entre a intenção e a implementação.

    
por 27.04.2012 / 10:57
fonte
4

Como referência para todos que também estão lidando com esse problema e se depararem com essa questão, descreverei como decidimos resolver o problema:

Basicamente, seguimos o caminho descrito como # 2 (classes de wrapper para utilitários estáticos). Mas nós só os usamos quando é muito complexo para fornecer à concessionária os dados necessários para produzir a saída desejada (ou seja, quando temos que ridicularizar o 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 facilmente) e não usamos mocks para métodos estáticos (se achamos que podemos precisa de tempo para escrever uma classe wrapper e, em seguida, zombar de uma instância do wrapper).

    
por 08.05.2012 / 11:02
fonte
2

Eu testaria essas classes usando o Groovy . O Groovy é simples de adicionar em qualquer projeto Java. Com isso, você pode zombar dos métodos estáticos com bastante facilidade. Veja Mocking Static Methods usando Groovy para um exemplo.

    
por 03.05.2012 / 21:58
fonte
1

Eu trabalho para uma grande companhia de seguros e nosso código-fonte sobe para 400MB de arquivos java. Temos desenvolvido todo o aplicativo sem pensar em TDD. A partir de janeiro deste ano, começamos com testes junit para cada componente individual.

A melhor solução em nosso departamento era usar objetos Mock em alguns métodos JNI que eram confiáveis para o sistema (escritos em C) e, como tal, você não poderia estimar exatamente os resultados todas as vezes em todos os sistemas operacionais. Não tínhamos outra opção senão usar classes ridicularizadas e implementações específicas de métodos JNI especificamente para o propósito de testar cada módulo individual do aplicativo para cada SO que suportamos.

Mas foi muito rápido e tem funcionado muito bem até agora. Eu recomendo - link

    
por 08.05.2012 / 13:09
fonte
1

Os objetos interagem uns com os outros para atingir um objetivo, quando você tem um objeto difícil de testar devido ao ambiente (um ponto de extremidade do serviço web, camada dao acessando o banco de dados, controladores lidando com parâmetros de solicitação http) ou deseja testar seu objeto isolamento, então você zomba desses objetos.

a necessidade de zombar de métodos estáticos é um mau cheiro, você tem que projetar sua aplicação mais Orientada a Objetos, e métodos estáticos de teste unitário não agregam muito valor ao projeto, a classe wrapper é uma boa abordagem dependendo da situação , mas tente testar os objetos que usam os métodos estáticos.

    
por 08.05.2012 / 13:33
fonte
1

Às vezes, 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 a uma instância de interface conectável. Codifique um inicializador estático que se conecte em uma implementação concreta. Conecte uma implementação simulada para teste.

Algo parecido com isto.

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 de utilitários estáticos, o que parece certo para mim quando estou tentando usar a classe em todo o código.

Math.sum(17, 29, 42);
// vs
new Math().sum(17, 29, 42);
    
por 22.05.2015 / 18:46
fonte