24 dias de Hackage, 2015 - dia 15 - IOSpec: Testando IO e algumas dicas para o QuickCheck
por Franklin Chen
traduzido por Pedro Yamada
Esse é um artigo escrito por Franklin Chen e traduzido para o português. Ler original.
Índice de toda a série
O índice de toda a série está no topo do artigo para o dia 1.
Encontro HaskellBR São Paulo
Dia 15
No
dia 11
mencionei tangencialmente me sentir envergonhado de que ao contrário dos meus
outros exemplos, porque estava usando IO
, eu não comecei escrevendo testes de
cara. Eu não queria desviar do tópico em questão, que não tinha nada a ver com
IO
mas com combinadores monádicos genéricos.
Agora estou de volta para preencher esse vazio, mostrando uma forma de “mockar”
o IO
, a biblioteca
IOSpec
. Não é uma solução
perfeita e gostaria que houvesse uma forma padrão de testar IO
no ecossistema
do Haskell, ao invés dos muitos métodos diferentes disponíveis por aí
(comumente inventados de novo). Mas pelo menos ela dá um gostinho de uma forma
de atacar o problema.
Ironicamente, eu encontrei e
consertei um bug
no dia 11 no processo de escrever um teste para ele! As vezes, eu acho que sou
muito asarado, porque quando faço edições de último minúto no código sem testes
(nesse caso transformar uma expressão print
em duas), eu inevitavelmente
introduzo bugs. Isso é precisamente porque eu gosto de escrever testes. Eu não
confio em mim mesmo sem testes.
O código do dia 11 para testar
Abaixo está uma cópia da descrição introdutória da tarefa no dia 11, com a primeira versão do código em Haskell. Nós vamos testar essa primeira versão.
Vamos escrever uma função para simular um login, no qual:
- Pedimos a senha do usuário
- Entramos em um loop onde
- Uma tentativa é lida
- Se não for correta, pedimos outra senha e começamos de novo
- Se for correta saímos do loop
- Imprimimos “Congratulations!”
Mudanças necessárias para usar o IOSpec
Copiei todo o módulo do dia 11 para um novo módulo IOSpecExample.hs
, com
algumas modificações:
Modando o nome do módulo
Escondendo e adicionando imports relacionados a IO
Mudando assinaturas de tipo
Nós mudamos IO a
para IOSpec Teletype a
em todos os lugares, para
configurar o uso do simulador “teletype” para ler e imprimir caracteres, já que
acontece que nosso logIn
só usa essas operações. Se usássemos outras
operações, há outros simuladores disponíveis e o IOSpec
usa um mecânismo de tipo de “tipos de dados a la carte” para encontrar e misturar soluções.
Como IOSpec
funciona
O IOSpec
funciona fazendo tudo contruir uma estrutura de dados que é então
rodada com um Scheduler
para produzir um Effect
. Nós usamos evalIOSpec
:
Nós (os testadores) podemos então interpretar o Effect
da forma que
quisermos. Nós vamos só usar um “scheduler” básico single-threaded para esse
exemplo.
O teste
Para o nosso teste, nós vamos usar o QuickCheck para gerar input randômico do
usuário. Como estamos simulando um teletype, nós assumimos que o usuário entra
com uma stream (que representamos como uma lista) de strings que não contém
quebras de linha. Nós queremos verificar que uma delas é a palavra "secret"
,
então o output total deve ser só uma prompt introdutória, o número de prompts
mostradas se o input estiver incorreto e o “parabéns” no final.
Aqui está o spec, com código auxiliar discutido abaixo:
newtype
s para controlar o QuickCheck
Nós estamos usando um macete padrão do QuickCheck para gerar geradores randômicos de um tipo existênte para um domínio específico.
Um dos nossos tipos é NotSecretString
, representando uma linha digitada pelo
o usuário que não é igual a "secret"
; ela também não deve conter uma quebra
de linha. A instância de Arbitrary
do QuickCheck é só boilerplate para manter
as invariantes desejadas.
Nós refinamos ainda mais até o nível do Char
, para gerar NotNewlineChar
:
Uma nota sobre strings randômicas
Nós usamos um hack rápido para conseguir uma string “arbitrária”. Não é tão
arbitrária, como você pode ver a partir do
código-fonte do QuickCheck.
Para gerar strings Unicode de qualidade, use o
quickcheck-unicode
.
Uma nota sobre coerce
: é só um hack de otimização
Se ainda não viu coerce
de
Data.Coerce
,
você pode estar se perguntando do que se trata. É só um hack de eficiência para
tomar vantagem da garantia do Haskell que uma representação newtype
é
idêntica ao tipo que contém. coerce
resolve o problema de garantir que algo
que contém algo que contém (algo que contém…) alguma coisa ainda é
reconhecido como tendo a mesma representação. Para que não haja nenhum custo
durante runtime de tratar o valor como sendo de um tipo diferente. É um hack
feio mas conveniente. Se algo não pode ser forçado para essa forma, temos um
erro de tipagem, então isso é seguro.
Se você não gosta de usar coerce
, terá que usar muito map
e unwrapping e
rewrapping para converter entre todos os tipos diferentes, como:
Seria mais fácil ainda que menos embasado para mim só pensar “eles são todos só
Char
e [Char]
por baixo dos panos”. Se alguém tem guias sobre quando usar o
coerce
e quando não, ficaria feliz em as linkar aqui (e mudar meu código como
apropriado).
Interpretando um Effect
Nós interpretamos um Effect
da mesma forma de um
exemplo vindo da biblioteca.
Nós convertemos Print
e ReadChar
da forma que você esperaria se estivesse
gerando uma stream de caracteres ou lendo uma.
Receber output é direto, interpretando Done
e Print
:
Receber input é mais difícil, porque o que está acontecendo de fato é que uma
stream de caracteres de input é usada para converter um Effect
em outro. O
construtor ReadChar
tem tipo Char -> Effect a
e então quando chamado em um
Char
, retorna “a próxima coisa que acontece”.
Com esse teste, encontrei um bug no meu código, no qual o output não era o que eu esperava, por causa de um sinal de porcentagem que desapareceu depois de uma edição.
Uma nota final: há um padrão popular chamado “monad livre”
Se você já sabe sobre monads livres, você provavelmente queria me dizer “e quanto ao monad livre?” desde o começo do artigo, mas eu não queria entrar nisso ainda. Talvez depois.
Conclusão
Hoje eu apresentei uma forma de adicionar uma camada de abstração a operações
IO
de forma que possamos interpretar elas de outra forma além de apenas as
realizar; para escrever e rodar testes.
Todo o código
Todo o código para a série estará nesse repositório do GitHub.
Nota do tradutor
Se você quer ajudar com esse tipo de coisa, agora é a hora. Entre no
Slack ou no
IRC da HaskellBR e
contribua. Esse blog e outros projetos associados estão na
organização haskellbr
no GitHub e em
haskellbr.com/git.