Testando com python de uma vez por todas #Introdução

Sei que muitas vezes, quando pensamos em testes automatizados, seguimos a regra básica de usar a mesma linguagem do time de desenvolvimento, porém, em muitos casos o desenvolvimento usa linguagens extremamente complexas e nos vemos perdidos nesse cenário. Ferramentas complexas desenvolvidas em C/C++, por exemplo, ou mesmo sistemas legados que só recebem manutenção (Delphi, VB, entre outros…). Neste cenário é normal nos sentirmos perdidos. Alguns testers não tem conhecimento profundo em desenvolvimento, outros têm preferências por algumas linguagens específicas e isso é muito bom em um time. A diferença e a vivência de cada membro envolvido no tema faz todo sentido quando falamos de qualidade de software. Não tem nenhum problema usar linguagens mais simples pra fazer automação. Sério mesmo, e se alguém te contrariar, não diga nada. Então hoje vamos aprender e sair do zero com a minha linguagem preferida: Python.

Por que Python? É legal, simpática, amigona. Mas sendo mais técnico, Python é uma linguagem com uma baixa curva de aprendizado (e isso conta muito quando estamos aprendendo automação), é uma linguagem multiplataforma, tem uma excelente integração com o sistema operacional e tem frameworks incríveis para testes.

Python tem uma biblioteca nativa para teste unitários (unittest) com opções para fazer mocs, nativamente também existem maneiras de se testar usando a documentação (isso é bom para o dev escrever bons comentários), ah… isso serve para fazer BDD também, mas vamos usar um framework específico para isso.

Uma introdução sem pretensões de ensinar nada

Para começar, vamos ver a sintaxe básica do Python. Vou ser bem rápido, eu prometo. Se você já usa ou usou python é claro que você pode pular essa parte. Vamos começar com uma função que executa uma soma:

def soma(x, y):
    """
    Função que soma dois valores
    """
    return x + y

Com esse leve exemplo, já podemos ver algumas características sintáticas e semânticas da linguagem. Python não usa chaves para abrir blocos de código e a indentação é que define o que está dentro de um bloco. Os operadores são bem simples como se pode notar (+, -, /, *). Então vamos olhar uma classe feita em Python agora:

def Gato:
    """
    Classe que abstrai um bichano
    """
    def __init__(self, nome, idade, cor):
        """
        Método inicializador da classe.

        Em python não se usa o método construtor como em outras linguagems,
            como Java e C#, raramente um new será feito em python

        Args:
            self: o argumento self necessariamente tem que existir em todos os
                métodos em python e precisa ser explícito
        """
        self.nome = nome
        self.idade = idade
        self.cor = cor

    def miar(self):
        """
        Método que faz o gatinho miar.


        Todo mundo adora gatinhos miando, não é mesmo?
        """
        return 'miau'

O que eu queria mostrar sobre isso é bem simples e nada complexo, só apresentar a cara de python para quem nunca viu. Mas vamos voltar ao que nos interessa, testes.

Testando unidades com Python

O unittest do Python, foi fortemente influenciado pelo JUnit e costuma ser muito parecido com os frameworks de testes das outras linguagens. Embora a classe seja montada em formato de testes, mas uma coisa interessante sobre ele é que cada método da função é tratado individualmente pelo Python, o que costuma gerar menos efeitos colaterais, eles são unidos em uma mesma classe por organização e pelo fato de todos rodarem juntos. Não podemos rodar um teste específico da classe (ou todos ou nenhum). Um outro fato legal sobre a classe de teste é que podemos criar outros métodos que não necessariamente são executados pelo framework. O framework executa sozinho somente os métodos que iniciam com a palavra test. Podemos criar gerenciadores de contexto e tudo que der vontade para que nossos testes sejam sólidos. Podemos criar dois métodos comuns do Junit SetUp e TearDown como espécie de hook para facilitar o uso da classe. Mas aqui também não vamos nos atentar a profundidade dos testes unitários (acho que vamos fazer uma parte só pra eles), é só pra gente ver como eles são feitos. Bom, vamos ao código:

from unittest import TestCase

def soma(x, y):
    """
    Já vimos essa função, não é mesmo?

    Isso, isso, isso.
    """
    return x + y

class Testes_da_funcao_de_soma(TestCase):
    def test_2_com_2(self):
        """
        Testes somando 2 e dois, o resultado deve ser 4.

        O método assertEqual da classe TestCase faz uma validação simples
            x == y. No caso o resultado da soma() == 4
        """
        self.assertEqual(soma(2, 2), 4)

Como é possível notar, os testes de unidade são bem simples de fazer usando python. Nós podemos inserir vários métodos na nossa classe Testes_da_funcao_de_soma para aumentar a nossa cobertura. A classe TestCase tem vários métodos de assertiva que nos ajudam a resolver muitos problemas, vou listar alguns deles aqui só ser possível ter uma noção da variedade de assertivas:

Método Equivalência
assertEqual(a, b ) a == b
assertNotEqual(a, b ) a != b
assertTrue(x) x is True
assertFalse(x) x is False
assertIs(a, b ) a is b
assertIsNot(a, b ) a is not b
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIn(a, b ) a in b
assertNotIn(a, b ) a not in b

Além dessas, existem muitas outras assertivas que podem nos ajudar em muitos outros contextos, porém como essa é uma introdução vamos falar de outras e encher de exemplos em uma próxima oportunidade.

Se você quiser testar para ver o que acontece, a gente pode fazer algo assim:

from unittest import TestCase, main

def soma(x, y):
    """
    Já vimos essa função, não é mesmo?

    Isso, isso, isso.
    """
    return x + y

class Testes_da_funcao_de_soma(TestCase):
    def test_2_com_2(self):
        """
        Testes somando 2 e dois, o resultado deve ser 4.

        O método assertEqual da classe TestCase faz uma validação simples
            x == y. No caso o resultado da soma() == 4
        """
        self.assertEqual(soma(2, 2), 4)

main()

O resultado tende a ser algo parecido com isso:

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Aquele primeiro pontinho significa que um teste nosso funcionou, a última linha indica que está tudo OK e no meio sabemos o tempo que levou para o teste ser executado e quantos testes rodaram. Vamos continuar e fazer um teste falhar agora:

from unittest import TestCase, main

def soma(x, y):
    """
    Já vimos essa função, não é mesmo?

    Isso, isso, isso.
    """
    return x + y

class Testes_da_funcao_de_soma(TestCase):
    def test_2_com_2(self):
        """
        Testes somando 2 e dois, o resultado deve ser 4.

        O método assertEqual da classe TestCase faz uma validação simples
            x == y. No caso o resultado da soma() == 4
        """
        self.assertEqual(soma(2, 2), 4)
    def test_3_com_3(self):
        """
        Testes somando 3 e 3, o resultado deve ser 7.
        """
        self.assertEqual(soma(3, 3), 7)

main()

E essa é a resposta exibida:

.F
======================================================================
FAIL: test_3_com_3 (__main__.Testes_da_funcao_de_soma)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-16-8b5f450eebd0>", line 24, in test_3_com_3
    self.assertEqual(soma(3, 3), 7)
AssertionError: 6 != 7

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

O lugar onde deveria ter um segundo pontinho, agora tem um F, pois nosso segundo teste falhou.

Bom, por agora fomos introduzidos aos testes de unidade, vamos continuar com os testes de documentação.

Testando com a documentação

Vou confessar que esse é um recurso da linguagem que é um pouco ignorado, mas quando se pretende gerar boas documentações e relatórios. Já ouvi dizer que pessoas escreveram BDD com doctest, mas ver mesmo eu nunca vi. Bom, vamos lá.

Os testes usando doctests testam a documentação de algo. Como ela faz isso? Buscando pedaços de códigos executáveis nos comentários. Vamos exemplificar com a nossa função de soma:

def soma(x, y):
    """
    >>> soma(2, 2)
    4
    """
    return x + y

O console do Python, por definição, tem no começo das linhas esses três sinais de maior >>>. Então, todas as vezes que o doctest encontrar uma sequência >>> ele vai tentar executar esse código. Então isso serve como base também para definição e documentação da função, mas executa os testes ao mesmo tempo. Poderíamos escrever isso no formato BDD, facilita a visualização do comportamento, porém ainda não é o ideal:

def soma(x, y):
    """
    Função que soma dois números (x e y).

    Args:
        x: Primeiro valor a ser somado
        y: Segundo valor a ser somado

    Quando somar 2 + 2
    Então o resultado deverá ser 4
    >>> soma(2,2)
    4
    """
    return x + y

Com isso, a documentação da função fica muito mais completa, como é possível notar. Foi descrito a função, seus argumentos e seu comportamento. Tá, ok, mas como a gente roda isso?


from doctest import testmod

testmod()

Ele ele vai nos retornar TestResults(failed=0, attempted=1). Ou seja nenhum doctest falhou, mas poderíamos complicar isso e colocar um resultado que vai falhar, só pra gente ver como é a mensagem de erro:

from doctest import testmod

def soma(x, y):
    """
    Função que soma dois números (x e y).

    Args:
        x: Primeiro valor a ser somado
        y: Segundo valor a ser somado

    Quando somar 2 + 2
    Então o resultado deverá ser 4
    >>> soma(2, 2)
    4

    Quando somar 3 + 3
    Então o resultado deverá ser 7
    >>> soma(3, 3)
    7
    """
    return x + y

testmod()

E o resultado vai ser algo parecido com isso:

**********************************************************************
File "__main__", line 17, in __main__.soma
Failed example:
    soma(3, 3)
Expected:
    7
Got:
    6
**********************************************************************
1 items had failures:
   1 of   2 in __main__.soma
***Test Failed*** 1 failures.

TestResults(failed=1, attempted=2)

Ou seja, na nossa linha 17 o resultado esperado era 7, o errado, mas a função retornou 6 (o correto). Bom, não vamos nos aprofundar nisso também, mas queria ao menos introduzir essa biblioteca para conhecimento.

Testando com BDD

De todos os testes que podemos pensar agora, os testes com BDD são os mais complexos pois exigem a instalação de um framework externo, no caso vamos usar o Behave. Porque mais complexos? Pois assim como os grandes frameworks do mercado, como cucumber, o Behave exige uma estrutura de pastas e tudo combinado para que ele funcione. Bom caso você não tenha grandes conhecimentos com python, na documentação do behave o modo de instalação e tudo mais está detalhado por lá. Vamos nos atentar apenas ao uso básico da ferramenta.

Acho que não preciso introduzir vocês ao conceito do BDD, e mesmo que eu faça, existem artigos muito melhores que o meu aqui mesmo no agile testers que fariam o meu parecer brincadeira de criança. Dito isso, vamos ao uso do framework.

O Behave precisa de uma estrutura básica de diretórios e arquivos para funcionar, vamos a sua descrição:

|features/              (Diretório principal)
| -> arquivos.feature   (Arquivos com extensão feature)
| -> steps/             (Diretório onde vai estar o código)
| | -> steps.py         (Arquivos de código)

Dito isso, não tem mais muita complexidade envolvida. Caso você erre o diretório ou os nomeie diferentes, o framework não vai reconhecer e isso pode acarretar em alguns problemas.

Vamos tentar descrever nossa função soma() e usar o formato BDD.

  • features/soma.feature
# language: pt

Funcionalidade: Função de soma
Cenário: Soma de números inteiros e positivos
  Quando somar "2" com "2"
  Então o resultado deve ser "4"

Viu, não foi tão dolorido assim, já criamos nossa primeira feature. Um comentário que vale a pena é que a primeira linha # language: pt define que iremos usar português como default para nossa feature. Então, vamos ao código:

from behave import when, then

def soma(x, y):
    """
    Nossa função velha de guerra.
    """
    return x + y


@when('somar "{num_1:d}" com "{num_2:d}"')
def executa_soma(context, num_1, num_2):
    """
    Executa a função de soma e guarda seu resultado no contexto do behave
    """
    context.resultado = soma(num_1, num_2)

@then('o resultado deve ser "{result:d}"')
def assert_resultado(context, result):
    """
    Usa o valor armazenado no contexto e valida se é igual ao resultado
    """
    assert context.resultado == result

Aqui podemos fazer vários comentários, mas não vamos nos atentar a todos os detalhes, afinal, isso é uma introdução. Na primeira linha importamos os decoradores de função when e then para fazer o behave funcionar colocamos nossa função no arquivo (isso não é uma boa prática) e criamos nossas funções. Pode definição o behave exige que as funções com seus decoradores recebam context, que é nada mais nada menos que o contexto de toda a execução do nosso projeto. Também recebe com argumentos as variáveis passadas no gherkin (ou feature file). Aquel :d só indica que o valor que vai ser passado como parâmetro é um valor decimal.

Vamos rodar pra ver no que dá? A linha de comando é exatamente assim:

behave features/soma.feature

E já estamos fazem BDD com Python. O resultado que vamos ver é esse:

$ behave features/soma.feature
Funcionalidade: Função de soma # features/soma.feature:3

  Cenário: Soma de números inteiros e positivos  # features/soma.feature:4
    Quando somar "2" com "2"                     # features/steps/steps.py:10 0.000s
    Então o resultado deve ser "4"               # features/steps/steps.py:17 0.000s

1 feature passed, 0 failed, 0 skipped
1 scenario passed, 0 failed, 0 skipped
2 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.000s

Então por hoje era só isso. Perguntas/sugestões, beijos e abraços. Comentem aí.

Ótimo post Eduardo!
É muito bom ver nossas pessoas blogando e escrevendo artigos sobre coisas legais :)

Eu particularmente sou mega fã do PyTest, mas unittest nativo tb é bem legal :)

A comunidade so tem a ganhar com conteúdos assim ;) parabéns!

@Leonardo-Galani Eu também acho o pytest massa, mesmo não usando bastante. A nível pythonico ele faz muito mais sentido que o unittest. Mas vamos ver, se a galera tiver iteresse sobre o assunto eu posso continuar falando das ferramentas de test do python e pq não dar um overview sobre o pytest também? As fixtures dele são muito boas.

SIM… fixtures do pytest é divino…rs eu fiz um talk sobre pytest + factoryboy no TDC de 2015… ninguem entendeu nada… mas foi legal XD hahahahaha

@Leonardo-Galani Muito massa. Você acha que a galera tem interesse em testes com python para fazer disso um série?

Você teve 9 thumbsup e 113 views em 4 dias… acho que as pessoas tem interesse :)
Nesse embalo eu escrevendo testes do sherlock em pytest + pytest-flask ;)

Log in to reply

Looks like your connection to Agile Testers was lost, please wait while we try to reconnect.