Escolar Documentos
Profissional Documentos
Cultura Documentos
Neste apêndice, abordo como escrever testes eficazes e como tornar seu pro-
grama mais testável usando testes baseados em propriedades. Você aprenderá
como
Por poucotodo programador concorda que algum tipo de teste de unidade é ne-
cessário, embora o teste certamente não seja a maneira ideal de garantir que os
programas estejam corretos. Os testes não podem provar que um programa está
correto.
Os testes com falha provam que o programa provavelmente está incorreto (ape-
nas 'provavelmente' porque pode ser um problema com o próprio teste!). Mas tes-
tes bem-sucedidos certamente não provam que o programa testado está correto.
Eles apenas provam que você não foi esperto o suficiente para encontrar os bugs.
Se você colocar o mesmo esforço no desenvolvimento de seus testes como faz
para escrever seus programas, ainda não seria suficiente. E ainda assim, você ge-
ralmente investe muito menos esforço em testes do que em escrever programas.
A melhor maneira de proceder seria provar que seus programas estão corretos.
Isso é o que a programação funcional tenta fazer, mas raramente é totalmente
possível. O programa ideal é aquele para o qual existe apenas uma implementa-
ção possível. Pode parecer loucura, mas se você pensar bem, vai perceber que o
risco de bugs é proporcional ao número de implementações possíveis para seus
programas. Você deve, portanto, tentar minimizar o número de possíveis
implementações.
Uma maneira de fazer isso é por meio da abstração. Tomemos o exemplo de um
programa que encontra todos os elementos em uma lista de inteiros que são múl-
tiplos de um determinado e obtém o máximo deles. Na programação tradicional,
você pode fazer isso com um loop indexado como no exemplo a seguir (os erros
são intencionais):
// exemplo00
fun maxMultiple(multiple: Int, list: List<Int>): Int {
var resultado = 0
for (i em 1 até list.size) {
if (lista[i] / múltiplo * múltiplo == lista[i] && lista[i] > resultado) {
resultado = lista[i]
}
}
resultado de retorno
}
Como você testaria tal programa? Claro, você vê os bugs, então você começaria
corrigindo-os antes de escrever os testes. Mas se você fosse o responsável por es-
ses bugs, não os veria. Em vez disso, você deseja testar a implementação de valo-
res limite. Sendo os dois parâmetros um número inteiro e uma lista, você prova-
velmente deseja testar 0 como int parâmetro ou uma lista vazia. A primeira con-
dição causaria uma java.lang.ArithmeticException: / by zero exceção. A
segunda condição produziria um resultado de 0.
Observe que passar 0 e uma lista vazia para essa implementação não causaria
uma exceção, mas produziria 0 como resultado.
Outro bug neste exemplo é que o primeiro elemento é ignorado. Na maioria dos
casos, isso não causará nenhum problema:
Claro, agora você sabe o que fazer: escreva um teste com um primeiro parâmetro
diferente de zero e uma lista na qual o primeiro elemento é o maior múltiplo. Mas
espere... se você fosse inteligente o suficiente para descobrir isso, não teria come-
tido esse erro, então testar, neste caso, não é útil.
Alguns programadores dizem que os testes devem ser escritos antes das imple-
mentações. Embora eu concorde totalmente com isso, como isso ajudaria aqui?
Você pode escrever um teste para 0 e listas vazias, mas como saber com antece-
dência que deve escrever um teste com um primeiro parâmetro diferente de zero
e uma lista na qual o primeiro elemento é o maior múltiplo? Você só pode fazer
isso quando conhece a implementação.
1. Escreva a interface.
2. Escreva os testes.
3. Escreva a implementação e verifique se os testes foram aprovados.
Escrevendo a interface
Escrevendo os testes
Agoravocê tem que escrever os testes. Para um teste tradicional, você pode come-
çar com
// exemplo00 teste
importar io.kotlintest.shouldBe
importar io.kotlintest.specs.StringSpec
iniciar {
"maxMultiple" {
val múltiplo = 2
val lista = listaOf(4, 11, 8, 2, 3, 1, 14, 9, 5, 17, 6, 7)
maxMultiple(vários, lista).shouldBe(14)
}
}
}
Você pode escrever quantos testes quiser usando valores específicos. Você irá, é
claro, tratar todos os valores especiais como 0 e lista vazia, mas o que mais você
pode fazer?
Testar essas propriedades seria suficiente para garantir que o programa está cor-
reto. (Os três primeiros nem são necessários.) Com isso, você pode verificar mi-
lhões de strings geradas aleatoriamente sem se preocupar com o resultado real. A
única coisa que importa é que as propriedades sejam verificadas.
A primeira coisa que você precisa fazer antes mesmo de começar a codificar (seja
o programa principal ou os testes) é pensar no problema em termos das proprie-
dades que devem ser verificadas ao considerar a entrada e a saída da função.
Você pode ver imediatamente que uma função sem efeitos colaterais é muito mais
fácil de lidar nesse caso.
Por exemplo, ao iterar sobre elementos na lista, você não deve encontrar um que
seja múltiplo do primeiro parâmetro e maior que o resultado. O problema é que
ao testar isso, você provavelmente estaria usando o mesmo algoritmo da imple-
mentação da função, então não seria mais relevante do que chamar a função duas
vezes e verificar se você obtém o mesmo resultado em cada chamada! A solução
aqui éabstração.
Abstração e testes baseados em propriedades
Agora você precisa testar a função usada para a dobra. Como consequência, você
não estará usando uma função anônima como a lambda do exemplo anterior,
mas algo assim:
IMPORTANTE Você nunca deve testar uma função da linguagem ou de uma bibli-
oteca externa. Se você não confia, não use.
A dificuldade com tal implementação é que ela não resolve seu problema de teste.
Mais uma vez, a abstração é o caminho a percorrer. Você pode abstrair o multi-
ple parâmetrodo seguinte modo:
// exemplo01
fun isMaxMultiple(multiple: Int) =
{ máx: Int, valor: Int ->
quando {
valor / múltiplo * múltiplo == valor && valor > max -> valor
senão -> máximo
}
}
Abstração é algo que você sempre fará. Por exemplo, aqui é absolutamente natu-
ral para todos os programadores abstrair value / multiple * multiple ==
value em uma chamada para a rem função, representado em notação infixa pelo
% operador:
Você também pode encontrar outras propriedades. Idealmente, você deve termi-
nar com o conjunto mínimo de propriedades que garante que o resultado seja
correto. Na realidade, não importa muito se você tiver alguma redundância. Pro-
priedades redundantes são inofensivas (a menos que demorem muito para serem
verificadas). As propriedades ausentes são mais um problema. Mas um grande be-
nefício é que agora você pode testar seuteste!
testCompile("io.kotlintest:kotlintest-runner-junit5:${project
.rootProject.ext["kotlintestVersion"]}")
testRuntime("org.slf4j:slf4j-nop:${project
.rootProject.ext["slf4jVersion"]}")
}
ext["kotlintestVersion"] = "3.1.10"
ext["slf4jVersion"] = "1.7.25"
Kotlintest usa Slf4j para registro e é, por padrão, detalhado. Se você não deseja re-
gistrar para testes, use a slf4j-nop dependênciapara suprimir erros de registro
e registro. Você também pode escolher qualquer implementação de log que dese-
jar e fornecer a configuração correspondente para atender às suas necessidades.
Escrevendo testes baseados em propriedades
// exemplo01 teste
importar io.kotlintest.properties.forAll
importar io.kotlintest.specs.StringSpec
iniciar {
"teste1" {
para todos {
// verifique as propriedades aqui
}
}
}
}
Como alternativa, você pode especificar o número de testes a serem executados (o
padrão é 1.000):
importar io.kotlintest.properties.forAll
importar io.kotlintest.specs.StringSpec
iniciar {
"teste1" {
forAll(3000) {
// verifique as propriedades aqui
}
}
}
}
O último parâmetro da forAll funçãoé uma função que deve retornar tru-
e para que o teste passe. Esta função pode receber vários argumentos que são ge-
rados automaticamente. Por exemplo, você pode usar
// exemplo01 teste
class MyKotlinLibraryTest: StringSpec() {
iniciar {
"isMaxMultiple" {
forAll { múltiplo: Int, max:Int, valor: Int ->
isMaxMultiple(multiple)(max, value).let { resultado ->
resultado >= valor
&& resultado % múltiplo == 0 || resultado == máximo
&& ((resultado % múltiplo == 0 && resultado >= valor)
|| resultado % múltiplo != 0)
}
}
}
}
}
iniciar {
"isMaxMultiple" {
forAll(Gen.int(), Gen.int(), Gen.int())
{ múltiplo: Int, max:Int, valor: Int ->
isMaxMultiple(multiple)(max, value).let { resultado ->
resultado >= valor
&& resultado % múltiplo == 0 || resultado == máximo
&& ((resultado % múltiplo == 0 && resultado >= valor)
|| resultado % múltiplo != 0)
}
}
}
}
...
Isso é útil para testes que falham quando um valor é muito alto. Não seria prático
apenas relatar a falha. Você precisará tentar o valor mais baixo para encontrar o
ponto de falha. Aqui você pode ver que todos os argumentos diminuem para 0,
sem fazer o teste passar. Na verdade, o teste sempre falha se o primeiro argu-
mento for 0 porque você não pode dividir por 0.
O que você precisa aqui é um gerador de inteiros não nulos. a Gen interfaceofe-
rece muitos geradores para todos os tipos de usos. Por exemplo, números inteiros
positivos podem sergerado com Gen.positiveIntegers() :
// exemplo02 teste
class MyKotlinLibraryTest: StringSpec() {
iniciar {
"isMaxMultiple" {
forAll(Gen.Inteiros positivos(), Gen.int(), Gen.int())
{ múltiplo: Int, max:Int, valor: Int ->
isMaxMultiple(multiple)(max, value).let { resultado ->
resultado >= valor
&& resultado % múltiplo == 0 || resultado == máximo
&& ((resultado % múltiplo == 0 && resultado >= valor)
|| resultado % múltiplo != 0)
}
}
}
}
E agora o teste é um sucesso.
Kotlintestoferece muitos geradores para a maioria dos tipos padrão (números, bo-
oleanos e strings), bem como coleções. Mas às vezes você precisará criar seu pró-
prio gerador. O caso padrão é quando você deseja gerar instâncias de suas pró-
prias classes. O processo é simples. Você precisa implementar a seguinte
interface:
interface Gen<T> {
fun constants(): Iterable<T>
fun random(): Sequência<T>
}
Tanto quanto possível, você deve evitar criar um gerador do zero. Todos os obje-
tos de dados são composições de números, strings, enumerações booleanas e, re-
cursivamente, outros objetos. No final, você pode gerar qualquer objeto combi-
nando os geradores existentes. A melhor maneira de combinar geradores é
usando uma de suas bind funções, talComo
Por exemplo, se você precisa escrever um programa que calcula o conjunto de ca-
racteres usados em uma string, como você verificaria se o resultado de uma string
gerada aleatoriamente está correto? Você pode criar critérios para testar, como
Isso é bastante simples. Mas imagine que ao invés de um conjunto, sua função te-
nha que produzir um mapa com cada caractere usado na string como uma chave
no mapa e o número de ocorrências desse caractere como o valor. Por exemplo,
imagine que você precise escrever um programa que receba uma lista de strings e
agrupe aquelas que são compostas exatamente pelo mesmo conjunto de caracte-
res. Você poderia escrevê-lo como
// exemplo03
fun main(args: Array<String>) {
val palavras = listOf("o", "agir", "gato", "é", "morcegos",
"tabs", "tac", "aabc", "abbc", "abca")
println(mapa)
}
Como você testaria seu programa? Você pode experimentá-lo em várias listas de
palavras, mas isso não garante que funcionará em todas as combinações. É me-
lhor usar testes baseados em propriedades, gerando listas de strings aleatórias e
verificando algumas propriedades. Mas como elaborar um bom conjunto de pro-
priedades? Essas propriedades seriam complexas.
// exemplo03 teste
val stringGenerator: Gen<List<Pair<String, Map<Char, Int>>>> =
Gen.list(Gen.list(Gen.choose(97, 122)))
.map { intListList ->
intListList.asSequence().map { intList ->
intList.map { n ->
n.toChar()
}
}.map { charList ->
Pair(String(charList.toCharArray()),
makeMap(charList))
}.listar()
}
Este gerador produz uma sequência de lista de pares de uma string aleatória e um
mapa contendo todos os caracteres da string como chaves e o número de ocorrên-
cias de cada um como valores. Observe que os mapas não são obtidos analisando
as strings. As strings são construídas a partir dos mesmos dados dos mapas.
Usando este gerador, o getCharUsed programa pode ser facilmente testado por-
que você só tem uma propriedade para testar. Essa propriedade ainda é um
pouco complexa de ler, mas pelo menos você não precisa de um conjunto enorme
de propriedades e tem certeza que o teste é exaustivo:
// exemplo03 teste
importar io.kotlintest.properties.forAll
importar io.kotlintest.specs.StringSpec
// exemplo04 teste
class StringGenerator(private val maxList: Int,
private val maxString: Int):
Gen<List<Pair<String, Map<Char, Int>>>> {
sobrepor
fun constants(): Iterable<List<Pair<String, Map<Char, Int>>>> =
listOf(listOf(Pair("", mapOf())))
sobrepor
fun random(): Sequence<List<Pair<String, Map<Char, Int>>>> =
Random().let { random ->
gerarSequência {
(0 até random.nextInt(maxList)).map {
(0 até random.nextInt(maxString))
.fold(Pair("", mapOf<Char, Int>())) { pair, _ ->
(random.nextInt(122 - 96) + 96).toChar().let { char ->
Pair("${pair.first}$char", updateMap(pair.second, char))
}
}
}
}
}
Você pode fazer o mesmo para os valores máximo e mínimo dos caracteres nas
strings geradas. Mas uma maneira melhor de fazer isso é criar um gerador de
lista modificado:
iniciar {
"getCharUsed" {
forAll(stringGenerator(100, 100)) {
lista: List<Pair<String, Map<Char, Int>>> ->
getCharUsed(list.map { it.first }).keys.toSet() ==
list.asSequence().map { it.second }.toSet()
}
}
}
}
// exemplo05
fun getCharUsed(palavras: List<String>): Map<Map<Char, Int>, List<String>> =
palavras.groupBy(::getCharMap)
Para qualquer char e map contendo isso char como uma chave,
getCharMap(map, char)[char] deve ser igual a map[char] + 1 .
Para qualquer char e map não contendo isso char como uma chave,
getCharMap(map, char)[char] deve ser igual a 1 .
Para qualquer char e map , o resultado da remoção da char chave de
getCharMap(map, char)[char] e de map deve ser igual. (Esta propriedade
verifica se nenhuma outra modificação é feita no mapa.)
Para este teste, você precisará gerar mapas aleatórios preenchidos com dados ale-
atórios. Você pode escrever o seguinte MapGenerator (gerando caracteres no
[a..z] intervalo:
// exemplo05 teste
fun mapGenerator(min: Char = 'a', max: Char = 'z'): Gen<Map<Char, Int>> =
Gen.list(Gen.choose(min.toInt(), max.toInt())
.map(Int::toChar)).map(::makeMap)
// exemplo05 teste
class UpdateMapTest: StringSpec() {
iniciar {
"getCharUsed" {
forAll(mapGenerator()) { map: Map<Char, Int> ->
(random.nextInt(max.toInt() - min.toInt())
+ min.toInt()).toChar().let {
if (map.containsKey(it)) {
updateMap(map, it)[it] ==
map[it]!! +1
} outro {
updateMap(map, it)[it] ==
1
} && updateMap(map, it) - it == map - it
}
}
}
}
}
Observe que o princípio da abstração máxima também se aplica aos testes. Uma
coisa que pode ser abstraída é a geração de cada um Char . a Gen interfacenão
oferece um Char gerador, mas você pode facilmente criar um. E como você pode
querer gerar apenas alguns caracteres específicos, também pode abstrair a sele-
ção de caracteres em uma função separada:
// exemplo06 teste
fun charGenerator(p: (Char) -> Boolean): Gen<Char> =
Gen.choose(0, 255).map(Int::toChar).filter(p)
Agora você pode reescrever seu teste de uma maneira muito mais limpa:
iniciar {
"getCharUsed" {
forAll(MapGenerator,
charGenerator(Char::isLetterOrDigit)) {
mapa: Mapa<Char, Int>, char ->
if (map.containsKey(char)) {
updateMap(map, char)[char] == map[char]!! +1
} outro {
updateMap(mapa, char)[char] == 1
}
&& updateMap(mapa, char) - char == mapa - char
}
}
}
}
Mas você ainda pode simplificar o código usando um valor padrão para o mapa.
Quando você consulta o mapa em busca de um caractere que não foi encontrado,
o valor deve ser 0 . Isso faz sentido porque é o número de ocorrências.
iniciar {
"getCharUsed" {
forAll(MapGenerator,
charGenerator(Char::isLetterOrDigit)) {
mapa: Mapa<Char, Int>, char ->
updateMap(map, char)[char] == map.getOrDefault(char, 0) + 1
&& updateMap(mapa, char) - char == mapa - char
}
}
}
}
Agora você reduziu a peça para testar ao mínimo e configurou uma maneira po-
derosa de testar os resultados. Ao executar o teste enquanto gera milhares de va-
lores em cada compilação, você logo terá testado seu programa com milhões de
casos de dados. Porque tanto o programa principal quanto o teste usam a upda-
teMap função, você provavelmente deve colocá-lo em um módulo comum.
Resumo
Neste apêndice, você aprendeu como escrever testes eficazes e tornar seu pro-
grama mais testável usando testes baseados em propriedades. você temaprendi-
doComo aspara