Abordagens graduais para injeção de dependência


12

Estou trabalhando para tornar minhas aulas testáveis ​​por unidade, usando injeção de dependência. Mas algumas dessas classes têm muitos clientes e não estou pronto para refatorar todas elas para começar a passar nas dependências ainda. Então, eu estou tentando fazer isso gradualmente; mantendo as dependências padrão por enquanto, mas permitindo que elas sejam substituídas para teste.

Uma abordagem que estou discutindo é apenas mover todas as "novas" chamadas para seus próprios métodos, por exemplo:

public MyObject createMyObject(args) {
  return new MyObject(args);
}

Em meus testes de unidade, posso apenas subclassificar essa classe e substituir as funções de criação, para que eles criem objetos falsos.

Será esta uma boa abordagem? Existem desvantagens?

Em geral, é bom ter dependências codificadas, desde que você possa substituí-las para teste? Eu sei que a abordagem preferida é explicitamente exigi-los no construtor e gostaria de chegar lá eventualmente. Mas eu estou querendo saber se este é um bom primeiro passo.

Uma desvantagem que me ocorreu: se você tem subclasses reais que precisa testar, não poderá reutilizar a subclasse de teste que escreveu para a classe pai. Você precisará criar uma subclasse de teste para cada subclasse real e ela deverá substituir as mesmas funções de criação.


Você pode explicar por que eles não são testáveis ​​por unidade agora?
Austin Salonen

Existem muitos "novos X" espalhados por todo o código, além de muitos usos de classes estáticas e singletons. Então, quando tento testar uma classe, estou realmente testando várias delas. Se eu conseguir isolar as dependências e substituí-las por objetos falsos, terei mais controle sobre os testes.
JW01

Respostas:


13

Essa é uma boa abordagem para você começar. Observe que o mais importante é cobrir seu código existente com testes de unidade; Depois de fazer os testes, você pode refatorar mais livremente para melhorar ainda mais seu design.

Portanto, o ponto inicial não é tornar o design elegante e brilhante - apenas para tornar a unidade de código testável com as mudanças menos arriscadas . Sem testes de unidade, você deve ser extremamente conservador e cauteloso para evitar a quebra de seu código. Essas alterações iniciais podem até fazer o código parecer mais desajeitado ou feio em alguns casos - mas, se permitirem que você escreva os primeiros testes de unidade, poderá refatorar para o design ideal que você tem em mente.

O trabalho fundamental para ler sobre esse assunto é Trabalhar efetivamente com o código legado . Ele descreve o truque que você mostra acima e muitos, muitos mais, usando vários idiomas.


Apenas uma observação sobre isso "depois de fazer os testes, você pode refatorar mais livremente" - Se você não tiver testes, não estará refatorando, apenas mudando o código.
ocodo 30/01

@Slomojo Entendo o seu ponto de vista, mas você ainda pode refatorar com segurança sem testes, especialmente os ordenadores IDE automatizados, como por exemplo (no eclipse para Java) extrair código duplicado para métodos, renomear tudo, mover classes e extrair campos, constantes etc.
Alb 30/01

Estou apenas citando as origens do termo Refatoração, que está modificando o código em padrões aprimorados, protegidos contra erros de regressão por uma estrutura de teste. É claro que a refatoração baseada em IDE tornou muito mais trivial 'refatorar' o software, mas eu tendem a preferir evitar a auto-ilusão, se o meu software não tiver testes e eu "refatorar" estou apenas alterando o código. Claro, isso pode não ser o que eu digo a outra pessoa;)
ocodo

1
@ Alb, refatorar sem testes é sempre mais arriscado. As refatorações automatizadas certamente diminuem esse risco, mas não a zero. Sempre há casos difíceis de bordar com os quais as ferramentas podem não ser capazes de lidar corretamente. Na melhor das hipóteses, o resultado é uma mensagem de erro da ferramenta, mas, na pior das hipóteses, pode introduzir silenciosamente erros sutis. Portanto, é recomendável manter as alterações mais simples e seguras possíveis, mesmo com ferramentas automatizadas.
Péter Török

3

O pessoal da Guice recomenda usar

@Inject
public void setX(.... X x) {
   this.x = x;
}

para todas as propriedades, em vez de apenas adicionar @Inject à sua definição. Isso permitirá que você os trate como beans normais, que podem ser newtestados (e configurados manualmente) e @Inject na produção.


3

Quando me deparo com esse tipo de problema, identificarei cada dependência da minha classe a ser testada e criarei um construtor protegido que permitirá que eu as passe. É efetivamente o mesmo que criar um setter para cada um, mas prefiro declarar dependências em meus construtores para que eu não esqueça de instancia-las no futuro.

Portanto, minha nova classe de unidade testável terá 2 construtores:

public MyClass() { 
  this(new Client1(), new Client2()); 
}

public MyClass(Client1 client1, Client2 client2) { 
  this.client1 = client1; 
  this.client2 = client2; 
}

2

Você pode considerar o idioma "getter cria" até poder usar a dependência injetada.

Por exemplo,

public class Example {
  private MyObject myObject=null;

  public MyObject getMyObject() {
    if (myObject==null) {
      myObject=new MyObject();
    } 
    return myObject;
  }

  public void setMyObject(MyObject myObject) {
    this.myObject=myObject;
  }

  private void myMethod() {
    if (getMyObject().doSomething()) {
      // Instance automatically created
    }
  }
}

Isso permitirá refatorar internamente para que você possa referenciar seu myObject através do getter. Você nunca precisa ligar new. No futuro, quando todos os clientes estiverem configurados para permitir a injeção de dependência, basta remover o código de criação em um único local - o getter. O setter permitirá que você injete objetos simulados, conforme necessário.

O código acima é apenas um exemplo e expõe diretamente o estado interno. Você deve considerar cuidadosamente se essa abordagem é apropriada para sua base de código.

Também recomendo que você leia " Trabalhando efetivamente com o código herdado ", que contém várias dicas úteis para obter sucesso em uma grande refatoração.


Obrigado. Estou considerando essa abordagem em alguns casos. Mas o que você faz quando o construtor do MyObject requer parâmetros que estão disponíveis apenas dentro da sua classe? Ou quando você precisa criar mais de um MyObject durante a vida útil da sua classe? Você precisa injetar um MyObjectFactory?
JW01

@ JW01 Você pode configurar o código do criador do getter para ler o estado interno da sua classe e apenas ajustá-lo. Criar mais de uma instância durante a vida útil será difícil de orquestrar. Se você se deparar com essa situação, sugiro uma reformulação, em vez de uma solução alternativa. Não torne seus getters muito complexos ou eles se tornarão uma pedra de moinho no pescoço. Seu objetivo é facilitar o processo de refatoração. Se parecer muito difícil, vá para uma área diferente para corrigi-la lá.
Gary Rowe

isso é um singleton. Apenas não use singletons O_O
CoffeDeveloper

@DarioOO Na verdade, é um singleton muito pobre. Uma implementação melhor usaria uma enumeração conforme Josh Bloch. Singletons são um padrão de design válido e têm seus usos (criação única cara, etc).
Gary Rowe
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.