Alura > Cursos de Programação > Cursos de Clojure > Conteúdos de Clojure > Primeiras aulas do curso Clojure: explorando testes

Clojure: explorando testes

Boundary e Category partition - Introdução

Boas vindas a mais um curso de Clojure: explorando testes!

Neste curso, falaremos e escreveremos bastante sobre testes, fazendo comentários a medida que exploramos cada um definindo o modelo e a lógica usando ou não esquema, redefinindo e implementando novos códigos ao longo do trabalho.

Neste processo, veremos diversas maneiras de tentar garantir um comportamento de sucesso em nosso sistema, garantindo que o codigo que implementamos não apresente bugs e que funcione bem para os casos elaborados.

Falaremos sobre Test Driven Development (TDD) e Test Driven Design, bem como Boundary Tests e separação de testes por categoria ou Category Partition. Veremos sobre a validação de testes através de esquemas e adição de certos contratos antes e depois da execução de uma função.

Tentaremos garantir o comportamento de diversas maneiras possíveis, usando ferramentas nativas de Clojure ou Java, biblioteca da parte de testes da linguagem, esquema e trechos de código para compor uma bateria de testes, sejam eles automatizados, esquemas como validações na hora de invocar funções ou ainda contratos em execução ativadas ou não, sempre visando a melhoria do código.

Abordaremos criticamente as maneiras como realizamos e pensamos testes, como temos ilusão do funcionamento destes e de que funções são mais simples do que imaginamos deixando de testar erroneamente, abrindo espaço para bugs.

Portanto, tem muita coisa para vermos sobre testes. Vamos lá!

Boundary e Category partition - Boundary tests e testes em clojure

Dando início ao curso, abra o IntelliJ IDEA e crie um novo projeto clicando em "Create New Project" de Clojure com Leiningen. Em seguida, clique em "Next", depois selecione a pasta de sua preferência em "Project location" e nomeie como "hospital". Clique em "Finish" para gerar o novo projeto.

Na aba lateral, encontramos os diretórios criados. Dê duplo clique em "hospital > src > hospital > core.clj" e abra a nova aba de hospital.core. Nesta, a definição de foo não será usada, logo podemos apagar deixando apenas o namespace.

Criaremos alguns modelos que representam nosso hospital clicando com o botão direito sobre "src > hospital" para selecionar "New > File" e nomear como model.clj; este é um padrão de nomenclatura em inglês bastante comum em empresas para encontrar modelos, arquivos de bancos e etc. No novo arquivo, declare o namespace (ns hospital.model).

Funções de lógica que trabalham com nossos modelos são criados em um outro novo arquivo chamado logic.clj da mesma forma que fizemos com o anterior. Na nova aba, declare o namespace (ns hospital.logic).

Queremos criar uma função de lógica que permite adicionar pessoas em uma fila de hospital; então estabelecemos o tamanho da fila com o número máximo de 5 vagas.

Comece definindo a função cabe-na-fila? com defn para perguntar se cabem mais pessoas; se houver menos de 5 pessoas, é possível a entrada de mais gente, e se houver 5 ou mais, não há como incluir mais pacientes.

Para isso, escreva a implementação de cabe-na-fila? recebendo um hospital e o departamento. Pegue a fila do departamento deste hospital com ->. Como departamento é uma keyword, chamamos este no hospital passando-o como parâmetro para o departamento.

Para saber quantas pessoas estão dentro do departamento, use count para verificar com get hospital e departamento. Agora, queremos saber se há 5 pessoas nesta fila, pois se houver, não cabem mais pacientes. Portanto, deve ser not= para 5.

Aparentemente há bug em nosso código, mas descobriremos mais adiante. Comente a linha que contém count por enquanto.

A partir desta lógica, geraremos testes para garantir que o sistema funcione. Uma maneira é realmente escrever o código antes, e outra é escrever os testes automatizados antes do código.

(ns hospital.logic)

(defn cabe-na-fila?
    [hospital departamento]
    ;(count (get hospital departamento))
    (-> hospital
        departamento
        count
        (not= 5)))

