Propriedades e testes com QuickCheck

Propriedades em vez de exemplos, geradores automáticos e leitura de contraexemplos.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Um teste de exemplo verifica reverse [1,2,3] == [3,2,1]. Uma propriedade afirma algo sobre uma família de entradas: reverse (reverse xs) == xs para listas finitas.

O QuickCheck gera casos e procura um que viole a afirmação. Os testes observam esses casos; a afirmação universal continua a exigir uma demonstração.

A primeira propriedade

Propriedades são funções que devolvem Bool, com tipos concretos para a ferramenta saber o que gerar:

import Test.QuickCheck

prop_revRev :: [Int] -> Bool
prop_revRev xs = reverse (reverse xs) == xs

No GHCi, com o pacote QuickCheck instalado:

ghci> quickCheck prop_revRev
+++ OK, passed 100 tests.

A ferramenta gerou 100 listas aleatórias1, que podem incluir listas vazias, comprimentos distintos, negativos e repetidos. A igualdade valeu em todas.

Um teste de exemplo verifica a entrada escolhida. A propriedade permite testar outras entradas automaticamente, mas só os casos gerados foram observados.

Contrato da ordenação

Uma função de ordenação deve produzir uma lista ordenada com os mesmos elementos e multiplicidades. Em [2,1,2], devolver [1,2] conserva os valores distintos, mas perde uma ocorrência. Devolver [2,1,2] conserva tudo, mas não ordena.

A idempotência f (f xs) == f xs verifica outra lei. A função f xs = [] satisfaz essa igualdade para qualquer lista finita, embora perca os dados. Para caracterizar a ordenação, testa a ordem e a conservação das multiplicidades. Quando tens uma implementação de referência independente, podes comparar os resultados.

Quando falha: o contraexemplo

Uma propriedade falsa dá um contraexemplo reduzido. Afirma que ordenar é a identidade:

import Data.List (sort)

prop_sortId :: [Int] -> Bool
prop_sortId xs = sort xs == xs
ghci> quickCheck prop_sortId
*** Failed! Falsifiable (after 3 tests):
[1,0]

O QuickCheck responde que a propriedade é falsificável e mostra a testemunha [1,0]; o shrinking tenta reduzir a entrada encontrada conservando a falha, embora as reduções disponíveis não garantam o mínimo global. O número de testes varia de corrida para corrida, mas a lista curta fora de ordem é o essencial. Perante uma falha, compara o contraexemplo com o contrato antes de mudar a implementação; neste caso, a ordenação está certa e a propriedade é que está errada:

A propriedade idempotente da ordenação passa:

prop_sortSort :: [Int] -> Bool
prop_sortSort xs = sort (sort xs) == sort xs
ghci> quickCheck prop_sortSort
+++ OK, passed 100 tests.

Geradores e condições

Por omissão, o QuickCheck gera valores arbitrários do tipo pedido: a distribuição e os limites dependem da instância Arbitrary e do parâmetro de tamanho. Quando a propriedade só faz sentido sob uma condição, impõe-na com ==>:

prop_divMod :: Integer -> Integer -> Property
prop_divMod x y = y /= 0 ==> (x `div` y) * y + (x `mod` y) == x

Os casos com y == 0 são descartados e não contam como testes bem-sucedidos. Se a pré-condição for rara, a ferramenta pode desistir sem obter a amostra pedida. Um gerador que constrói entradas válidas evita gastar a maior parte das tentativas em descartes.

Um gerador à medida é uma ação que produz valores com a forma que queres. Para testar que o máximo de um par ordenado é o segundo elemento, gera pares já ordenados:

parOrdenado :: Gen (Int, Int)
parOrdenado = do
  x <- arbitrary
  y <- arbitrary
  return (min x y, max x y)

prop_maxDireita :: Property
prop_maxDireita = forAll parOrdenado $ \(x, y) -> max x y == y

Geramos dois inteiros e colocamos o menor primeiro, por isso todas as tentativas produzem um par válido que o forAll usa para testar a propriedade. suchThat é outra possibilidade, mas repete tentativas até satisfazer a condição, e uma condição impossível ou muito rara pode impedir a geração de terminar.

forAll não reduz automaticamente entradas de um gerador próprio; usa forAllShrink com um redutor que preserve a condição quando precisares desse comportamento.

