Você está na página 1de 32

Apêndice B.

Teste baseado em propriedade


em Kotlin

O teste éprovavelmente um dos assuntos mais controversos na programação. Esta


controvérsia diz respeito a todos os aspectos do teste: se deve testar, quando deve
testar, o que deve testar, quanto deve testar, com que frequência deve testar,
como pode medir a qualidade dos testes, quais são as melhores métricas de cober-
tura, e mais. Mas nenhum desses assuntos pode ser considerado sozinho. E eles
são quase todos dependentes de outras questões que são debatidas com muito me-
nos frequência.

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

Elabore um conjunto de propriedades que o resultado de seu programa deve


cumprir
Escreva interfaces, depois testes e, por último, implementações para que seus
testes não dependam apenas da implementação
Use a abstração para simplificar o teste removendo partes nas quais você deve
confiar
Escreva geradores para gerar valores aleatórios para seus testes, juntamente
com os dados de acompanhamento que você pode usar para verificar as
propriedades
Configure testes que usam milhares de dados de entrada e que são executados
antes de cada compilação

OBSERVAÇÃO O código de exemplo para este apêndice está disponível em


https://github.com/pysaumont/fpinkotlin no diretório de exemplos.

Por que testes baseados em propriedades?

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:

Quando a lista estiver vazia.


Quando o primeiro parâmetro for 0 (porque você corrigiu o problema da divi-
são por 0).
Quando o primeiro elemento não é múltiplo do primeiro parâmetro.
Quando o primeiro elemento é múltiplo do primeiro parâmetro, mas não o
maior.

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.

Testar sabendo a implementação não é o ideal porque, se você escreveu a imple-


mentação, será tendencioso. Por outro lado, se você estiver escrevendo testes para
uma implementação que não escreveu, pode ser divertido tentar fazê-la falhar.

Mas o verdadeiro jogo é tentar quebrar o programa sem ver a implementação. E


como você escreve o teste e a implementação, não há melhor momento para es-
crever esse tipo de teste do que antes de começar a trabalhar na implementação.
O processo deve então ser

1. Escreva a interface.
2. Escreva os testes.
3. Escreva a implementação e verifique se os testes foram aprovados.

Vejamos cada um deles.

Escrevendo a interface

Escritaa interface é fácil. Consiste em escrever a assinatura dofunção:

fun maxMultiple(multiple: Int, list: List<Int>): Int = TODO()

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

classe interna MyKotlinLibraryKtTest: 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?

Geralmente, o que os programadores fazem é escolher alguns valores de entrada


e verificar se a saída correspondente está correta. No exemplo anterior, você está
testando a igualdade entre 14 e o resultado de passar 2 e [4, 11, 8, 2, 3,
1, 14, 9, 5, 17, 6, 7] para a função testada.

Mas como você encontrou 14 ? Aplicando à tupla de argumentos o mesmo pro-


cesso que você estará implementando para a função. Pode falhar porque os hu-
manos não são perfeitos. Mas na maioria das vezes terá sucesso porque esse teste
consiste em fazer a mesma coisa duas vezes: uma vez na sua cabeça e outra com
um programa de computador. Isso não é diferente de escrever a implementação
primeiro, depois executá-la com os parâmetros fornecidos e depois escrever o
teste para verificar se é a mesma saída.

OBSERVAÇÃO Você está testando se escreveu o que pensou ser a implementação


correta. Você não está testando se esta é uma implementação correta para o pro-
blema. Para um teste melhor, você precisa verificar algo diferente da igualdade
entre o resultado calculado pelo seu código e o calculado na sua cabeça. É disso
que se trata o teste baseado em propriedade.

Antes de ver como escrever uma implementação, vamos nos aprofundar um


pouco maisbaseado em propriedadeteste.

O que é teste baseado em propriedade?

Com base na propriedadetestar é testar se o resultado verifica algumas proprieda-


des em relação aos dados de entrada. Por exemplo, se você escrever um programa
para concatenar strings (escrito como + ), as propriedades a serem verificadas po-
dem ser

(string1 + string2).length == string1.length + string2.length


string + "" == string
"" + string == string
string1.reverse() + string2.reverse() == (string2 +
string1).reverse()

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.

Considerando seus problemas máximos múltiplos, ao escrever a interface, você


percebe que nem sempre é fácil encontrar tais propriedades. Lembre-se que você
deve encontrar um conjunto de propriedades que possam ser verificadas em to-
das as tuplas do tipo ((Int, List<Int>), Int) que serão verdadeiras se a úl-
tima Int for o resultado correto da aplicação da função ao par Int ,
List<Int> . Aqui está um lembrete da assinatura da função:

fun maxMultiple(multiple: Int, list: List<Int>): Int = TODO()

Encontrar algumas propriedades pode parecer fácil, mas encontrar propriedades


significativas é mais difícil. Idealmente, você deve encontrar o menor conjunto de
propriedades que garanta que o resultado esteja correto. Mas você deve fazer isso
sem usar a mesma lógica que usará para implementar a função. Duas maneiras
de encontrar tais propriedades incluem

Encontrando as condições que devem ocorrer


Encontrando condições que não deveriam acontecer

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

o quevocê deve fazer é encontrar uma maneira de abstrair partes do problema,


escrever funções para implementar cada parte e testá-las individualmente. Você
já sabe como fazer isso. Se você se lembrar do que aprendeu no capítulo 5, verá
que a operação é uma dobra. Você pode abstrair o problema em duas funções - a
função de dobra e a função usada como o segundo parâmetro da função de dobra:

fun maxMultiple(multiple: Int, list: List<Int>): Int =


list.fold(initialValue) { acc, int -> ... }

Aqui estou usando a fold função Kotlin padrãotrabalhando em listas Kotlin. Se


você preferir usar o List que você desenvolveu no capítulo 5, seria

fun maxMultiple(multiple: Int, list: List<Int>): Int =


list.foldLeft(initialValue) { acc -> { int -> ... } }

OBSERVAÇÃO No restante deste apêndice, uso tipos Kotlin padrão.

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:

fun maxMultiple(multiple: Int, list: List<Int>): Int =


list.fold(0, ::isMaxMultiple)

fun isMaxMultiple(acc: Int, valor: Int) = ...


Ao fazer isso, você transformou o problema em um que pode ser mais fácil de tes-
tar. A razão para isso é que você abstraiu a parte da iteração. E você não precisa
testar esta parte. Supõe-se que tenha sido testado em outro lugar. fold Se você
usar a função Kotlin, você simplesmente confia no idioma. Se você usar a fol-
dLeft funçãodo capítulo 5, você já o testou, então pode confiar nele também.

IMPORTANTE Você nunca deve testar uma função da linguagem ou de uma bibli-
oteca externa. Se você não confia, não use.

O problema que você precisa resolver agora é um pouco complicado. Digamos


que o valor para o multiple parâmetroé 2 . A implementação é então direta:

fun isMaxMultiple(acc: Int, elem: Int): Int =


if (elem / 2 * 2 == elem && elem > acc) elem else acc

O que você precisa fazer é substituir 2 pelo valor do multiple parâmetro.


Usando uma função local seria possível:

fun maxMultiple(multiple: Int, list: List<Int>): Int {


fun isMaxMultiple(acc: Int, elem: Int): Int =
if (elem / multiple * multiple == elem && elem > acc) elem else acc
return list.fold(0, ::isMaxMultiple)
}

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:

fun isMaxMultiple(multiple: Int) =


{ máx: Int, valor: Int ->
quando {
valor % múltiplo == 0 && valor > max -> valor
senão -> máximo
}
}

Agora a função pode ser testada na unidade. a isMaxMultiple funçãopode ser


usado em testes como

fun test(valor: Int, max:Int, multiple: Int): Boolean {


val result = isMaxMultiple(multiple)(max, value)
... verifique as propriedades
}

Você pode encontrar várias propriedades para testar:

result >= max


result % multiple == 0 || result == max
(result % multiple == 0 && result >= value) || result % multi-
ple != 0

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!

Dependências para teste de unidade baseado em propriedade

Paraescrever testes de unidade em Kotlin, você pode usar vários frameworks de


teste. A maioria deles depende de uma forma ou de outra de estruturas de teste
Java conhecidas, como JUnit. Para implementar o teste baseado em propriedade,
você usará o Kotlintest. O Kotlintest depende do JUnit e adiciona umDSL (Domain
Specific Languages) que permite escolher entre vários estilos de teste, incluindo
testes baseados em propriedades. Para usar Kotlintest em seu projeto, adicione a
seguinte linha ao dependencies bloco de seu arquivo build.gradle:
dependências {
...

testCompile("io.kotlintest:kotlintest-runner-junit5:${project
.rootProject.ext["kotlintestVersion"]}")
testRuntime("org.slf4j:slf4j-nop:${project
.rootProject.ext["slf4jVersion"]}")
}

Observe o uso de variáveis ​para os números de versão. Adicione essas linhas em


todos os seus scripts de construção de módulo. Para evitar ter que atualizar os nú-
meros de versão em todos os módulos, adicione a seguinte definição ao script de
construção do projeto pai:

ext["kotlintestVersion"] = "3.1.10"
ext["slf4jVersion"] = "1.7.25"

A testRuntime dependência do Slf4j não é obrigatória. Se você omitir, receberá


uma mensagem de aviso, mas os testes ainda funcionarão.

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

testessão criados no mesmo local que os testes JUnit padrão: no diretório


src/test/kotlin de cada subprojeto. Se você estiver usando o IntelliJ, clique na fun-
ção para testar, digite Alt+Enter e selecione Criar teste. Na caixa de diálogo, seleci-
one uma versão do JUnit. O IntelliJ reclamará que esta versão está ausente e pro-
porá corrigir isso. Ignore este erro e verifique se o pacote de destino está na rami-
ficação correta ( java ou kotlin , no caso de um projeto misto). Altere o nome
proposto para a classe se desejar, mas não selecione nenhuma função para testar
e clique em OK. O IntelliJ cria um arquivo vazio no pacote correto.

Cada classe de teste tem a seguinte estrutura:

// exemplo01 teste
importar io.kotlintest.properties.forAll
importar io.kotlintest.specs.StringSpec

class MyClassTest: 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

class MyClassTest: 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)
}
}
}
}
}

O nome do teste é "isMaxMultiple" . O nome é usado para exibir o resultado.


Você pode colocar vários desses blocos dentro do init bloco, mas eles devem ter
nomes diferentes. O nome é encontrado através da introspecção. Ter nomes dife-
rentes não é obrigatório em tempo de compilação, mas se vários testes forem en-
contrados com o mesmo nome em tempo de execução, você obterá um Illega-
lArgumentException com a mensagem “Não é possível adicionar teste com
nome duplicado isMaxMultiple” e nenhum teste será executado.

Todos os parâmetros do lambda passados ​como argumento da forAll funçãosão


gerados pelos geradores padrão fornecidos pelo Kotlintest. Isso pode não ser o
que vocêprecisar. Para números inteiros, o gerador padrão é fornecido pela
Gen.int() função. O teste anterior é então equivalente a

class MyKotlinLibraryTest: StringSpec() {

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)
}
}
}
}

Se você especificar um gerador explicitamente, precisará especificar todos.


Gen.int() O gerador padrão gera 0 , Int.MAX_VALUE , Int.MIN_VALUE e mui-
tos valores aleatórios Int para fazer os 1.000 testes padrão ou o número de testes
especificados. Cada gerador tenta gerar valores limite mais valores aleatórios. O
String gerador, por exemplo, sempre gera uma string vazia. Se você executar
este teste, ele falhará com a seguinte mensagem de erro:

A tentativa de encolher falhou arg -2147483648


Redução #1: 0 falha
Encolher resultado => 0

...

java.lang.AssertionError: Falha na propriedade para


Arg 0: 0 (encolhido de -2147483648)
Arg 1: 0 (encolhido de -2147483648)
Arg 2: 0 (encolhido de 2147483647)
após 1 tentativas
Causado por: esperado: verdadeiro, mas foi: falso
Esperado: verdadeiro
Real: falso
Isso significa que o teste falhou para algum valor. Nesse caso, o Kotlintest tenta re-
duzir o valor para encontrar o menor valor que faz o teste falhar.

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.

Criando seus próprios geradores personalizados

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>
}

A constants função produz o conjunto de valores limite (como


Int.MIN_VALUE , 0 e INT.MAX_VALUE para inteiros). Na maioria das vezes, você
fará com que ele retorne uma lista vazia. A random função retorna uma sequên-
cia de instâncias criadas aleatoriamente.

Criar uma instância é um processo recursivo. Se o construtor da classe da qual


você deseja gerar instâncias só aceita parâmetros para os quais já existem gerado-
res em Gen , basta usá-los. Para outros tipos de parâmetros, use geradores
personalizados.

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

classe de dados Person(nome do valor: String, idade do valor: Int)

val genPessoa: Gen<Pessoa> =


Gen.bind(Gen.string(), Gen.choose(1, 50))
{ nome, idade -> Pessoa(nome, idade) }

Usando geradores personalizados

ComoEm um caso de uso, você pode querer usar um gerador customizado se já


existir um, mas deseja evitar alguns valores específicos ou deseja incluir uma con-
dição nos valores gerados. Um caso mais incomum, mas útil, é quando você deseja
manter algum controle sobre os valores gerados.

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

Todos os caracteres no conjunto resultante devem existir na string de entrada.


Todos os caracteres na string de entrada devem existir no conjunto resultante.

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")

val map = getCharUsed(palavras)

println(mapa)
}

fun getCharUsed(palavras: List<String>) =


palavras.groupBy(::getCharMap)

fun getCharMap(s: String): Map<Char, Int> {


val resultado = mutableMapOf<Char, Int>()
for (i em 0 até s.length) {
val ch = s[i]
if (result.containsKey(ch)) {
resultado.replace(ch, resultado[ch]!! + 1)
} outro {
resultado[ch] = 1
}
}
resultado de retorno
}

Este programa retornaria o seguinte (embora em uma única linha):


{
{t=1, h=1, e=1}=[o],
{a=1, c=1, t=1}=[agir, gato, tac],
{i=1, s=1}=[é],
{b=1, a=1, t=1, s=1}=[bats, tabs],
{a=2, b=1, c=1}=[aabc, abca],
{a=1, b=2, c=1}=[abbc]
}

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.

Em vez de criar propriedades complexas, você pode criar um gerador de strings


que adiciona caracteres aleatoriamente à string gerada durante a atualização de
um mapa. Quando a geração é concluída, o gerador gera um arquivo
Pair<String, Map<Char, Int>> . Com esses dados gerados, você só tem uma
propriedade a testar: o mapa produzido pelo programa testado deve ser igual ao
mapa gerado. Aqui está um exemplo de tal gerador:

// 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()
}

fun makeMap(charList: List<Char>): Map<Char, Int> =


charList.fold(mapOf(), ::updateMap)

fun updateMap(map: Map<Char, Int>, char: Char) = quando {


map.containsKey(char) -> map + Pair(char, map[char]!! + 1)
else -> map + Pair(char, 1)
}

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

class SameLettersStringKtTest: StringSpec() {


iniciar {
"getCharUsed" {
forAll(stringGenerator) {
lista: List<Pair<String, Map<Char, Int>>> ->
getCharUsed(list.map { it.first }).keys.toSet() ==
list.asSequence().map { it.second }.toSet()
}
}
}
}

Observe que as listas são transformadas em conjuntos antes da comparação. Isso


é para levar em consideração que a lista de strings gerada pode conter várias
ocorrências da mesma string. O gerador gera uma lista de até 100 strings de até
100 caracteres. Se você deseja parametrizar esses valores, pode ficar tentado a es-
crever um novo gerador do zero; por exemplo:

// 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:

class ListGenerator<T>(private val gen: Gen<T>,


private val maxLength: Int): Gen<List<T>> {

valor privado aleatório = aleatório ()

substituir constantes divertidas(): Iterable<List<T>> =


listOf(gen.constants().toList())

override fun random(): Sequence<List<T>> = generateSequence {


val tamanho = random.nextInt(maxLength)
gen.random().take(size).toList()
}
override fun shrinker() = ListShrinker<T>()
}

Este gerador pode então ser usado no gerador de strings:

fun stringGenerator(maxList: Int,


maxString: Int): Gen<List<Pair<String, Map<Char, Int>>>> =
ListGenerator(ListGenerator(Gen.choose(32, 127), maxString), maxList)
.map { intListList ->
intListList.asSequence().map { intList ->
intList.map { n ->
n.toChar()
}
}.map { charList ->
Pair(String(charList.toCharArray()), makeMap(charList))
}.listar()
}

O comprimento da string gerada pode ser limitado adicionando um filtro ao gera-


dor de lista externo (gerando a lista de strings), mas isso seria totalmente inefici-
ente porque apenas 1/10.000.000 de strings geradas passariam pelo filtro, então a
geração seria lenta. Além disso, esse tipo de filtragem só permitiria limitar o com-
primento das strings geradas, não o comprimento da lista de strings.

Veja como este gerador pode ser usado:

class SameLettersStringKtTest: StringSpec() {

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()
}
}
}
}

Outra maneira de resolver o mesmo problema de teste é forçar a abstração ainda


mais, como você viu no início desteapêndice.

Simplificando o código por meio de mais abstrações

o getCharUsed funçãojá usa duas abstrações: a groupBy funçãoe a getChar-


Map função. Esta função pode, de fato, ser ainda mais abstraída:

// exemplo05
fun getCharUsed(palavras: List<String>): Map<Map<Char, Int>, List<String>> =
palavras.groupBy(::getCharMap)

fun getCharMap(s: String): Map<Char, Int> = s.fold(mapOf(), ::updateMap)

fun updateMap(map: Map<Char, Int>, char: Char): Map<Char, Int> =


quando {
map.containsKey(char) -> map + Pair(char, map[char]!! + 1)
else -> map + Pair(char, 1)
}
a getCharMap funçãousa duas abstrações. Uma é a fold função, que não precisa
ser testada, e a outra é a updateMap função, que é o único que precisa ser tes-
tado. Encontrar propriedades para testar é fácil:

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)

E você pode usá-lo como no seguinte teste:

// exemplo05 teste
class UpdateMapTest: StringSpec() {

valor privado aleatório = aleatório ()


valor privado min = 'a'

valor máx privado = 'z'

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:

class UpdateMapTest: StringSpec() {

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.

Em vez de inicializar o mapa com todos os caracteres possíveis vinculados à


0 ocorrência, você precisa chamar a getOrDefault funçãoem vez disso, ou a
[] sintaxe (que é equivalente à get função):
// exemplo06 teste
class UpdateMapTest: StringSpec() {

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

Elabore um conjunto de propriedades que o resultado de seu programa deve


cumprir
Escreva interfaces, depois testes e, por último, implementações, para que seus
testes não dependam da implementação
Use a abstração para simplificar o teste removendo partes nas quais você deve
confiar
Escreva geradores para gerar valores aleatórios para seu teste junto com os da-
dos que podem ser usados ​para verificar as propriedades
Configure testes usando milhares de dados de entrada e que são executados
antes de cada compilação

Você também pode gostar