No desenvolvimento voltado a testes, aparecem alguns termos como Test Driven Development ou Test Driven Design (TDD), significando que os testes indicam o caminho do código, diferente do que estamos fazendo até então, pois escrevemos a função cabe-na-fila? antes, como acreditamos que seria chamada.

O estudo de autoria do Maurício Aniche, instrutor de alguns cursos da Plataforma Alura mostra qual caminho é melhor para ter menos bugs acerca dessa temática. Nesta pesquisa, é mostrado que a qualidade do código não é tão influenciada pela utilização de testes antes da implementação quanto é influenciada pela própria experiência da desenvolvedora ou desenvolvedor. Portanto, utilizar TDD e Test First não necessariamente garante o bom funcionamento do programa.

Então, a maneira como implementamos o código apresenta bug, mas ter escrito o teste antes também não garantiria a qualidade. De qualquer forma, faremos o teste para avaliar o resultado.

Em Clojure, quando criamos um projeto com Leiningen já há um diretório chamado "test" contendo "hospital", o qual possui um arquivo de namespace chamado core_test.clj que receberá os testes do módulo core.clj.

Abra o core_test.clj para ver que já há um conteúdo escrito. Apague o bloco de deftest, pois não o usaremos, deixando apenas o namespace e os recursos importados com :require.

