JUnit 5 para desenvolvedores Kotlin
1. Introdução
O recém-lançadoJUnit 5 é a próxima versão da conhecida estrutura de teste para Java. Esta versão inclui uma série defeatures that specifically target functionality introduced in Java 8 - é principalmente construída em torno do uso de expressões lambda.
Neste artigo rápido, mostraremos quão bem a mesma ferramentaworks with the Kotlin language.
2. Testes JUnit 5 simples
Na sua forma mais simples, um teste JUnit 5 escrito em Kotlin funciona exatamente como seria de esperar. Escrevemos uma classe de teste, anotamos nossos métodos de teste com a anotação@Test, escrevemos nosso código e executamos as asserções:
class CalculatorTest {
private val calculator = Calculator()
@Test
fun whenAdding1and3_thenAnswerIs4() {
Assertions.assertEquals(4, calculator.add(1, 3))
}
}
Tudo aqui simplesmente funciona fora da caixa. Podemos fazer uso das anotações padrão@Test, @BeforeAll, @BeforeEach, @AfterEach,e@AfterAll. Também podemos interagir com os campos na classe de teste exatamente da mesma forma que em Java.
Observe que as importações necessárias são diferentes ewe doassertions using the Assertions class instead of the Assert class. Essa é uma alteração padrão para o JUnit 5 e não é específica para o Kotlin.
Antes de prosseguir, vamos mudar o nome do teste e usarbacktick identifiers em Kotlin:
@Test
fun `Adding 1 and 3 should be equal to 4`() {
Assertions.assertEquals(4, calculator.add(1, 3))
}
Agora é muito mais legível! No Kotlin, podemos declarar todas as variáveis e funções usando crases, mas não é recomendado fazer isso para casos de uso normais.
3. Asserções Avançadas
JUnit 5 adds some advanced assertions forworking with lambdas. Eles funcionam da mesma forma no Kotlin e no Java, mas precisam ser expressos de uma maneira ligeiramente diferente devido à maneira como a linguagem funciona.
3.1. Afirmando exceções
O JUnit 5 adiciona uma asserção para quando uma chamada deve lançar uma exceção. We can test that a specific call — rather than just any call in the method — throws the expected exception. Podemos até afirmar sobre a própria exceção.
Em Java, passaríamos um lambda para a chamada deAssertions.assertThrows. Fazemos o mesmo em Kotlin, mas podemosmake the code more readable anexando um bloco ao final da chamada de asserção:
@Test
fun `Dividing by zero should throw the DivideByZeroException`() {
val exception = Assertions.assertThrows(DivideByZeroException::class.java) {
calculator.divide(5, 0)
}
Assertions.assertEquals(5, exception.numerator)
}
Este códigoworks exactly the same as the Java equivalent but is easier to read, visto que não precisamos passar um lambda dentro dos colchetes onde chamamos a funçãoassertThrows.
3.2. Múltiplas afirmações
O JUnit 5 adiciona a capacidade aperform multiple assertions at the same time, e irá avaliar todos eles e relatar todas as falhas.
Isso nos permite reunir mais informações em uma única execução de teste, em vez de ser forçado a corrigir um erro apenas para acertar o próximo. Para fazer isso, chamamosAssertions.assertAll, passando um número arbitrário de lambdas.
In Kotlin, precisamos lidar com isso de forma um pouco diferente. Ofunction actually takes a varargs parameter of type Executable.
No momento, não há suporte para converter automaticamente um lambda em uma interface funcional, então precisamos fazer isso manualmente:
fun `The square of a number should be equal to that number multiplied in itself`() {
Assertions.assertAll(
Executable { Assertions.assertEquals(1, calculator.square(1)) },
Executable { Assertions.assertEquals(4, calculator.square(2)) },
Executable { Assertions.assertEquals(9, calculator.square(3)) }
)
}
3.3. Fornecedores para testes verdadeiros e falsos
Ocasionalmente, queremos testar se alguma chamada retorna um valortrue oufalse. Historicamente, calcularíamos esse valor e chamaríamosassertTrue ouassertFalse conforme apropriado. A JUnit 5 permite que seja fornecido um lambda que retorna o valor que está sendo verificado.
Kotlin allows us to pass in a lambda da mesma maneira que vimos acima para testar exceções. We can also pass in method references. Isso é especialmente útil ao testar o valor de retorno de algum objeto existente, como fazemos aqui usandoList.isEmpty:
@Test
fun `isEmpty should return true for empty lists`() {
val list = listOf()
Assertions.assertTrue(list::isEmpty)
}
3.4. Fornecedores para mensagens de falha
Em alguns casos, queremos queprovide our own error message seja exibido em uma falha de asserção em vez da falha padrão.
Freqüentemente, são strings simples, massometimes we may want to use a string that is expensive to compute. No JUnit 5, podemosprovide a lambda to compute this string, e éonly called on failure em vez de ser calculado antecipadamente.
Isso pode ajudarmake the tests run faster and reduce build times. Isso funciona exatamente da mesma maneira que vimos antes:
@Test
fun `3 is equal to 4`() {
Assertions.assertEquals(3, 4) {
"Three does not equal four"
}
}
4. Testes baseados em dados
Uma das grandes melhorias no JUnit 5 é onative support for data-driven tests. Esseswork equally well in Kotlin, e o uso de mapeamentos funcionais em coleções podemmake our tests easier to read and maintain.
4.1. Métodos TestFactory
A maneira mais fácil de lidar com testes orientados por dados é usando a anotação@TestFactory. Isso substitui a anotação@Test, e o método retorna alguma coleção de instânciasDynamicNode - normalmente criada chamandoDynamicTest.dynamicTest.
Isso funciona exatamente da mesma forma em Kotlin, e podemospass in the lambda in a cleaner way novamente, como vimos antes:
@TestFactory
fun testSquares() = listOf(
DynamicTest.dynamicTest("when I calculate 1^2 then I get 1") { Assertions.assertEquals(1,calculator.square(1))},
DynamicTest.dynamicTest("when I calculate 2^2 then I get 4") { Assertions.assertEquals(4,calculator.square(2))},
DynamicTest.dynamicTest("when I calculate 3^2 then I get 9") { Assertions.assertEquals(9,calculator.square(3))}
)
No entanto, podemos fazer melhor do que isso. Podemos facilmentebuild our list by performing some functional mapping on a simple input list of data:
@TestFactory
fun testSquares() = listOf(
1 to 1,
2 to 4,
3 to 9,
4 to 16,
5 to 25)
.map { (input, expected) ->
DynamicTest.dynamicTest("when I calculate $input^2 then I get $expected") {
Assertions.assertEquals(expected, calculator.square(input))
}
}
Imediatamente, podemos adicionar facilmente mais casos de teste à lista de entradas e ele adicionará testes automaticamente.
Também podemoscreate the input list as a class fielde compartilhá-lo entre vários testes:
private val squaresTestData = listOf(
1 to 1,
2 to 4,
3 to 9,
4 to 16,
5 to 25)
@TestFactory
fun testSquares() = squaresTestData
.map { (input, expected) ->
DynamicTest.dynamicTest("when I calculate $input^2 then I get $expected") {
Assertions.assertEquals(expected, calculator.square(input))
}
}
@TestFactory
fun testSquareRoots() = squaresTestData
.map { (expected, input) ->
DynamicTest.dynamicTest("when I calculate the square root of $input then I get $expected") {
Assertions.assertEquals(expected, calculator.squareRoot(input))
}
}
4.2. Testes parametrizados
Existemexperimental extensions to JUnit 5 para permitir maneiras mais fáceis de escrever testes parametrizados. Isso é feito usando a anotação@ParameterizedTest da dependênciaorg.junit.jupiter:junit-jupiter-params:
org.junit.jupiter
junit-jupiter-params
5.0.0
A versão mais recente pode ser encontrada emMaven Central.
A anotação@MethodSource nos permiteproduce test parameters by calling a static function que reside na mesma classe do teste. Isso é possível, masnot obvious in Kotlin. Temos queuse the @JvmStatic annotation inside a companion object:
@ParameterizedTest
@MethodSource("squares")
fun testSquares(input: Int, expected: Int) {
Assertions.assertEquals(expected, input * input)
}
companion object {
@JvmStatic
fun squares() = listOf(
Arguments.of(1, 1),
Arguments.of(2, 4)
)
}
Isso também significa que os métodos usados para produzir parâmetros devem estar todos juntos desdewe can only have a single companion object per class.
Todas as outras maneiras de usar testes parametrizados funcionam exatamente da mesma forma no Kotlin e em Java. @CsvSource é uma nota especial aqui, já que podemos usar isso em vez de@MethodSource para dados de teste simples na maioria das vezes para tornar nossos testes mais legíveis:
@ParameterizedTest
@CsvSource(
"1, 1",
"2, 4",
"3, 9"
)
fun testSquares(input: Int, expected: Int) {
Assertions.assertEquals(expected, input * input)
}
5. Testes marcados
A linguagem Kotlindoes not currently allow for repeated annotations em classes e métodos. Isso torna o uso de tags um pouco mais prolixo, pois somos obrigados awrap them in the @Tags annotation:
@Tags(
Tag("slow"),
Tag("logarithms")
)
@Test
fun `Log to base 2 of 8 should be equal to 3`() {
Assertions.assertEquals(3.0, calculator.log(2, 8))
}
Isso também é necessário no Java 7 e já é totalmente suportado pelo JUnit 5.
6. Sumário
O JUnit 5 adiciona algumas ferramentas de teste poderosas que podemos usar. Quase todos eles funcionam perfeitamente bem com a linguagem Kotlin, embora em alguns casos com sintaxe ligeiramente diferente da que vemos nos equivalentes Java.
Muitas vezes, porém, essas alterações na sintaxe são mais fáceis de ler e trabalhar ao usar o Kotlin.
Exemplos de todos esses recursos podem ser encontradosover on GitHub.