Pergunta

Meu lema para Java é "Só porque Java tem blocos estáticos, isso não significa que você deve usá -los". Piadas à parte, existem muitos truques em Java que tornam os testes um pesadelo.Duas das que mais odeio são classes anônimas e blocos estáticos.Temos muitos códigos legados que fazem uso de blocos estáticos e esses são um dos pontos irritantes em nosso esforço para escrever testes unitários.Nosso objetivo é ser capaz de escrever testes unitários para classes que dependem dessa inicialização estática com alterações mínimas no código.

Até agora, minha sugestão aos meus colegas é mover o corpo do bloco estático para um método estático privado e chamá-lo staticInit.Este método pode então ser chamado de dentro do bloco estático.Para testes unitários, outra classe que depende desta classe poderia facilmente simular staticInit com JMockit para não fazer nada.Vamos ver isso no exemplo.

public class ClassWithStaticInit {
  static {
    System.out.println("static initializer.");
  }
}

Será alterado para

public class ClassWithStaticInit {
  static {
    staticInit();
  }

  private static void staticInit() {
    System.out.println("static initialized.");
  }
}

Para que possamos fazer o seguinte em um JUnit.

public class DependentClassTest {
  public static class MockClassWithStaticInit {
    public static void staticInit() {
    }
  }

  @BeforeClass
  public static void setUpBeforeClass() {
    Mockit.redefineMethods(ClassWithStaticInit.class, MockClassWithStaticInit.class);
  }
}

No entanto, esta solução também traz seus próprios problemas.Você não pode correr DependentClassTest e ClassWithStaticInitTest na mesma JVM, já que você realmente deseja que o bloco estático seja executado ClassWithStaticInitTest.

Qual seria a sua maneira de realizar essa tarefa?Ou alguma solução melhor, não baseada em JMockit, que você acha que funcionaria de maneira mais limpa?

Foi útil?

Solução

Quando me deparo com esse problema, geralmente faço a mesma coisa que você descreve, exceto que torno o método estático protegido para poder invocá-lo manualmente.Além disso, certifico-me de que o método pode ser invocado várias vezes sem problemas (caso contrário, não é melhor que o inicializador estático no que diz respeito aos testes).

Isso funciona razoavelmente bem e posso realmente testar se o método inicializador estático faz o que espero/quero que faça.Às vezes é mais fácil ter algum código de inicialização estático e simplesmente não vale a pena construir um sistema excessivamente complexo para substituí-lo.

Quando uso esse mecanismo, certifico-me de documentar que o método protegido é exposto apenas para fins de teste, na esperança de que não seja usado por outros desenvolvedores.É claro que isso pode não ser uma solução viável, por exemplo, se a interface da classe for visível externamente (seja como um subcomponente de algum tipo para outras equipes ou como uma estrutura pública).Porém, é uma solução simples para o problema e não requer uma biblioteca de terceiros para ser configurada (o que eu gosto).

Outras dicas

PowerMock é outra estrutura simulada que estende EasyMock e Mockito.Com PowerMock você pode facilmente remover comportamento indesejado de uma classe, por exemplo, um inicializador estático.No seu exemplo, você simplesmente adiciona as seguintes anotações ao seu caso de teste JUnit:

@RunWith(PowerMockRunner.class)
@SuppressStaticInitializationFor("some.package.ClassWithStaticInit")

PowerMock não usa um agente Java e, portanto, não requer modificação dos parâmetros de inicialização da JVM.Você simplesmente adiciona o arquivo jar e as anotações acima.

Isso vai entrar no JMockit mais "avançado".Acontece que você pode redefinir blocos de inicialização estáticos no JMockit criando um public void $clinit() método.Então, em vez de fazer essa mudança

public class ClassWithStaticInit {
  static {
    staticInit();
  }

  private static void staticInit() {
    System.out.println("static initialized.");
  }
}

podemos muito bem sair ClassWithStaticInit como está e faça o seguinte no MockClassWithStaticInit:

public static class MockClassWithStaticInit {
  public void $clinit() {
  }
}

Na verdade, isso nos permitirá não fazer nenhuma alteração nas classes existentes.

Ocasionalmente, encontro inicializadores estáticos em classes das quais meu código depende.Se eu não puder refatorar o código, eu uso PowerMockde @SuppressStaticInitializationFor anotação para suprimir o inicializador estático:

@RunWith(PowerMockRunner.class)
@SuppressStaticInitializationFor("com.example.ClassWithStaticInit")
public class ClassWithStaticInitTest {

    ClassWithStaticInit tested;

    @Before
    public void setUp() {
        tested = new ClassWithStaticInit();
    }

    @Test
    public void testSuppressStaticInitializer() {
        asserNotNull(tested);
    }

    // more tests...
}

Leia mais sobre suprimindo comportamento indesejado.

Isenção de responsabilidade:PowerMock é um projeto de código aberto desenvolvido por dois colegas meus.

Parece-me que você está tratando um sintoma:design pobre com dependências de inicialização estática.Talvez alguma refatoração seja a solução real.Parece que você já refatorou um pouco seu staticInit() função, mas talvez essa função precise ser chamada a partir do construtor, não a partir de um inicializador estático.Se você puder acabar com o período dos inicializadores estáticos, será melhor.Só você pode tomar essa decisão (Não consigo ver sua base de código), mas alguma refatoração certamente ajudará.

Quanto à simulação, eu uso o EasyMock, mas me deparei com o mesmo problema.Os efeitos colaterais dos inicializadores estáticos no código legado dificultam os testes.Nossa resposta foi refatorar o inicializador estático.

Você poderia escrever seu código de teste no Groovy e simular facilmente o método estático usando metaprogramação.

Math.metaClass.'static'.max = { int a, int b -> 
    a + b
}

Math.max 1, 2

Se você não pode usar o Groovy, você realmente precisará refatorar o código (talvez para injetar algo como um inicializador).

Atenciosamente

Suponho que você realmente queira algum tipo de fábrica em vez do inicializador estático.

Alguma combinação de um singleton e uma fábrica abstrata provavelmente seria capaz de fornecer a mesma funcionalidade de hoje e com boa testabilidade, mas isso adicionaria bastante código padrão, então talvez seja melhor apenas tentar refatorar a coisa estática foi completamente removida ou se você poderia pelo menos conseguir alguma solução menos complexa.

Difícil dizer se é possível sem ver seu código.

Não tenho muito conhecimento em estruturas Mock, então, corrija-me se estiver errado, mas você não poderia ter dois objetos Mock diferentes para cobrir as situações mencionadas?Como

public static class MockClassWithEmptyStaticInit {
  public static void staticInit() {
  }
}

e

public static class MockClassWithStaticInit {
  public static void staticInit() {
    System.out.println("static initialized.");
  }
}

Então você pode usá-los em seus diferentes casos de teste

@BeforeClass
public static void setUpBeforeClass() {
  Mockit.redefineMethods(ClassWithStaticInit.class, 
                         MockClassWithEmptyStaticInit.class);
}

e

@BeforeClass
public static void setUpBeforeClass() {
  Mockit.redefineMethods(ClassWithStaticInit.class, 
                         MockClassWithStaticInit.class);
}

respectivamente.

Não é realmente uma resposta, mas apenas uma pergunta - não há nenhuma maneira de "reverter" a chamada para Mockit.redefineMethods?
Se esse método explícito não existir, executá-lo novamente da seguinte maneira não deveria funcionar?

Mockit.redefineMethods(ClassWithStaticInit.class, ClassWithStaticInit.class);

Se tal método existir, você poderia executá-lo na classe ' @AfterClass método e teste ClassWithStaticInitTest com o bloco inicializador estático "original", como se nada tivesse mudado, da mesma JVM.

Este é apenas um palpite, então posso estar faltando alguma coisa.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top