Na pasta "hospital" dentro de "test", clique com o botão direito para selecionar "New > File" e gerar um novo arquivo de teste da lógica. Atente ao uso padrão de underline ao invés de ponto na nomenclatura neste caso, ficando logic_test.clj. Já no namespace, use hífen na declaração, escrevendo (ns hospital.logic-test.

De volta à aba h.core-test, temos alguns imports para fazer que devem ser observados neste arquivo; é comum usar :require no namespace de teste e naquele a ser testado.

Em h.logic-test, ao invés de ficarmos inserindo o prefixo da biblioteca do recurso que iremos utilizar constantemente no programa, importamos usando :require de clojure.test para logo em seguida referenciar pelo nome com :refer para todo conteúdo escrevendo, :all.

Ao clicar em clojure.test, acessamos todos os recursos defs presentes que podemos utilizar.

Então, definiremos um teste em h.logic-test: com deftest, defina cabe-na-fila?. Em seguida, use testing - também presente em clojure.test - para testar uma string escrevendo "Que cabe na fila". Dentro, queremos que seja igual a 1 e 1 com is para um primeiro teste simples que deveria funcionar.

Em nosso caso, rodamos project.clj clicando com o botão direito sobre este na lista lateral para escolher "Run 'REPL for hospital'". Assim que o REPL for carregado, podemos rodar os testes.

(ns hospital.logic-test
    (:require [clojure.test :refer :all]))

(deftest cabe-na-fila?
    (testing "Que cabe na fila"
        (is (= 1 1))))

Encontramos os comandos de REPL tanto em "Run" quanto em "Tools" na barra superior de opções, mas para nossa intenção acesse "Tools > REPL > Run tests in current NS in REPL" e observe o retorno na janela lateral que apresenta um teste sendo rodado e uma asserção relativa ao is sem alertas de falha, significando que nosso programa está funcionando como esperado.

Agora, podemos implementar o teste real. Pegue o hospital que tem uma fila vazia escrevendo :espera seguido de [] entre colchetes no lugar de 1. Tentando colocar um paciente nesta fila, pegue o hospital novamente e chame cabe-na-fila? para :espera, já devolvendo verdadeiro ou falso, logo retire = da sentença.

Desta forma, deveria caber mais pacientes. Rode no REPL clicando em "Tools > REPL > Run Tests in current NS in REPL" novamente.

(ns hospital.logic-test
    (:require [clojure.test :refer :all]))

(deftest cabe-na-fila?
    (testing "Que cabe na fila"
        (is (cabe-na-fila? {:espera []}, :espera))))

O erro apresentado acima de toda a exceção é de aridade, ou seja, chamamos a função errada. Se observarmos em hospital.logic, a função cabe-na-fila? recebe dois argumentos: hospital e departamento.

De volta ao h.logic-test, clique sobre cabe-na-fila? com a tecla "Ctrl" ou "Command" apertada para ver sua definição. Com isso, o sistema não nos direciona para o arquivo de lógica e sim para a definição nesta mesma aba.

Isso acontece porque deftest define um símbolo. Logo, definimos um símbolo chamado cabe-na-fila? e o invocamos no bloco, mas queremos que seja invocada a função do hospital.logic.

Para isso, precisamos importar também hospital.logic referindo tudo que estiver público com :refer :all para podermos acessar todas as funções e realizar nosso teste, evitando a necessidade de inserir prefixo constantemente a cada invocação.

(ns hospital.logic-test
    (:require [clojure.test :refer :all]
            [hospital.logic :refer :all]))

(deftest cabe-na-fila?
    (testing "Que cabe na fila"
        (is (cabe-na-fila? {:espera []}, :espera))))

Porém, ao tentarmos rodar desta forma com o mesmo caminho que fizemos, o REPL retorna um erro de sintaxe apontando que cabe-na-fila? já existe e refere hospital.logic/cabe-na-fila?, e não podemos definir duas vezes o mesmo símbolo em locais diferentes em um mesmo namespace.

Então, para evitar mais erros deste tipo, usamos por padrão cabe-na-fila?-test em deftest, lembrando que este mesmo padrão pode variar de acordo com a empresa. Rode novamente para analisar o resultado.

O sistema não indica mais problemas, significando que coube na fila.

Para evitar rodar o código dessa forma toda vez, vá na barra superior de opções e clique em "IntelliJ IDEA > Preferences...". Na janela "Preferences", temos o item "Keymap" na lista lateral. Queremos procurar justamente o comando "Tools > REPL > Run Tests in current NS in REPL"; no campo de busca, escreva "REPL" para acessar as ocorrências.

No resultado da pesquisa, busque por "Tools > REPL > Run Tests in current NS in REPL" e dê duplo clique para escolher a opção "Add Keyboard Shortcut" e assim gerar um atalho do teclado deste comando.

Um sugestão é apertar as teclas "Shift + Ctrl + T" ou "Command + Shift + T" ou ainda "Ctrl + Command T", o que preferir. É importante gerar um atalho que ainda não exista no campo aberto da caixa de diálogo "Keyboard Shortcut".

Definido o atalho do teclado de sua preferência, clique em "OK" na caixa de diálogo e depois em "OK" novamente na janela "Preferences". Assim, podemos rodar automaticamente ao invés de seguir todo o caminho anterior.

Insira mais um teste em deftest com testing, escrevendo "Que não cabe na fila quando a fila está cheia". Isso significa que is not cabe-na-fila? quando passamos uma :espera que já possui cinco pessoas dentro, ou seja, 1 2 3 4 5 entre colchetes.

Depois, indique que queremos colocar mais pessoas na fila de :espera e teste rodando no REPL com o atalho.

(deftest cabe-na-fila?-test
    (testing "Que cabe na fila"
        (is (cabe-na-fila? {:espera []}, :espera))))
    (testing "Que não cabe na fila quando a fila está cheia"
        (is (not (cabe-na-fila? {:espera [1 2 3 4 5]}, :espera)))))

O REPL indica que há um teste com duas asserções rodando sem problemas, ou seja, nosso código está funcionando como planejado. Se retirarmos uma das pessoas da fila e rodarmos novamente, o REPL aponta um teste com duas asserções e uma falha.

No código, ao posicionar o cursor sobre a implementação recém alterada, o sistema avisa que esperava que não coubesse mas recebeu true, não sendo o que queríamos. Corrija o teste adicionando novamente o paciente que foi retirado.

Para mostrar o bug e a discussão inicial sobre o teste, se estivéssemos escrito o teste antes, escreveríamos exatamente este teste que estamos trabalhando. Então, teríamos implementado o código presente em hospital.logic e seria apresentado o mesmo bug de agora.

