Como adicionar cobertura de teste a um construtor privado?

110

Este é o código:

package com.XXX;
public final class Foo {
  private Foo() {
    // intentionally empty
  }
  public static int bar() {
    return 1;
  }
}

Este é o teste:

package com.XXX;
public FooTest {
  @Test 
  void testValidatesThatBarWorks() {
    int result = Foo.bar();
    assertEquals(1, result);
  }
  @Test(expected = java.lang.IllegalAccessException.class)
  void testValidatesThatClassFooIsNotInstantiable() {
    Class cls = Class.forName("com.XXX.Foo");
    cls.newInstance(); // exception here
  }
}

Funciona bem, a classe é testada. Mas Cobertura diz que não há cobertura de código do construtor privado da classe. Como podemos adicionar cobertura de teste a um construtor privado?

Yegor256
fonte
Parece-me que você está tentando impor o padrão Singleton. Se sim, você pode gostar de dp4j.com (que faz exatamente isso)
simpatico
não deveria "intencionalmente vazio" ser substituído por lançar exceção? Nesse caso, você poderia escrever o teste que espera aquela exceção específica com mensagem específica, não? não tenho certeza se isso é um exagero
Ewoks de

Respostas:

85

Bem, existem maneiras de usar o reflexo, etc. - mas realmente vale a pena? Este é um construtor que nunca deve ser chamado , certo?

Se houver uma anotação ou algo semelhante que você possa adicionar à classe para fazer o Cobertura entender que ela não será chamada, faça isso: não acho que vale a pena se esforçar para adicionar cobertura artificialmente.

EDIT: Se não há como fazer, apenas viva com a cobertura ligeiramente reduzida. Lembre-se de que a cobertura deve ser útil para você - você deve ser o responsável pela ferramenta, e não o contrário.

Jon Skeet
fonte
18
Não quero "reduzir ligeiramente a cobertura" em todo o projeto apenas por causa deste construtor em particular ..
yegor256
36
@Vincenzo: Então, IMO, você está colocando um valor muito alto em um número simples. A cobertura é um indicador de teste. Não seja escravo de uma ferramenta. O objetivo da cobertura é fornecer a você um nível de confiança e sugerir áreas para testes extras. Chamar artificialmente um construtor não utilizado não ajuda em nenhum desses pontos.
Jon Skeet
19
@JonSkeet: Concordo totalmente com "Não seja escravo de uma ferramenta", mas não cheira bem lembrar de cada "contagem de falhas" em cada projeto. Como ter certeza de que o resultado 7/9 é uma limitação do Cobertura, e não do programador? Um novo programador deve inserir cada falha (que pode ser muito em grandes projetos) para verificar aula por aula.
Eduardo Costa
5
Isso não responde à pergunta. e, a propósito, alguns gerentes olham para os números de cobertura. Eles não se importam por quê. Eles sabem que 85% é melhor do que 75%.
ACV de
2
Um caso de uso prático para testar código de outra forma inacessível é atingir 100% de cobertura de teste para que ninguém precise olhar para aquela classe novamente. Se a cobertura estiver estagnada em 95%, muitos desenvolvedores podem tentar descobrir o motivo disso apenas para se deparar com esse problema repetidamente.
thisismydesign
140

Não concordo inteiramente com Jon Skeet. Eu acho que se você pode obter uma vitória fácil para lhe dar cobertura e eliminar o ruído em seu relatório de cobertura, então você deve fazer isso. Diga à sua ferramenta de cobertura para ignorar o construtor ou coloque o idealismo de lado e escreva o seguinte teste e pronto:

@Test
public void testConstructorIsPrivate() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
  Constructor<Foo> constructor = Foo.class.getDeclaredConstructor();
  assertTrue(Modifier.isPrivate(constructor.getModifiers()));
  constructor.setAccessible(true);
  constructor.newInstance();
}
Javid Jamae
fonte
25
Mas isso está eliminando o ruído no relatório de cobertura, adicionando ruído ao conjunto de testes. Teria acabado de terminar a frase "ponha o idealismo de lado". :)
Christopher Orr
11
Para dar a este teste qualquer tipo de significado, você provavelmente também deve afirmar que o nível de acesso do construtor é o que você espera que seja.
Jeremy
Adicionando o reflexo do mal mais as ideias de Jeremy e um nome significativo como "testIfConstructorIsPrivateWithoutRaisingExceptions", acho que esta é "A" resposta.
Eduardo Costa
1
Isso é sintaticamente incorreto, não é? O que é constructor? Não deve Constructorser parametrizado e não um tipo bruto?
Adam Parkin,
2
Isso está errado: constructor.isAccessible()sempre retorna falso, mesmo em um construtor público. Deve-se usar assertTrue(Modifier.isPrivate(constructor.getModifiers()));.
timomeinen
78

Embora não seja necessariamente para cobertura, criei este método para verificar se a classe de utilitário está bem definida e fazer um pouco de cobertura também.

/**
 * Verifies that a utility class is well defined.
 * 
 * @param clazz
 *            utility class to verify.
 */
public static void assertUtilityClassWellDefined(final Class<?> clazz)
        throws NoSuchMethodException, InvocationTargetException,
        InstantiationException, IllegalAccessException {
    Assert.assertTrue("class must be final",
            Modifier.isFinal(clazz.getModifiers()));
    Assert.assertEquals("There must be only one constructor", 1,
            clazz.getDeclaredConstructors().length);
    final Constructor<?> constructor = clazz.getDeclaredConstructor();
    if (constructor.isAccessible() || 
                !Modifier.isPrivate(constructor.getModifiers())) {
        Assert.fail("constructor is not private");
    }
    constructor.setAccessible(true);
    constructor.newInstance();
    constructor.setAccessible(false);
    for (final Method method : clazz.getMethods()) {
        if (!Modifier.isStatic(method.getModifiers())
                && method.getDeclaringClass().equals(clazz)) {
            Assert.fail("there exists a non-static method:" + method);
        }
    }
}

Coloquei o código completo e exemplos em https://github.com/trajano/maven-jee6/tree/master/maven-jee6-test

Arquimedes Trajano
fonte
11
+1 Isso não apenas resolve o problema sem enganar a ferramenta, mas testa totalmente os padrões de codificação para configurar uma classe de utilitário. Tive que alterar o teste de acessibilidade para usar Modifier.isPrivatecomo isAccessibleestava voltando truepara construtores privados em alguns casos (simulando interferência de biblioteca?).
David Harkness
4
Eu realmente quero adicionar isso à classe Assert do JUnit, mas não quero receber crédito pelo seu trabalho. Eu acho que é muito bom. Seria ótimo ter Assert.utilityClassWellDefined()no JUnit 4.12+. Você considerou uma solicitação de pull?
Visionary Software Solutions
Observe que usar setAccessible()para tornar o construtor acessível causa problemas para a ferramenta de cobertura de código do Sonar (quando faço isso, a classe desaparece dos relatórios de cobertura de código do Sonar).
Adam Parkin
Obrigado, eu redefino o sinalizador acessível. Talvez seja um bug no próprio Sonar?
Archimedes Trajano
Eu olhei para o meu relatório do Sonar para cobertura no meu plug-in do batik maven, parece cobrir corretamente. site.trajano.net/batik-maven-plugin/cobertura/index.html
Archimedes Trajano
19

Eu tornei privado o construtor da minha classe de funções utilitárias estáticas, para satisfazer CheckStyle. Mas assim como o pôster original, tive Cobertura reclamando do teste. No início, tentei essa abordagem, mas isso não afeta o relatório de cobertura porque o construtor nunca é realmente executado. Então, na verdade, todos esses testes são se o construtor está permanecendo privado - e isso se torna redundante pela verificação de acessibilidade no teste subsequente.

@Test(expected=IllegalAccessException.class)
public void testConstructorPrivate() throws Exception {
    MyUtilityClass.class.newInstance();
    fail("Utility class constructor should be private");
}

Segui a sugestão de Javid Jamae e usei reflexão, mas acrescentei afirmações para pegar alguém mexendo com a classe sendo testada (e nomeei o teste para indicar altos níveis do mal).

@Test
public void evilConstructorInaccessibilityTest() throws Exception {
    Constructor[] ctors = MyUtilityClass.class.getDeclaredConstructors();
    assertEquals("Utility class should only have one constructor",
            1, ctors.length);
    Constructor ctor = ctors[0];
    assertFalse("Utility class constructor should be inaccessible", 
            ctor.isAccessible());
    ctor.setAccessible(true); // obviously we'd never do this in production
    assertEquals("You'd expect the construct to return the expected type",
            MyUtilityClass.class, ctor.newInstance().getClass());
}

Isso é um exagero, mas tenho que admitir que gosto da sensação calorosa de 100% de cobertura de método.

Ben Hardy
fonte
Pode ser um exagero, mas se fosse em Unitils ou similar, eu usaria
Stewart
+1 Bom começo, embora eu tenha ido com o teste mais completo de Arquimedes .
David Harkness
O primeiro exemplo não funciona - a IllegalAccesException significa que o construtor nunca é chamado, portanto, a cobertura não é registrada.
Tom McIntyre
IMO, a solução no primeiro trecho de código é a mais limpa e a mais simples nesta discussão. Apenas se alinhar com fail(...)não é necessário.
Piotr Wittchen
9

Com o Java 8 , é possível encontrar outra solução.

Presumo que você simplesmente deseja criar uma classe de utilitário com poucos métodos estáticos públicos. Se você pode usar o Java 8, então você pode usar no interfacelugar.

package com.XXX;

public interface Foo {

  public static int bar() {
    return 1;
  }
}

Não há construtor e nem reclamação da Cobertura. Agora você precisa testar apenas as linhas de seu interesse.

Arnost Valicek
fonte
1
Infelizmente, no entanto, você não pode declarar a interface como "final", impedindo que alguém a subclasse - caso contrário, esta seria a melhor abordagem.
Michael Berry
5

O raciocínio por trás do código de teste que não faz nada é atingir 100% de cobertura de código e notar quando a cobertura de código cai. Caso contrário, pode-se sempre pensar, ei, eu não tenho mais 100% de cobertura de código, mas é PROVAVELMENTE por causa dos meus construtores privados. Isso facilita a localização de métodos não testados sem ter que verificar se era apenas um construtor privado. Conforme sua base de código cresce, você vai realmente sentir uma sensação de calor agradável olhando para 100% em vez de 99%.

IMO, é melhor usar reflexão aqui, caso contrário, você teria que obter uma ferramenta de cobertura de código melhor que ignore esses construtores ou de alguma forma dizer à ferramenta de cobertura de código para ignorar o método (talvez uma anotação ou um arquivo de configuração) porque então você ficaria preso com uma ferramenta de cobertura de código específica.

Em um mundo perfeito, todas as ferramentas de cobertura de código ignorariam os construtores privados que pertencem a uma classe final, porque o construtor está lá como uma medida de "segurança", nada mais :)
Eu usaria este código:

    @Test
    public void callPrivateConstructorsForCodeCoverage() throws SecurityException, NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException
    {
        Class<?>[] classesToConstruct = {Foo.class};
        for(Class<?> clazz : classesToConstruct)
        {
            Constructor<?> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            assertNotNull(constructor.newInstance());
        }
    }
Em seguida, basta adicionar classes ao array conforme você avança.

Jontejj
fonte
5

As versões mais recentes do Cobertura têm suporte integrado para ignorar getters / setters / construtores triviais:

https://github.com/cobertura/cobertura/wiki/Ant-Task-Reference#ignore-trivial

Ignorar trivial

Ignorar trivial permite a capacidade de excluir construtores / métodos que contêm uma linha de código. Alguns exemplos incluem uma chamada apenas para um superconstrutor, métodos getter / setter etc. Para incluir o argumento ignorar trivial, adicione o seguinte:

<cobertura-instrument ignoreTrivial="true" />

ou em uma versão do Gradle:

cobertura {
    coverageIgnoreTrivial = true
}
Mike Buhot
fonte
4

Não. Qual é o objetivo de testar um construtor vazio? Como a cobertura 2.0 existe uma opção para ignorar esses casos triviais (junto com setters / getters), você pode habilitá-lo no maven adicionando a seção de configuração ao plugin cobertura maven:

<configuration>
  <instrumentation>
    <ignoreTrivial>true</ignoreTrivial>                 
  </instrumentation>
</configuration>

Alternativamente, você pode usar Cobertura Anotações : @CoverageIgnore.

Krzysztof Krasoń
fonte
3

Finalmente, existe uma solução!

public enum Foo {;
  public static int bar() {
    return 1;
  }
}
kan
fonte
Porém, como isso está testando a classe postada na pergunta? Você não deve presumir que pode transformar cada classe com um construtor privado em um enum, ou que você gostaria.
Jon Skeet
@JonSkeet eu posso para a classe em questão. E a maioria das classes utilitárias que possuem apenas um monte de métodos estáticos. Caso contrário, uma classe com o único construtor privado não tem nenhum sentido.
kan
1
Uma classe com um construtor privado pode ser instanciada a partir de métodos estáticos públicos, embora seja fácil obter a cobertura. Mas fundamentalmente eu preferiria qualquer aula que se estendesse Enum<E>para realmente ser um enum ... Eu acredito que revela melhor a intenção.
Jon Skeet
4
Uau, eu absolutamente preferem código que faz sentido, ao longo de um número bastante arbitrária. (Cobertura não é garantia de qualidade, nem é 100% de cobertura viável em todos os casos. Seus testes devem guiar seu código na melhor das hipóteses - não conduzi-lo a um penhasco de intenções bizarras.)
Jon Skeet
1
@Kan: Adicionar uma chamada fictícia ao construtor para blefar a ferramenta não deve ser a intenção. Qualquer pessoa que depende de uma única métrica para determinar o bem-estar do projeto já está no caminho da destruição.
Apoorv Khurasia
1

Não sei sobre o Cobertura, mas uso o Clover e ele tem um meio de adicionar exclusões de correspondência de padrões. Por exemplo, tenho padrões que excluem as linhas de log do apache-commons para que não sejam contadas na cobertura.

John Engelman
fonte
1

Outra opção é criar um inicializador estático semelhante ao seguinte código

class YourClass {
  private YourClass() {
  }
  static {
     new YourClass();
  }

  // real ops
}

Dessa forma, o construtor privado é considerado testado e a sobrecarga do tempo de execução basicamente não é mensurável. Eu faço isso para obter 100% de cobertura usando EclEmma, ​​mas provavelmente funciona para todas as ferramentas de cobertura. A desvantagem dessa solução, é claro, é que você escreve o código de produção (o inicializador estático) apenas para fins de teste.

Christian Lewold
fonte
Eu faço isso um pouco. Barato como barato, barato como sujo, mas eficaz.
pholser de
Com o Sonar, isso realmente faz com que a aula seja totalmente perdida por cobertura de código.
Adam Parkin
1

ClassUnderTest testClass = Whitebox.invokeConstructor (ClassUnderTest.class);

acpuma
fonte
Esta deveria ter sido a resposta correta, pois responde exatamente ao que é perguntado.
Chakian
0

Às vezes, o Cobertura marca o código que não deve ser executado como 'não coberto', não há nada de errado com isso. Por que você está preocupado em ter 99%cobertura em vez de 100%?

Tecnicamente, porém, você ainda pode invocar esse construtor com reflexão, mas parece muito errado para mim (neste caso).

Nikita Rybak
fonte
0

Se eu fosse adivinhar a intenção de sua pergunta, diria:

  1. Você quer verificações razoáveis ​​para construtores privados que fazem o trabalho real, e
  2. Você deseja que o trevo exclua os construtores vazios das classes util.

Para 1, é óbvio que você deseja que toda a inicialização seja feita através dos métodos de fábrica. Nesses casos, seus testes devem ser capazes de testar os efeitos colaterais do construtor. Isso deve cair na categoria de teste de método privado normal. Faça os métodos menores para que eles façam apenas um número limitado de coisas determinadas (idealmente, apenas uma coisa e uma coisa bem) e, em seguida, teste os métodos que dependem deles.

Por exemplo, se meu construtor [privado] configurar os campos de instância de minha classe apara 5. Então eu posso (ou melhor, devo) testá-lo:

@Test
public void testInit() {
    MyClass myObj = MyClass.newInstance(); //Or whatever factory method you put
    Assert.assertEquals(5, myObj.getA()); //Or if getA() is private then test some other property/method that relies on a being 5
}

Para 2, você pode configurar o trevo para excluir construtores Util se você tiver um padrão de nomenclatura definido para classes Util. Por exemplo, em meu próprio projeto eu uso algo assim (porque seguimos a convenção de que os nomes de todas as classes Util devem terminar com Util):

<clover-setup initString="${build.dir}/clovercoverage.db" enabled="${with.clover}">
    <methodContext name="prvtCtor" regexp="^private *[a-zA-Z0-9_$]+Util *( *) *"/>
</clover-setup>

Eu deixei deliberadamente de fora um .*seguinte )porque tais construtores não pretendem lançar exceções (eles não pretendem fazer nada).

É claro que pode haver um terceiro caso em que você pode querer ter um construtor vazio para uma classe não utilitária. Nesses casos, recomendo que você coloque um methodContextcom a assinatura exata do construtor.

<clover-setup initString="${build.dir}/clovercoverage.db" enabled="${with.clover}">
    <methodContext name="prvtCtor" regexp="^private *[a-zA-Z0-9_$]+Util *( *) *"/>
    <methodContext name="myExceptionalClassCtor" regexp="^private MyExceptionalClass()$"/>
</clover-setup>

Se você tiver muitas dessas classes excepcionais, poderá escolher modificar o construtor privado generalizado reg-ex que sugeri e remover Utildele. Nesse caso, você terá que se certificar manualmente de que os efeitos colaterais do construtor ainda sejam testados e cobertos por outros métodos em sua classe / projeto.

<clover-setup initString="${build.dir}/clovercoverage.db" enabled="${with.clover}">
    <methodContext name="prvtCtor" regexp="^private *[a-zA-Z0-9_$]+ *( *) .*"/>
</clover-setup>
Apoorv Khurasia
fonte
0
@Test
public void testTestPrivateConstructor() {
    Constructor<Test> cnt;
    try {
        cnt = Test.class.getDeclaredConstructor();
        cnt.setAccessible(true);

        cnt.newInstance();
    } catch (Exception e) {
        e.getMessage();
    }
}

Test.java é o seu arquivo de origem, que tem seu construtor privado

DPREDDY
fonte
Seria bom explicar por que essa construção ajuda na cobertura.
Markus
Verdadeiro, e em segundo lugar: por que capturar uma exceção em seu teste? A exceção lançada deve realmente fazer o teste falhar.
Jordi
0

O seguinte funcionou para mim em uma classe criada com a anotação do Lombok @UtilityClass, que adiciona automaticamente um construtor privado.

@Test
public void testConstructorIsPrivate() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
    Constructor<YOUR_CLASS_NAME> constructor = YOUR_CLASS_NAME.class.getDeclaredConstructor();
    assertTrue(Modifier.isPrivate(constructor.getModifiers())); //this tests that the constructor is private
    constructor.setAccessible(true);
    assertThrows(InvocationTargetException.class, () -> {
        constructor.newInstance();
    }); //this add the full coverage on private constructor
}

Embora constructor.setAccessible (true) deva funcionar quando o construtor privado foi escrito manualmente, com a anotação do Lombok não funciona, uma vez que a força. Constructor.newInstance () realmente testa se o construtor é invocado e isso completa a cobertura do próprio construtor. Com o assertThrows você evita que o teste falhe e gerenciou a exceção, pois é exatamente o erro que você esperava. Embora esta seja uma solução alternativa e eu não aprecie o conceito de "cobertura de linha" versus "cobertura de funcionalidade / comportamento", podemos encontrar um sentido neste teste. Na verdade, você tem certeza de que a Classe Utilitário possui um Construtor privado que lança uma exceção corretamente quando invocado também por meio de reflexão. Espero que isto ajude.

Riccardo Solimena
fonte
Olá, @ShanteshwarInde. Muito obrigado. Minha entrada foi editada e concluída de acordo com suas sugestões. Saudações.
Riccardo Solimena
0

Minha opção preferida em 2019: Use lombok.

Especificamente, a @UtilityClassanotação . (Infelizmente, apenas "experimental" no momento em que este livro foi escrito, mas funciona muito bem e tem uma perspectiva positiva, portanto, provavelmente em breve será atualizado para estável.)

Essa anotação adicionará o construtor privado para evitar a instanciação e tornará a classe final. Quando combinado com lombok.addLombokGeneratedAnnotation = truein lombok.config, praticamente todas as estruturas de teste irão ignorar o código gerado automaticamente ao calcular a cobertura do teste, permitindo que você ignore a cobertura desse código gerado automaticamente sem hacks ou reflexão.

Michael Berry
fonte
-2

Você não pode.

Aparentemente, você está criando o construtor privado para evitar a instanciação de uma classe destinada a conter apenas métodos estáticos. Em vez de tentar obter cobertura desse construtor (o que exigiria que a classe fosse instanciada), você deve se livrar dele e confiar que seus desenvolvedores não adicionarão métodos de instância à classe.

Anon
fonte
3
Isso está incorreto; você pode instanciá-lo por meio de reflexão, conforme observado acima.
theotherian
Isso é ruim, nunca deixe o construtor público padrão aparecer, você deve adicionar o privado para evitar chamá-lo.
Lho Ben