SUnit: Testes em Pharo

Traduzido de SUnit: Tests in Pharo

SUnit é uma estrutura mínima mas poderosa que apóia a criação e validação de testes. Como pode ser adivinhado pelo nome, o projeto da SUnit se concentrou nos testes de unidade, mas na verdade ela pode ser usada para testes de integração e testes funcionais também. A SUnit foi originalmente desenvolvida por Kent Beck e posteriormente ampliada por Joseph Pelrine e muitos colaboradores. SUnit é a mãe de todas as outras estruturas xUnit.

Este capítulo é o mais curto possível para mostrar que os testes são simples. Para uma descrição mais profunda da SUnit e diferentes abordagens de testes, você pode ler o livro: Learning Object-Oriented Programming, Design and TDD with Pharo disponível em http://books.pharo.org.

Neste capítulo, começamos por discutir por que testamos, e o que faz um bom teste. Em seguida, apresentamos uma série de pequenos exemplos mostrando como usar a SUnit. Finalmente, analisamos a implementação da SUnit, para que você possa entender como o Pharo usa o poder da reflexão para apoiar suas ferramentas. Note que a versão documentada neste capítulo e utilizada no Pharo é uma versão modificada da SUnit 3.3.

1.1 Introdução

O interesse em testes e desenvolvimento orientado por testes não está limitado ao Pharo. Os testes automatizados se tornaram uma marca registrada do Agile software development movement, e qualquer desenvolvedor de software preocupado em melhorar a qualidade do software faria bem em adotá-lo. De fato, os desenvolvedores em muitas linguagens passaram a apreciar o poder dos testes unitários, e agora existem versões do xUnit para cada linguagem de programação.

Nem os testes, nem a construção de conjuntos de teste são novidade.Até agora, todos sabem que os testes são uma boa maneira de detectar erros. A Programação eXtreme, ao fazer dos testes uma prática central e ao enfatizar os testes automatizados, tem ajudado a tornar os testes produtivos e divertidos, ao invés de uma tarefa que os programadores não gostam.

O SUnit é valioso porque nos permite escrever testes executáveis que são auto-verificações: o próprio teste define qual deve ser o resultado correto. Ele também nos ajuda a organizar os testes em grupos, a descrever o contexto no qual os testes devem ser executados e a executar um grupo de testes automaticamente. Em menos de dois minutos você pode escrever testes usando SUnit, portanto, em vez de escrever pequenos trechos de código em um Playground, nós o encorajamos a usar SUnit e obter todas as vantagens de testes armazenados e automaticamente executáveis.

1.2 Por que os testes são importantes

Infelizmente, muitos desenvolvedores acreditam que os testes são um desperdício de seu tempo. Afinal, eles não criam bugs, apenas outros programadores fazem isso. A maioria de nós já disse, em algum momento ou outro: Eu escreveria testes se tivesse mais tempo. Se você nunca cria um bug, e se seu código nunca será alterado no futuro, então os testes são realmente um desperdício de seu tempo. No entanto, isto provavelmente significa também que sua aplicação é trivial, ou que não é usada por você ou por qualquer outra pessoa. Pense nos testes como um investimento para o futuro: ter um conjunto de testes é bastante útil agora, mas será extremamente útil quando sua aplicação, ou o ambiente em que ela funciona, mudar no futuro.

Os testes desempenham vários papéis. Primeiro, eles fornecem documentação sobre a funcionalidade que cobrem. Esta documentação está ativa: ver os testes passarem diz-lhe que a documentação está atualizada. Segundo, os testes ajudam os desenvolvedores a confirmar que algumas mudanças que acabaram de fazer em um pacote não quebraram mais nada no sistema, e a encontrar as partes que quebram quando essa confiança se revela equivocada. Finalmente, escrever testes durante, ou mesmo antes, da programação força você a pensar sobre a funcionalidade que você deseja projetar, e como ela deve parecer ao código do cliente, ao invés de como implementá-la.

Ao escrever os testes primeiro, ou seja, antes do código, você é obrigado a declarar o contexto em que sua funcionalidade será executada, a forma como interagirá com o código do cliente e os resultados esperados. Seu código irá melhorar. Experimente-o.

Não podemos testar todos os aspectos de qualquer aplicação realista. Cobrir uma aplicação completa é simplesmente impossível e não deve ser o objetivo do teste. Mesmo com um bom conjunto de testes, alguns bugs ainda se infiltrarão na aplicação, onde podem ficar adormecidos esperando por uma oportunidade de danificar seu sistema. Se você descobrir que isto aconteceu, aproveite! Assim que você descobrir o bug, escreva um teste que o exponha, execute o teste e veja o bug falhar. Agora você pode começar a consertar o bug: os testes lhe dirão quando você terminou.

1.3 O que faz um bom teste?

Escrever bons testes é uma habilidade que pode ser aprendida através da prática. Vejamos as propriedades que os testes devem ter para obter o máximo benefício.

  • Os testes devem ser repetíveis. Você deve ser capaz de fazer um teste com a freqüência que quiser e sempre obter a mesma resposta.
  • Os testes devem ser realizados sem a intervenção humana. Você deve ser capaz de executá-los sem intervenção humana.
  • Os testes devem contar uma história. Cada teste deve cobrir um aspecto de um pedaço de código. Um teste deve agir como um cenário que você ou outra pessoa possa ler para entender um pedaço de funcionalidade.
  • Os testes devem validar um aspecto. Quando um teste falha, ele deve mostrar que um único aspecto está quebrado. De fato, se um teste cobrir vários aspectos, primeiro ele quebrará com mais freqüência e, segundo, forçará o desenvolvedor a entender um conjunto maior de opções ao consertar.

Uma conseqüência de tais propriedades é que o número de testes deve ser um tanto proporcional ao número de aspectos a serem testados: mudar um aspecto do sistema não deve quebrar todos os testes, mas apenas um número limitado. Isto é importante porque ter 100 testes fracassados deve enviar uma mensagem muito mais forte do que ter 10 testes fracassados. Entretanto, nem sempre é possível atingir este ideal: em particular, se uma mudança quebra a inicialização de um objeto, ou a configuração de um teste, é provável que cause a falha de todos os testes.

1.4 SUnit passo a passo

Escrever testes não é difícil em si mesmo. Agora vamos escrever nosso primeiro teste e mostrar-lhe os benefícios de usar SUnit. Usamos um exemplo que testa a classe Set. Faremos o seguinte:

  • Definir uma classe para agrupar os testes e se beneficiar do comportamento do SUnit. – Definir os métodos de teste.
  • Usar asserções para verificar os resultados esperados.
  • Executar os testes.

Escrevemos o código e executamos os testes à medida que avançamos.

1.5 Etapa 1: Crie a classe de teste

Primeiro você deve criar uma nova subclasse de TestCase chamada MyExampleSetTest. Adicione duas variáveis de instância para que sua nova classe se pareça com esta:

Listando 1-1 Um exemplo de teste da classe Set

TestCase subclass: #MyExampleSetTest 
    instanceVariableNames: 'full empty' 
    classVariableNames: ''  
    package: 'MySetTest'

Utilizaremos a classe MyExampleSetTest para agrupar todos os testes relacionados com a classe Set. Ela define o contexto no qual os testes serão executados. Aqui o contexto é descrito pelas duas variáveis de instância full e empty que utilizaremos para representar um conjunto ‘full’ e um ‘empty’.

O nome da classe não é crítico, mas por convenção deve terminar em Test. Se você definir uma classe chamada Pattern e chamar a classe de teste correspondente PatternTest , as duas classes serão listadas alfabeticamente juntas no navegador (assumindo que elas estejam no mesmo pacote). É fundamental que sua classe seja uma subclasse de TestCase.

1.6 Passo 2: Inicialize o contexto de teste

A mensagem TestCase >> setUp define o contexto no qual os testes serão executados, um pouco como um método de inicialização. O setUp é invocado antes da execução de cada método de teste definido na classe de teste.

Definir o método setUp como segue, para inicializar a variável empty para se referir a um conjunto vazio e a variável full para se referir a um conjunto contendo dois elementos.

MyExampleSetTest >> setUp 
    empty := Set new.  
    full := Set with: 5 with: 6

No jargão de teste, o contexto é chamado de “fixture” para o teste”.

1.7 Passo 3: Escreva alguns métodos de teste

Vamos criar alguns testes definindo alguns métodos na classe “MyExampleSetTest”. Cada método representa um teste. Os nomes dos métodos devem começar com a string test para que a SUnit os colete em conjuntos de teste. Os métodos de teste não aceitam argumentos.