Existe um bug deste código atual que não está sendo testado, e é preciso analisar a fundo para localizá-lo. Existem métodos formais para fazer esta análise e definir quais testes devem ser criados que serão discutidos adiante.

Criamos um teste para o vazio [], o qual é um caso importante. Mas também criamos para uma situação de cheio [1 2 3 4 5], o qual é um caso de borda exatamente em seu limite de vagas.

Façamos um teste para um cenário onde há 6 pessoas na fila; dentro de deftest, escreva "Que não cabe na fila quando tem mais do que uma fila cheia" com testing. Em seguida, insira is e not para cabe-na-fila?uma :espera que tenha 6 pacientes na fila, pedindo para incluir mais pessoas com :espera.

Rode no REPL para analisar o resultado.

(deftest cabe-na-fila?-test
    (testing "Que cabe na fila"
        (is (cabe-na-fila? {:espera []}, :espera))))
    (testing "Que não cabe na fila quando a fila está cheia"
        (is (not (cabe-na-fila? {:espera [1 2 3 4 5]}, :espera)))))
    (testing "Que não cabe na fila quando tem mais do que uma fila cheia"
        (is (not (cabe-na-fila? {:espera [1 2 3 4 5 6]}, :espera)))))

O retorno do REPL aponta que mesmo tendo ultrapassado o limite de vagas na fila, é permitida a entrada de mais pacientes, o que não é esperado. Isso aconteceu pois quando implementamos o código utilizamos diferente de 5 ou not= 5 presente na definição de cabe-na-fila? em hospital.logic.

Logo, todos os casos passariam sem falhas, até mesmo o 5 que devolve false. Porém, 6 também passaria já que é diferente de 5, e aí está o bug. O total de pessoas deve ser alterado para < 5 em cabe-na-fila? de hospital.logic.

Teste novamente o código de h.logic-test para ver que todos os testes passaram. Note que, se estivéssemos escrito os testes antes, teríamos feito apenas os dois primeiros e não o terceiro.

Como Maurício Aniche aponta em sua pesquisa, existe o problema do Oráculo: fazendo testes antes, como saber o que testar se ainda não sabemos o que escrever para ser testado?

Aqui em nosso caso, seguimos um método de testes formal e bem definido na comunidade onde testamos as bordas ou Boundary Tests, um pouco acima e um pouco abaixo dela, chamados de one off. Como no primeiro teste que testamos o zero como borda do vazio, o segundo que é a borda da fila cheia no limite de cinco pessoas e o terceiro que é ultrapassando a lotação por um paciente a mais.

(deftest cabe-na-fila?-test

    ;borda do zero
    (testing "Que cabe na fila"
        (is (cabe-na-fila? {:espera []}, :espera)))

    ;borda do limite
    (testing "Que não cabe na fila quando a fila está cheia"
        (is (not (cabe-na-fila? {:espera [1 2 3 4 5]}, :espera))))

    ;one off da borda do limite para cima
    (testing "Que não cabe na fila quando tem mais do que uma fila cheia"
        (is (not (cabe-na-fila? {:espera [1 2 3 4 5 6]}, :espera)))))

Se estamos sendo formais, quando criamos nossos testes temos algo como um checklist a ser seguido que nos ajuda a verificar os limites do programa. Depois de vários trabalhos utilizando esse método, acaba por ser feito naturalmente, e claro que existem outras abordagens possíveis, e é aí que também entra a questão da experiência da desenvolvedora ou desenvolvedor.

Seguindo essa metodologia, vemos que outros testes devemos fazer para nosso código. Falta-nos fazer pelo menos dois: um deles com quatro pessoas na fila e que pode receber mais pacientes e outro com duas pessoas que também podem receber mais pacientes.

