Cobertura de código com pytest

pytestcov-1_abertura-1

Expandindo um pouco aquele básico sobre o pytest com algo que é muito importante em uma rotina de testes, a cobertura do código. E que consiste em uma medida numérica descrevendo o quanto do seu código está sendo efetivamente executado pelas suas rotinas de teste.

No caso específico do Python com pytest uma forma de fazer esta verificação é através do plugin pytest-cov que faz uso de uma outra ferramenta, a coverage, para automaticamente registrar e produzir relatórios de cobertura. E, consequentemente, ajudar a escrever rotinas de teste que efetivamente alcançam todas as partes do programa.

Criando uma função

Começando com uma rotina de testes bem simples, a verifica_bebida(), para servir de exemplo…

IDADE_LEGAL = 18
SUGESTAO = 'copo d\'água'

def verifica_bebida(idade, bebida, alcool, sugestao=SUGESTAO):
if idade < IDADE_LEGAL and alcool:
return False, sugestao
else:
return True, bebida

Ou seja, a rotina simplesmente valida se alguém com a idade legal para consumo de bebidas alcoólicas pode fazê-lo ou retorna uma sugestão de consumo pré-definida em caso contrário.

Assim, para verificá-la você cria uma classe contendo suas rotinas de testes¹…

class TestClass(object):
def test_maior_pede_cerveja(self):
assert verifica_bebida(30, 'cerveja', True) == (True,
'cerveja')
def test_menor_pede_suco(self):
assert verifica_bebida(15, 'suco', False) == (True,
'suco')

Executa…

$ pytest -vv pode_beber.py
======================== test session starts =========================
(...)
collected 2 items


pode_beber.py::TestClass::test_maior_pede_cerveja PASSED       [ 50%]
pode_beber.py::TestClass::test_menor_pede_suco PASSED          [100%]

====================== 2 passed in 0.01 seconds ======================

E verifica que está tudo certo e funcionando com o código pois os testes passaram!

Mas será mesmo?

(¹) Claro que o pytest precisa ser instalado antes… 🙂

Instalando o pytest-cov

A instalação do pytest-cov é simples.

$ pip install pytest-cov
Collecting pytest-cov
...
Collecting coverage>=4.4 (from pytest-cov)
...
Successfully installed coverage-4.5.2 pytest-cov-2.6.0

Basta usar o pip e ele cuidará de resolver as respectivas dependências, inclusive a instalação do coverage..

Analisando a cobertura do teste

Para executá-lo junto com pytest basta informar que ponto do sistema de arquivos, ou módulo/módulos, em que ele irá começar a fazer a análise…

$ pytest -vv --cov=. pode_beber.py
======================== test session starts =========================
...
plugins: cov-2.6.0
...
----------- coverage: platform linux, python 3.6.5-final-0 -----------
Name            Stmts   Miss  Cover
-----------------------------------
pode_beber.py      11      1    91%
====================== 2 passed in 0.03 seconds ======================

Assim você descobre que sua rotina de testes não verifica todas as condições possíveis!

Para saber exatamente onde estão estes 9% não alcançado é necessário usar uma opção de saída do coverage que exiba mais informações e a mais simples deles é exibir as linhas do programa que não foram alcançadas no teste.

$ pytest -vv --cov=. --cov-report=term-missing pode_beber.py
======================== test session starts =========================
...
Name            Stmts   Miss  Cover   Missing
---------------------------------------------
pode_beber.py      11      1    91%   15
====================== 2 passed in 0.03 seconds ======================

Assim descobrimos que a 15ª linha do programa — return False, sugestao — não está sendo alcançada pelos testes. Ou seja, por “excesso de otimismo” a condição de um menor de idade pedindo bebida alcoólica não é validada. Como não há erro de sintaxe nela, ela funciona e retorna o resultado esperado mas isto poderia não ser verdade.

Melhorando a cobertura dos testes

Sendo assim, é necessário expandir a classe com as rotinas de testes…

class TestClass(object):
def test_maior_pede_cerveja(self):
assert verifica_bebida(30, 'cerveja', True) == (True,
'cerveja')
def test_menor_pede_cerveja(self):
assert verifica_bebida(15, 'cerveja', True) == (False,
SUGESTAO)
def test_maior_pede_suco(self):
assert verifica_bebida(30, 'suco', False) == (True,
'suco')
def test_menor_pede_suco(self):
assert verifica_bebida(15, 'suco', False) == (True,
'suco')

E assim…

$ pytest -vv --cov=pode_beber --cov-report=term-missing pode_beber.py
======================== test session starts =========================
(...)
collected 4 items
pode_beber.py::TestClass::test_maior_pede_cerveja PASSED       [ 25%]
pode_beber.py::TestClass::test_menor_pede_cerveja PASSED       [ 50%]
pode_beber.py::TestClass::test_maior_pede_suco PASSED          [ 75%]
pode_beber.py::TestClass::test_menor_pede_suco PASSED          [100%]
----------- coverage: platform linux, python 3.6.5-final-0 -----------
Name            Stmts   Miss  Cover   Missing
---------------------------------------------
pode_beber.py      15      0   100%
====================== 4 passed in 0.16 seconds ======================

E pronto, agora há 100% de cobertura do código no teste!

Considerações finais

pytestcov-1_html-1

Opcionalmente é possível produzir um relatório de cobertura em formato HTML com o parâmetro – -cov-report=html, e bastante útil para se inspecionar visualmente o código. Por padrão o plugin armazena tudo no diretório  “htmlcov” — é possível alterar tanto o nome quanto a localização.

O coverage permite que excluir classes, funções, blocos ou mesmo uma única linha de código através da adição do comentário “# pragma: no cover” colocado logo em seguida, por exemplo.

def faz_nada():  # pragma: no cover
    pass

Prático para fazê-lo desconsiderar na análise de cobertura, partes do código que não serão/poderão ser testadas. E é claro que os trechos ignorados não serão sumarizados dentro de “run” mas sim como “excluded”.

Para finalizar, o exemplo usado aqui foi realmente bem simples pois a ideia era mostrar a análise de cobertura das rotinas de teste e não necessariamente a detecção de falhas de lógica no programa/nas rotinas de teste. Porém, aprimorar os testes para alcançar 100% de cobertura te farão fazer isto (experiência própria).

E lembrando que os arquivos usados aqui estão no repositório do blog no GitHub.

Até!

 

Anúncios