Defina os seguintes métodos de teste. O primeiro teste, chamado ‘testIncludes’, testa o método includes: da classe Test. O teste diz que enviar a mensagem includes: 5 para um conjunto contendo 5 deve retornar true. Claramente, este teste se baseia no fato de que o método setUp já foi executado.

MyExampleSetTest >> testIncludes
    self assert: (full includes: 5). 
    self assert: (full includes: 6)

1.8 Passo 4: Executar os testes

O segundo teste, chamado testOccurrences, verifica que o número de ocorrências de 5 no conjunto full é igual a 1, mesmo se adicionarmos outro elemento 5 ao conjunto.

MyExampleSetTest >> testOccurrences  
    self assert: (empty occurrencesOf: 0) equals: 0. 
    self assert: (full occurrencesOf: 5) equals: 1. 
    full add: 5.  
    self assert: (full occurrencesOf: 5) equals: 1

Finalmente, testamos que o conjunto não contém mais o elemento 5 depois que o removemos.

MyExampleSetTest >> testRemove 
    full remove: 5.  
    self assert: (full includes: 6). 
    self deny: (full includes: 5)

Observe a utilização do método TestCase >> deny: para afirmar algo que não deveria ser ’true’. A aTest deny: anExpression é equivalente a aTest assert: anExpression not , mas é muito mais legível.

A maneira mais fácil de executar os testes é diretamente do system browser. Basta clicar no ícone do nome da classe, ou em um método de teste individual, e selecionar Run tests (t) ou pressionar o ícone. Os métodos de teste serão marcados em verde ou vermelho, dependendo se passam ou não (como mostrado na Figura 1-2).

Você também pode selecionar conjunto de test suites para executar, e obter um registro mais detalhado dos resultados utilizando o SUnit Test Runner, que você pode abrir selecionando World > Test Runner.

Abra um TestRunner, selecione o pacote MySetTest, e clique no botão Run Selected.

Você também pode executar um único teste (e imprimir o resumo habitual dos resultados de aprovação/reprovação) executando um ‘Print it’ no seguinte código:

(MyExampleSetTest selector: #testRemove) debug.

MyExampleSetTest debug: #testRemove

Algumas pessoas incluem um comentário executável em seus métodos de teste que permite executar um método de teste com um Do it a partir do system browser, como mostrado abaixo.

MyExampleSetTest >> testRemove 
    "self testRemove"
    full remove: 5.  
    self assert: (full includes: 6). 
    self deny: (full includes: 5)

Introduza um bug em MeuExemploSetTest >> testRemove e execute os testes novamente. Por exemplo, mude 6 para 7, como em:

MyExampleSetTest >> testRemove
    full remove: 5.  
    self assert: (full includes: 7). 
    self deny: (full includes: 5)

Os testes que não passaram (se houver) estão listados nos painéis da direita do Test Runner. Se você quiser depurar um, para ver porque falhou, basta clicar sobre o nome. Alternativamente, você pode executar uma das seguintes expressões:

(MyExampleSetTest selector: #testRemove) debug.
 
MyExampleSetTest debug: #testRemove 

1.9 Passo 5: Interpretar os resultados

O método assert: é definido na classe TestAsserter. Esta é uma superclasse de ‘TestCase’ e, portanto, de todas as outras subclasses de ‘TestCase’ e é responsável por todos os tipos de assertions possíveis. O método assert: espera um argumento booleano', geralmente o valor de uma expressão testada. Quando o argumento é true, o teste passa; quando o argumento é false`, o teste falha.

Na verdade, há três resultados possíveis de um teste: passingfailing, e  error.

  • Passing. O resultado que esperamos é que todas as assertivas do teste sejam verdadeiras, caso em que o teste passa. Visualmente as ferramentas de teste usam verde para indicar que um teste passa.
  • Failing. É óbvio que uma das afirmações pode ser falsa, fazendo com que o teste seja reprovado. As falhas nos testes estão associadas com a cor amarela.
  • Error. A outra possibilidade é que algum tipo de erro ocorra durante a execução do teste, tal como uma mensagem não reconhecida ou um erro de índice fora dos limites. Se ocorrer um erro, as afirmações no método de teste podem não ter sido executadas, portanto não podemos dizer que o teste falhou; no entanto, algo está claramente errado! Os erros são geralmente coloridos em vermelho.

Você pode tentar modificar seus testes para provocar tanto erros quanto falhas.

1.10 Usando assert:equals:

A mensagem assert:equals: oferece um relatório melhor do que assert: em caso de erro. Por exemplo, os dois testes a seguir são equivalentes. Entretanto, o segundo reportará o valor que o teste está esperando: isto facilita a compreensão da falha. Neste exemplo, supomos que aDateAndTime é uma variável de instância da classe de teste.

testAsDate  
    self assert: 
        aDateAndTime asDate 
        = ('February 29, 2004' asDate translateTo: 2 hours).
testAsDate
    self assert: aDateAndTime asDate  
        equals: ('February 29, 2004' asDate translateTo: 2 hours).

1.11 Saltar um teste

Às vezes, no meio de um desenvolvimento, você pode querer pular um teste em vez de removê-lo ou renomeá-lo para impedi-lo de funcionar. Você pode simplesmente invocar a mensagem skip de TestAsserter para ignorar o teste. Por exemplo, o teste a seguir o utiliza para definir um teste condicional.

 OCCompiledMethodIntegrityTest >> testPragmas 
    | newCompiledMethod originalCompiledMethod |
    (Smalltalk globals hasClassNamed: #Compiler) 
        ifFalse: [ ^ self skip ]. 
    ...

Isto é útil para garantir que sua execução automatizada de testes esteja relatando sucesso.

1.12 Testando exceções

SUnit fornece dois métodos importantes adicionais, TestAsserter >> should:raise: e TestAsserter >> shouldnt:raise: para testar o lançamento de exceções.

Por exemplo, você utilizaria self should: aBlock raise: anException para testar se uma exceção em particular é lançada durante a execução de aBlock. O método abaixo ilustra o uso de should:raise:.

Listagem 1-3 Teste de lançamento de erro

MyExampleSetTest >> testIllegal  
    self should: [ empty at: 5 ] raise: Error.  
    self should: [ empty at: 5 put: #zork ] raise: Error

Tente executar este teste. Note que o primeiro argumento dos métodos should: e shouldnt: é um bloco que contém a expressão a ser executada.

1.13 Executando testes por programa

Normalmente, você executará seus testes utilizando o Test Runner ou utilizando seu code browser.

Executando um único teste

Se você não quiser executar a “Test Runner UI” a partir do World menu, você pode executar Test Runner open. Você também pode executar um único teste como a seguir:

MyExampleSetTest run: #testRemove
       >>> 1 run, 1 passed, 0 failed, 0 errors 

Executando todos os testes numa classe de teste

Qualquer subclasse do TestCase responde ao conjunto de mensagens, que construirá um conjunto de testes que contém todos os métodos da classe cujos nomes começam com a string test.

Para executar os testes na suite, envie a mensagem run. Por exemplo:

MyExampleSetTest suite run

 >>> 4 run, 4 passed, 0 failed, 0 errors 

1.14 Conclusão

Este capítulo explicou porque os testes são um investimento importante no futuro de seu código. Explicamos passo a passo como definir alguns testes para a classe Set.

  • Para maximizar seu potencial, os testes unitários devem ser rápidos, repetíveis, independentes de qualquer interação humana direta e cobrir uma única unidade de funcionalidade.
    • Os testes para uma classe chamada MyClass pertencem a uma classe chamada MyClassTest, que deve ser introduzida como uma subclasse do “TestCase”.
    • Inicialize seus dados de teste em um método de setUp.
    • Cada método de teste deve começar com a palavra test. 4.
    • Utilize os métodos TestCase assert:deny: e outros para estabelecer assertivas.
    • Execute os testes!
    Várias metodologias de desenvolvimento de software, como eXtreme Programming e Test-Driven Development (TDD), defendem a escrita de testes antes de escrever o código. Isto pode parecer ir contra nossos instintos profundos como desenvolvedores de software. Tudo o que podemos dizer é: vá em frente e experimente. Descobrimos que escrever os testes antes do código nos ajuda a saber o que queremos codificar, nos ajuda a saber quando terminamos, e nos ajuda a conceituar a funcionalidade de uma classe e a projetar sua interface. Além disso, o primeiro desenvolvimento de testes nos dá a coragem de ir rápido, porque não temos medo de esquecer algo importante.

Deixe um comentário