(deftest cabe-na-fila?-test

    ;borda do zero
    (testing "Que cabe na fila"
        (is (cabe-na-fila? {:espera []}, :espera)))

    ;borda do limite
    (testing "Que não cabe na fila quando a fila está cheia"
        (is (not (cabe-na-fila? {:espera [1 2 3 4 5]}, :espera))))

    ;one off da borda do limite para cima
    (testing "Que não cabe na fila quando tem mais do que uma fila cheia"
        (is (not (cabe-na-fila? {:espera [1 2 3 4 5 6]}, :espera))))

    (testing "Que cabe na fila quando tem pouco menos do que uma fila cheia"
        (is (cabe-na-fila? {:espera [1 2 3 4]}, :espera)))

    (testing "Que cabe na fila quando tem pouca gente na fila"
        (is (cabe-na-fila? {:espera [1 2]}, :espera))))

Rodando no REPL, o sistema não aponta falhas.

Porém, não necessariamente estes testes de Boundary Tests e one offs são suficientes para garantir o funcionamento do código, mas é uma ferramenta importante para o trabalho, entre outras abordagens existentes. Ter um check list também é essencial para evitar bugs, seja na criação de testes, uso de variáveis e funções, etc.

Boundary e Category partition - Some threading e mais tipos de testes

Neste passo, veremos mais uma abordagem de teste.

Em nosso código, repare que criamos testes separados uns dos outros. O primeiro é relativo à borda do zero, ou seja, da fila vazia. O segundo diz respeito ao número máximo de pessoas possíveis na fila. O terceiro apresenta a ultrapassagem do limite em um paciente.

Os dois últimos que contém apenas is representam filas com certo número de indivíduos mas sem atingir a lotação máxima, cabendo mais pessoas e estando dentro da borda. Portanto, podem estar agrupadas sob o mesmo testing.

