Escrevi alguns testes de unidade para algum código novo no trabalho e o enviei para uma revisão de código. Um de meus colegas de trabalho fez um comentário sobre por que eu estava colocando variáveis usadas em vários desses testes fora do escopo do teste.
O código que eu publiquei era essencialmente
import org.junit.Test;
public class FooUnitTests {
@Test
public void testConstructorWithValidName() {
new Foo(VALID_NAME);
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorWithNullName() {
new Foo(null);
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorWithZeroLengthName() {
new Foo("");
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorWithLeadingAndTrailingWhitespaceInName() {
final String name = " " + VALID_NAME + " ";
final Foo foo = new Foo(name);
assertThat(foo.getName(), is(equalTo(VALID_NAME)));
}
private static final String VALID_NAME = "name";
}
Suas mudanças propostas foram essencialmente
import org.junit.Test;
public class FooUnitTests {
@Test
public void testConstructorWithValidName() {
final String name = "name";
final Foo foo = new Foo(name);
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorWithNullName() {
final String name = null;
final Foo foo = new Foo(name);
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorWithZeroLengthName() {
final String name = "";
final Foo foo = new Foo(name);
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorWithLeadingAndTrailingWhitespaceInName() {
final String name = " name ";
final Foo foo = new Foo(name);
final String actual = foo.getName();
final String expected = "name";
assertThat(actual, is(equalTo(expected)));
}
}
Onde tudo o que é necessário dentro do escopo do teste é definido dentro do escopo do teste.
Algumas das vantagens que ele argumentou foram
- Cada teste é independente.
- Cada teste pode ser executado isoladamente ou em agregação, com o mesmo resultado.
- O revisor não precisa rolar para onde esses parâmetros forem declarados para procurar qual é o valor.
Algumas das desvantagens de seu método que eu argumentei foram
- Aumenta a duplicação de código
- Pode adicionar ruído à mente dos revisores se houver vários testes semelhantes com valores diferentes definidos (por exemplo, teste com
doFoo(bar)
resultados em um valor, enquanto a mesma chamada tem um resultado diferente porquebar
é definido de maneira diferente nesse método).
Além da convenção, existem outras vantagens / desvantagens de usar um dos métodos sobre o outro?
java
unit-testing
tdd
Zymus
fonte
fonte
Respostas:
Você deve continuar fazendo o que está fazendo.
Repetir a si mesmo é uma idéia tão ruim no código de teste quanto no código comercial e pelo mesmo motivo. Seu colega foi enganado pela idéia de que todo teste deve ser independente . Isso é verdade, mas "independente" não significa que ele deve conter tudo o que precisa dentro do corpo do método. Isso significa apenas que ele deve fornecer o mesmo resultado, seja executado isoladamente ou como parte de um conjunto, e independentemente da ordem dos testes no conjunto. Em outras palavras, o código de teste deve ter a mesma semântica, independentemente do outro código executado antes dele ; ele não precisa ter todo o código necessário agrupado textualmente .
Reutilizar constantes, código de configuração e similares aumenta a qualidade do código de teste e não compromete sua autossuficiência; portanto, é bom que você continue fazendo isso.
fonte
Depende. Em geral, ter constantes repetidas declaradas em apenas um lugar é uma coisa boa.
No entanto, no seu exemplo, não faz sentido definir um membro
VALID_NAME
da maneira mostrada. Supondo que na segunda variante um mantenedor altere o textoname
no primeiro teste, o segundo teste provavelmente ainda será válido e vice-versa. Por exemplo, suponha que você queira testar adicionalmente letras maiúsculas e minúsculas, mas também mantenha um caso de teste apenas com letras minúsculas, com o menor número possível de casos de teste. Em vez de adicionar um novo teste, você pode alterar o primeiro teste parae ainda mantenha os testes restantes.
De fato, ter muitos testes dependendo dessa variável e alterar o valor de
VALID_NAME
mais tarde podem interromper acidentalmente alguns de seus testes em um momento posterior. E, em tal situação, você pode realmente melhorar a auto-contenção de seus testes, não introduzindo uma constante artificial com diferentes testes, dependendo dela.No entanto, o que o @KilianFoth escreveu sobre não se repetir também está correto: siga o princípio DRY no código de teste da mesma maneira que no código comercial. Por exemplo, introduza constantes ou variáveis-membro quando for essencial para o seu código de teste que o valor delas seja o mesmo em todos os lugares.
Seu exemplo mostra como os testes tendem a repetir o código de inicialização, que é um local típico para começar com a refatoração. Portanto, considere refatorar a repetição de
em uma função separada
pois isso permitirá que você altere a assinatura do
Foo
construtor em um momento posterior com mais facilidade (porque há menos locais ondenew Foo
realmente é chamado).fonte
Na verdade ... eu não iria para os dois , mas eu vou pegar vocês sobre o seu colega de trabalho para o seu exemplo simplificado.
Em primeiro lugar, eu tendem a preferir valores inline, em vez de fazer atribuições pontuais. Isso melhora a legibilidade do código para mim, pois não preciso dividir minha linha de raciocínio em várias instruções de atribuição e, em seguida , ler a (s) linha (s) de código em que são usadas. Portanto, prefiro sua abordagem ao invés de seu colega de trabalho, com o ligeiro nitpick que
testConstructorWithLeadingAndTrailingWhitespaceInName()
provavelmente pode ser apenas:Isso me leva à nomeação. Normalmente, se o seu teste está afirmando
Exception
que será lançado, o nome do teste deve descrever esse comportamento também para maior clareza. Usando o exemplo acima, e se eu tiver espaços em branco extras no nome quando construir meuFoo
objeto? E como isso está relacionado a jogar umIllegalArgumentException
?Com base nos
null
e""
testes, estou presumindo aqui que o mesmoException
é jogado desta vez para chamargetName()
. Se esse for realmente o caso, talvez definir o nome do método de teste comocallingGetNameWithWhitespacePaddingThrows()
(IllegalArgumentException
- que eu acho que fará parte da saída do JUnit, portanto opcional) seja melhor. Se o preenchimento de espaço em branco no nome durante a instanciação também gerar o mesmoException
, pularemos a asserção também, para deixar claro que está no comportamento do construtor.No entanto, acho que há uma maneira melhor de fazer as coisas aqui quando você obtém mais permutações para entradas válidas e inválidas, ou seja, testes parametrizados . O que você está testando é exatamente isso: você está criando um monte de
Foo
objetos com diferentes argumentos do construtor e, em seguida, afirmando que ele falha na instanciação ou na chamadagetName()
. Portanto, por que não deixar o JUnit executar a iteração e o andaime para você? Você pode, em termos leigos, dizer à JUnit: "esses são os valores com os quais quero criar umFoo
objeto" e, em seguida, fazer com que seu método de teste afirmeIllegalArgumentException
nos lugares certos. Infelizmente, como o modo de teste parametrizado da JUnit não funciona bem com testes para casos bem-sucedidos e excepcionais no mesmo teste (pelo que sei), você provavelmente precisará pular um pouco de argola com a seguinte estrutura:É certo que isso parece desajeitado para o que é um teste de quatro valores simples, e não é ajudado muito pela implementação um tanto restritiva da JUnit. Nesse sentido, considero a
@DataProvider
abordagem do TestNG mais útil e, definitivamente, vale a pena examinar mais de perto se você está considerando testes parametrizados.Veja como é possível usar o TestNG para seus testes de unidade (usando Java 8
Stream
para simplificar aIterator
construção). Alguns dos benefícios são, entre outros:@DataProvider
s fornecendo diferentes tipos de entradas.fonte