O mesmo princípio aplica-se a insert x xs: se o contrato pressupõe xs ordenada, ordena a lista gerada antes de testar a inserção, para cada tentativa exercitar uma entrada válida.

Um contraexemplo que cabe no papel

A lei reverse (xs ++ ys) == reverse xs ++ reverse ys é falsa. Com xs=[1] e ys=[2], o lado esquerdo é [2,1] e o direito [1,2]. A lei correta troca a ordem das partes: reverse (xs ++ ys) == reverse ys ++ reverse xs. Este exemplo distingue a ordem de concatenação sem depender de acaso ou de um grande conjunto de dados.

Para justificar uma propriedade, identifica ainda o domínio. A igualdade envolvendo length pressupõe listas finitas; leis aritméticas em Int podem ser afetadas por overflow. Usar Integer numa propriedade de aritmética exata evita confundir uma falha matemática com o limite da representação.

Notas de rodapé

  1. Os 100 casos são a omissão da ferramenta; podes pedir mais com argumentos de configuração. Voltar

Exercícios

Trocar a ordem das partes

Qual lei vale para listas finitas?

A tua resposta

Pista

Usa listas com um elemento distinto em cada parte.

Solução

A ordem correta é reverse ys antes de reverse xs.

  • reverse (xs++ys) == reverse ys ++ reverse xs. Inverter a concatenação troca a ordem das partes.
  • reverse (xs++ys) == reverse xs ++ reverse ys. Falha com xs=[1],ys=[2].
  • reverse xs == xs. Só vale para listas que coincidem com a sua inversão.

Erros frequentes

Não uses listas iguais nos dois argumentos, que escondem a troca.

Voltar à explicação
Uma propriedade insuficiente

Por que f(f xs)=f xs não prova que f ordena corretamente?

Pista

Experimenta uma função constante.

Solução

f xs=[] é idempotente mas perde os dados. Uma ordenação deve produzir uma lista ordenada com as mesmas ocorrências da entrada.

Confere a tua resposta:

  • Dou uma função errada que satisfaz a lei.
  • Exijo conservação de elementos e multiplicidades.
  • Exijo ordem do resultado.

Erros frequentes

Testar uma lei verdadeira não significa que ela caracterize todo o contrato.

Voltar à explicação
Testar uma ordenação contra implementações erradas

Escreve prop_ordena :: ([Int] -> [Int]) -> Property. Gera listas com valores entre -10 e 10, incluindo a possibilidade de lista vazia. A propriedade deve exigir saída ordenada e os mesmos elementos com as mesmas multiplicidades.

Mostra um contraexemplo para f xs = [] e outro para f xs = xs. Compara com sort. Se o gerador for usado com forAll, há redução automática dos contraexemplos?

Pista
Testa a função recebida, não apenas a função de referência.
Solução

Podemos comparar multiplicidades com uma referência independente e comparar vizinhos para a ordem:

import Data.List (sort)
import Test.QuickCheck

ordenada :: [Int] -> Bool
ordenada xs = and (zipWith (<=) xs (drop 1 xs))

prop_ordena :: ([Int] -> [Int]) -> Property
prop_ordena f = forAll genLista $ \xs ->
  let ys = f xs
  in ordenada ys && sort ys == sort xs
  where
    genLista :: Gen [Int]
    genLista = listOf (choose (-10,10))

sort ys == sort xs compara todas as ocorrências, incluindo valores fora do intervalo que uma implementação errada pudesse inventar. A função constante falha para [0]: [] perde esse elemento. A identidade falha para [1,0]: conserva os dados, mas a saída não está ordenada. sort satisfaz ambas as partes nos casos gerados.

forAll não aplica automaticamente um redutor ao gerador próprio. Para reduzir estas listas, usa forAllShrink genLista shrink; verifica sempre que o redutor conserva as condições exigidas pelo problema. Nenhuma das duas formas prova a lei para todas as listas.

Confere a tua resposta:

  • Gero listas de Int no intervalo dado.
  • Testo ordem e multiplicidades da saída de f.
  • Dou contraexemplos distintos para perda e falta de ordenação.

Erros frequentes

Idempotência passa nas duas funções erradas. Contar só valores do gerador pode ignorar uma saída inventada.
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.