(deftest cabe-na-fila?

    ;borda do zero
    (testing "Que cabe na fila"
        (is (cabe-na-fila? {:espera []}, :espera)))

    ;borda do limite
    (testing "Que não cabe na fila quando a fila está cheia"
        (is (not (cabe-na-fila? {:espera [1 2 3 4 5]}, :espera))))

    ;one off da borda do limite para cima
    (testing "Que não cabe na fila quando tem mais do que uma fila cheia"
        (is (not (cabe-na-fila? {:espera [1 2 3 4 5 6]}, :espera))))

    ;dentro das bordas
    (testing "Que cabe na fila quando tem pouco menos do que uma fila cheia"
        (is (cabe-na-fila? {:espera [1 2 3 4]}, :espera))
        (is (cabe-na-fila? {:espera [1 2]}, :espera)))

Rodando este código no REPL, o retorno aponta um teste com 5 asserções e nenhuma falha. Ainda assim não garantimos o pleno funcionamento do programa com estes testes, sendo necessárias outras abordagens para essa garantia.

Observando o texto de hospital.logic, percebemos mais um caso a ser testado. Tudo o que fazemos no código pode ter bordas; quando fizemos uma comparação < 5 e count com o caso do zero, descobrimos mais bordas. No caso de departamento também, pois ainda não existe em h.logic-test.

Trataremos o caso da inexistência do departamento. No código de teste, insira mais um "Que não cabe quando o departamento não existe" com testing, querendo que o sistema nos devolva nulo com not.

Passe uma fila de :espera com quatro pacientes, ou seja, cabendo mais pessoas nesta fila. Porém, peça para o setor de :raio-x que ainda não existe, consequentemente gerando um retorno de valor false.

(deftest cabe-na-fila?-test

    ;borda do zero
    (testing "Que cabe na fila"
        (is (cabe-na-fila? {:espera []}, :espera)))

    ;borda do limite
    (testing "Que não cabe na fila quando a fila está cheia"
        (is (not (cabe-na-fila? {:espera [1 2 3 4 5]}, :espera))))

    ;one off da borda do limite para cima
    (testing "Que não cabe na fila quando tem mais do que uma fila cheia"
        (is (not (cabe-na-fila? {:espera [1 2 3 4 5 6]}, :espera))))

    ;dentro das bordas
    (testing "Que cabe na fila quando tem pouco menos do que uma fila cheia"
        (is (cabe-na-fila? {:espera [1 2 3 4]}, :espera))
        (is (cabe-na-fila? {:espera [1 2]}, :espera)))

    (testing "Que não cabe quando o departamento não existe"
        (is (not (cabe-na-fila? {:espera [1 2 3 4]}, :raio-x)))))

Ao rodar no REPL, o sistema apresenta uma falha.

De volta ao arquivo de lógica, temos o macro de threading -> que, quando pega o departamento, este devolve nulo. Se chamarmos uma chave para :raio-x em um mapa inexistente, o retorno é nulo.

Se pegarmos o countde nil, o REPL devolve zero. Isso acontece porque várias funções de Clojure seguem a linha de pensamento do criador original de que uma sequência vazia e nulo têm certas ligações. Como em seq vazia pode ser nula, então o count dessa seq deve devolver zero.

Portanto, se a devolução for zero, indica que cabem mais pessoas na fila sem que nem exista esta sessão no departamento de hospital, e precisamos tratar este caso.

O que podemos fazer é verificar. Vimos que podemos usar let para extrair a fila como um get do departamento do hospital. Vimos também o if-let para se tiver o departamento, nos devolver o bloco de ->. Do contrário, o if devolve nulo e a parte hospital departamento já é a própria fila.

(ns hospital.logic)

(defn cabe-na-fila?
    [hospital departamento]
    (if-let [fila (get hospital departamento)]
        (-> fila
            count
            (< 5))))

Com este código, volte ao h.logic-test para testar esta nova alteração. Rode no REPL para ver que a falha não é apresentada novamente.

Com tudo funcionando, podemos refatorar o código de hospital.logic; podemos mudar if-let para when-let que a execução no REPL apresentaria o mesmo resultado.

Mas existe uma outra maneira de fazer o código anterior, o qual threadeava pelos valores hospital, departamento, count, < 5 e etc. Porém, no processo tivemos nil, e como nulo é importante em qualquer linguagem e possui um tratamento especial por várias funções, pode ser que o comportamento da função seguinte não apresente o que queríamos, que é exatamente nosso caso. Então, apague a linha de if-let.

O count tem um comportamento nulo que não é o planejado; o que queremos com nil em count é parar e devolver nulo. Também, é interessante que o código vá threadiando enquanto os valores não são nulos para devolver quando terminar. Se algum deles for, o programa pára e devolve nil.

Existe a macro de threading some-> que possui essa ação que queremos. Altere -> para some-> e execute no REPL o código de h.logic-test.

(ns hospital.logic)

(defn cabe-na-fila?
    [hospital departamento]
        (some-> hospital
            departamento
            count
            (< 5)))

Tudo funciona corretamente pois o departamento é nulo, mostrando a eficiência desta nova abordagem.

É interessante deixar as três abordagens ->, when-let e some-> usadas no arquivo de lógica para serem usadas em trabalhos futuros como registro, comentando e anotando os problemas nas que apresentaram problemas.

O código usado com -> possui uma vantagem ou desvantagem - depende do projeto - por ser menos explícito e por qualquer um que der nulo devolve nil em todas as situações dos threadings. Em geral estes serão menores, pois se forem muito grandes significa muitos elementos na mesma função e seria necessário quebrar para testar melhor.

Mas já que o código está funcionando com a versão que possui some->, volte ao h.logic-test para refatorar, criar mais tipos e classes de testes. Note que só a borda em uma única variável não é interessante, sendo necessário testar uma segunda variável com outro tipo de valor que estávamos passando para a função, não sendo só uma questão do estado da fila do hospital do departamento, mas também uma questão própria do hospital.

Logo, cada alteração e nova implementação em código pode ter erros e bordas para uma nova categoria de testes.

Sobre o curso Clojure: explorando testes

O curso Clojure: explorando testes possui 162 minutos de vídeos, em um total de 34 atividades. Gostou? Conheça nossos outros cursos de Clojure em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Clojure acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas