Até este momento vimos vários exemplos de código Python, porém todos dentro do Jupyter Notebook. Esta é uma ótima ferramenta para aprendizado, porém ela restringe o uso de nossos programas, estes só são acessíveis de dentro do Jupyter Notebook. A partir de agora vamos começar a criar nossos próprios programas e módulos em arquivos separados que poderão ser reutilizados e separados em vários arquivos conforme a necessidade.
Esses arquivos com código são comumente chamados de scripts e, para o Python, cada arquivo desses é um módulo. Módulos podem ser importado em outros módulos ou no módulo principal (main module).
Um módulo é um arquivo contendo contendo código Python. O nome do arquivo é o nome do módulo com um sufixo .py.
Vamos começar com um exemplo simples, abra um arquivo chamado fibonacci.py
e coloque o seguinte código (ou copie o arquivo do repositório na mesma pasta deste notebook):
""" Módulo de números da sequência de Fibonacci """
def fib(n):
""" Exibe na tela a sequência de Fibonacci até n """
a, b = 0, 1
while b < n:
print(b, end=' ')
a, b = b, a+b
print()
def fib2(n): # return Fibonacci series up to n
""" Retorna uma lista contendo os números da sequência de Fibonacci até n """
result = []
a, b = 0, 1
while b < n:
result.append(b)
a, b = b, a+b
return result
Na mesma pasta desse arquivo abra o interpretador python com o comando python3.5
.
Agora importe o módulo fibonacci da seguinte maneira:
In [2]:
import fibonacci
Acessamos as funções desse módulo:
In [3]:
fibonacci.fib(1000)
In [4]:
fibonacci.fib2(1000)
Out[4]:
Podemos inspecionar o nome do módulo acessando seu atributo __name__
:
In [5]:
fibonacci.__name__
Out[5]:
É importante notar que documentamos nosso módulo usando docstrings nas funções e no começo do arquivo. Isso permite que outras ferramentas gerem uma documentação de nosso código como a função help()
faz:
In [6]:
help(fibonacci)
É dessa forma que as bibliotecas (e o próprio código-fonte da linguagem) são documentados, como podemos constar com os números inteiros:
In [7]:
help(int)
Um módulo pode conter código python que inicializa o próprio módulo, além de ter definições de funções e variáveis. Esse código de inicialização é executado somente quando o interpretador Python encontra um import <nome do módulo>
. Aqui descobrimos que existem códigos que são executados em tempo de importação e outros (como códigos dentro de funções e métodos) que são rodados em tempo de execução.
Os módulos são isolados entre si. Cada um possui sua própria tabela privada de símbolos (contendo funções, variáveis etc.), portanto ao escrever módulos é possível definir variáveis "globais" sem se preocupar com choques de nomes.
Módulos podem importar outros módulos. Geralmente essas importações são feitas no começo do arquivo. Esses módulos importados são adicionados na tabela de símbolos do módulo que realizou a importação.
É considerado boa prática importar somente as funções que serão utilizadas em seu módulo.
Isso é feito assim:
In [10]:
from fibonacci import fib, fib2
In [11]:
fib(500)
In [12]:
fib2(500)
Out[12]:
Também é possível importar todas as funções usando import *
, porém essa prática deixa o código mais ilegível e a maioria dos programadores não a utiliza.
In [13]:
from fibonacci import *
In [14]:
fib(500)
In [15]:
fib2(500)
Out[15]:
Também podemos organizar nossos módulos em pastas. Por exemplo temos uma pasta exemplos
junto a essa aula que contém uma implementação de vetor euclidiano no arquivo vetor.py. Esse módulo nos fornece uma classe Vetor
que armazena as posições x
e y
em uma tupla, além das funções soma_vetor()
e subtrai_vetor()
.
Aqui está a implementação desse vetor para consulta:
""" Este módulo oferece um vetor espacial e operações de vetor
Fornece uma classe `Vetor` que armazena as posições x e y em uma `namedtuple`
e funções de soma (soma_vetor) e subtração (subtrai_vetor)
"""
from collections import namedtuple
Vetor = namedtuple('Vetor', ['x', 'y'])
def soma_vetor(v1, v2):
"""
Retorna a soma dos vetores v1 e v2 (v1 + v2)
"""
return Vetor(v1.x + v2.x, v1.y + v2.y)
def subtrai_vetor(v1, v2):
"""
Retorna a subtração dos vetores v1 e v2 (v1 - v2)
"""
return Vetor(v1.x - v2.x, v1.y - v2.y)
Agora vamos importar o módulo localizado em ./exemplos/vetor.py
:
In [31]:
from exemplos import vetor
Como vimos anteriomente temos acesso ao nome do módulo através do atributo __name__
da variável vetor
:
In [32]:
vetor.__name__
Out[32]:
Vale lembrar que o python busca por módulos na pasta em que o interpretador é rodado, como a pasta exemplos
está na pasta que o python (e o jupyter notebook) rodam é possível acessá-lo. O Python também busca módulos em outras pastas, porém esse assunto será tratado daqui a pouco.
Agora que temos acesso ao módulo vetor podemos acessar seus recursos e criar um vetor matemático
In [17]:
v1 = vetor.Vetor(x=1, y=5)
v1
Out[17]:
In [18]:
v2 = vetor.Vetor(x=-2, y=3)
v2
Out[18]:
Também podemos usar as funções de soma e subtração de vetor:
In [19]:
vetor.soma_vetor(v1, v2)
Out[19]:
In [20]:
vetor.subtrai_vetor(v1, v2)
Out[20]:
Assim como fizemos com o exemplo do fibonacci podemos importar diretamente as funções que vamos usar usando from ... import ...
:
In [25]:
from vetor import Vetor, soma_vetor, subtrai_vetor
v1 = Vetor(2, 3)
v1
Out[25]:
In [26]:
v2 = Vetor(4, -1)
v2
Out[26]:
In [27]:
soma_vetor(v1, v2)
Out[27]:
In [28]:
subtrai_vetor(v1, v2)
Out[28]:
Agora que nós entendemos como criar e usar módulos Python, vamos aprender como executar esses módulos como scripts. Um programa python é executado da seguinte forma:
$ python <nome-do-arquivo>.py <argumentos>
Dessa forma o código do módulo é executado, mas o __name__
recebe "__main__"
. Se adicionarmos o seguinte código no final do modulo fibonnaci.py
:
if __name__ == "__main__":
import sys
n = int(sys.argv[1])
fib(n)
Tornamos nosso módulo um script executável além de um módulo importável, pois o código que trata os argumentos é rodado somente quando o módulo executado como o arquivo principal ("main file"):
$ python fibonnaci.py 50
1 1 2 3 5 8 13 21 34 55 89
O módulo sys
(importado no exemplo anterior) fornece funções e variáveis do sistema. No exemplo usamos sys.argv
que armazena os argumentos enviados a um script python, armazenando-os como strings em uma lista. Para saber mais sobre esse módulo consulte a documentação oficial
Em alguma aula anterior foi falado sobre a função embutida dir()
que retorna uma lista de atributos do objeto fornecido. É possível usar essa função em módulos para saber quais funções e atributos esse oferece:
In [34]:
dir(fibonacci)
Out[34]:
In [35]:
dir(vetor)
Out[35]:
"Teste é o processo de executar um programa ou sistema com a intenção de encontrar erros". (Myers, 1979 - The art of software testing)
Diferentes tipos de teste podem ser utilizados para verificar se um programa se comporta como o especificado. Os testes podem ser classificados em teste de caixa-preta, teste de caixa-branca ou teste baseado em defeito. A técnica do teste é definida pelo tipo de informação utilizada para realizar o teste:
- Técnica caixa-preta: testes baseados na especificação de requisitos do programa. Nenhum conhecimento de como o programa é implementado é requerido.
- Técnica caixa-branca: os testes são baseados na implementação do software.
- Técnica baseada em defeito: os testes são baseados em informações históricas sobre defeitos cometidos frequentemente durante o processo de desenvolvimento de software.
Benefícios de testes:
Nesta aula veremos somente com Teste de Unidade (ou teste unitário) que faz parte das técnicas de caixa-branca.
O objetivo do teste unitário é identificar erros de lógica e de programação na menor unidade de programação. A unidade, em Python, podem ser métodos, classes e funções.
Antes de começar a mexer com testes unitários, vamos implementar testes simples usando o módulo doctest
.
O módulo doctest
procura por textos que parecem com o shell interativo do Python e executa essas linhas verificando se elas executam exatamente como mostrado. É como se você estivesse digitando no modo interativo e recebendo as respostas corretas.
doctest
oferece uma maneira simples e fácil de programar usando TDD (Test Driven Development) com Python para iniciantes na linguagem.
Só para deixar claro o módulo doctest
não oferece uma API para testes unitários, ele é utilizado para documentação, testes rasos e para usar TDD de uma maneira simples.
Bem, como diria Linus Torvalds "Falar é barato. Mostra-me o código": vamos aos exemplos de testes com doctest
para os módulo fibonacci
e vetor
vistos anteriormente:
""" Módulo de números da sequência de Fibonacci """
def fib(n):
"""
Exibe na tela a sequência de Fibonacci até n
>>> fib(10)
1 1 2 3 5 8
>>> fib(-1)
<BLANKLINE>
"""
a, b = 0, 1
while b < n:
print(b, end=' ')
a, b = b, a+b
print()
def fib2(n): # return Fibonacci series up to n
"""
Retorna uma lista contendo os números da sequência de Fibonacci até n
>>> fib2(10)
[1, 1, 2, 3, 5, 8]
>>> fib(-1)
[]
"""
result = []
a, b = 0, 1
while b < n:
result.append(b)
a, b = b, a+b
return result
Para executar os doctests nesse exemplo: grave o código em um arquivo, como por exemplo fibonacci.py
, abra um terminal e navegue até sua pasta e rode o seguinte comando:
$ python -m doctest fibonacci.py
Se esse comando não emitir mensagem de erro isso significa que o programa passou nos testes. Caso você queira um relatório completo da execução dos testes pode rodar esse comando no modo verboso da seguinte forma:
$ python -m doctest -v fibonacci.py
Ao rodar o último comando a saída deve ser igual a essa:
Trying:
fib(10)
Expecting:
1 1 2 3 5 8
ok
Trying:
fib(-1)
Expecting:
<BLANKLINE>
ok
Trying:
fib2(10)
Expecting:
[1, 1, 2, 3, 5, 8]
ok
Trying:
fib2(-1)
Expecting:
[]
ok
1 items had no tests:
fibonacci
2 items passed all tests:
2 tests in fibonacci.fib
2 tests in fibonacci.fib2
4 tests in 3 items.
4 passed and 0 failed.
Test passed.
Como visto no exemplo, os doctests ficam dentro das docstrings e os códigos que vem após >>>
são executados como se esivessem na shell Python e a linha a seguir é a resposta esperada da execução do código.
Quando alguma função é executada e não gera saída e nem retorna valor devemos colocar <BLANKLINE>
como resposta.
Nesse exemplo testamos: o funcionamento da função fib()
que deve imprimir no console a sequência até o número 10, também foi testado uma entrada inválida (n = -1) que não deve imprimir algo. O mesmo foi feito fib2()
, com a diferença que esta retorna uma lista.
Agora vamos ver como ficam os doctests para o módulo vetor
, grave o seguinte código em um arquivo vetor.py
:
"""
Oferece um vetor matemático e operações de vetor
>>> v = Vetor(x=10, y=5)
>>> v.x == v[0] == 10
True
>>> v.y == v[1] == 5
True
"""
from collections import namedtuple
Vetor = namedtuple('Vetor', ['x', 'y'])
def soma_vetor(v1, v2):
"""
Retorna um vetor que representa a soma dos vetores v1 e v2 (v1 + v2)
>>> v1 = Vetor(5, 2)
>>> v2 = Vetor(2, 7)
>>> soma_vetor(v1, v2)
Vetor(x=7, y=9)
"""
return Vetor(x=v1.x + v2.x, y=v1.y + v2.y)
def subtrai_vetor(v1, v2):
"""
Retorna um vetor que representa a soma dos vetores v1 e v2 (v1 + v2)
>>> v1 = Vetor(3, 4)
>>> v2 = Vetor(1, 5)
>>> subtrai_vetor(v1, v2)
Vetor(x=2, y=-1)
"""
return Vetor(x=v1.x - v2.x, y=v1.y - v2.y)
Para rodar os testes navegue até a pasta do arquivo vetor.py
em um terminal e digite:
$ python -m doctest -v vetor.py
O resultado desse comando deve ser igual a esse:
Trying:
v = Vetor(x=10, y=5)
Expecting nothing
ok
Trying:
v.x == v[0] == 10
Expecting:
True
ok
Trying:
v.y == v[1] == 5
Expecting:
True
ok
Trying:
v1 = Vetor(5, 2)
Expecting nothing
ok
Trying:
v2 = Vetor(2, 7)
Expecting nothing
ok
Trying:
soma_vetor(v1, v2)
Expecting:
Vetor(x=7, y=9)
ok
Trying:
v1 = Vetor(3, 4)
Expecting nothing
ok
Trying:
v2 = Vetor(1, 5)
Expecting nothing
ok
Trying:
subtrai_vetor(v1, v2)
Expecting:
Vetor(x=2, y=-1)
ok
4 items had no tests:
vetor.Vetor
vetor.Vetor.x
vetor.Vetor.y
vetor.multiplica_vetor
3 items passed all tests:
3 tests in vetor
3 tests in vetor.soma_vetor
3 tests in vetor.subtrai_vetor
9 tests in 7 items.
9 passed and 0 failed.
Test passed.
Nesse exemplo testamos: a criação de um objeto da classe Vetor
que deve conter os atributos Vetor.x
e Vetor.y
e permitir o acesso de seus elementos por índice. Também testamos as funções soma_vetor()
e subtrai_vetor()
em que criamos dois vetores e os passamos como parametro para as funções.
Com isso terminamos este tópico sobre o módulo doctest
- que oferece uma maneira simples de testar e documentar APIs. Caso você queria saber mais confira a documentação oficial.
Agora vamos abordar a biblioteca unittest
que é utilizada para implementar testes unitários para aplicações Python.
O módulo unittest
faz parte da biblioteca padrão do Python e oferece uma API de testes de unidade baseada no framework xUnit. Alguns pythonistas dizem que a unittest
não é pythônica por seu "sotaque javanês".
Nesta aula veremos somente uma simples introdução à testes unitários com unittest
. No curso de django falaremos mais profundamente sobre testes com foco em aplicações web.
Vamos começar implementando os testes unitários do módulo fibonacci
, para isso crie um arquivo test_fibonacci.py
na mesma pasta em que se encontra o arquivo fibonacci.py
.
A função fibonnaci.fib()
somente imprime a sequência de fibonacci na tela e é mais complicada de testar, por esse motivo vamos começar pelo teste da fibonnaci.fib2()
que retorna uma lista:
from unittest import TestCase
from fibonacci import fib2
class TesteFibonacci(TestCase):
def testa_fib2_entrada_invalida(self):
sequencia = fib2(-1)
self.assertEqual(sequencia, [])
def testa_fib2(self):
sequencia = fib2(100)
self.assertEqual(sequencia, [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89])
Rodamos o teste com o seguinte comando:
$ python -m unittest test_fibonnaci.py
Se nenhum erro aparecer isso quer dizer que os testes passaram! Para obter mais informações sobre o teste é possível rodá-los de forma verbosa:
$ python -m unittest -v test_fibonacci.py
E a saida verbosa deve ser igual a essa:
testa_fib2 (exemplos.test_fibonacci.TesteFibonacci) ... ok
testa_fib2_entrada_invalida (exemplos.test_fibonacci.TesteFibonacci) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
Nesse exemplo importamos a classe TestCase
do módulo unittest
que nos permite definir uma unidade individual de teste que checa respostas específicas para entradas dadas.
Criamos a classe TesteFibonnaci
que herda de unittest.TestCase
e, portanto, se torna um caso de teste que será rodado pelo "rodador" de testes (test runner). Nessa classe definimos um método chamado testa_fib2_entrada_invalida()
para testar a função fibonacci.fib2()
quando chamada com uma entrada inválida. Também definimos um método testa_fib2()
para testar a criação de uma sequência de fibonacci até 100.
Você deve ter reparado que os métodos da classe TesteFibonacci
recebem o argumento self
como parametro. Isso acontece pois todos os métodos de classe no Python recebem a instância (self) de forma explícita. Em Java ou C++ o acesso à instância se dá de forma implícita pelo uso do this
.
As linhas que chamam self.assertEqual(a, b)
são responsáveis por verificar se a saída esperada das funções testadas correspondem ao código implementado.
Para finalizar vamos escrever os testes de unidade do módulo vetor
visto anteriormente. Para isso crie um arquivo chamado test_vetor.py
na mesma pasta em que se encontra o arquivo vetor.py
.
Vamos começar testando a criação do vetor:
from unittest import TestCase
from vetor import Vetor
class TestaVetor(TestCase):
def testa_vetor(self):
v = Vetor(x=1, y=-1)
self.assertEqual(v.x, 1)
self.assertEqual(v.y, -1)
Para rodar os testes use o seguinte comando:
$ python -m unittest -v test_vetor.py
A saída desse comando deve ser igual a essa:
testa_vetor (exemplos.test_vetor.TesteVetor) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Nesse teste garantimos a existência e funcionamento da classe Vetor: que deve receber dois argumentos x
e y
e criar dois atributos Vetor.x
e Vetor.y
e permitir seu acesso.
Agora vamos testar as funções soma_vetor()
e subtrai_vetor()
:
class TestaVetor(TestCase):
def testa_vetor(self):
v = Vetor(x=1, y=-1)
self.assertEqual(v.x, 1)
self.assertEqual(v.y, -1)
def testa_soma(self):
v1 = Vetor(5, 1)
v2 = Vetor(0, 3)
v = soma_vetor(v1, v2)
self.assertEqual(v, Vetor(5, 4))
def testa_subtrai(self):
v1 = Vetor(5, 1)
v2 = Vetor(2, 3)
v = subtrai_vetor(v1, v2)
self.assertEqual(v, Vetor(3, -2))
Nos métodos testa_soma()
e testa_subtrai()
criamos dois vetores, realizamos as operações e testamos com self.assertEqual()
se os resultados das funções correspondem ao esperado.
Ao rodar esses testes com o comando
$ pythoon -m unittest -v test_vetor.py
A seguinte mensagem deve aparecer no terminal:
testa_soma (exemplos.test_vetor.TestaVetor) ... ok
testa_subtrai (exemplos.test_vetor.TestaVetor) ... ok
testa_vetor (exemplos.test_vetor.TestaVetor) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
Assim terminanos a aula, agora você já sabe o básico de testes de unidade em Python e agora pode implementar esses testes para suas aplicações caso julgue necessário.
Testando funções que o resultado aparece na saída padrão (stdout) como a fibonacci.fib()
. Esse teste trará algumas técnicas mais "avançadas" de Python e é totalmente opcional.
Para testar a função fibonacci.fib()
precisamos trocar a saída padrão (que exibe caracteres na tela) para uma stream que tenhamos acesso. A stream padrão está em sys.stdout
e podemos utilizar a io.StringIO()
uma stream em memória que armazena textos em formato string.
Sabendo disso podemos criar nosso teste:
from io import StringIO
import sys
from unittest import TestCase
from fibonacci import fib
class TestFibonacci(TestCase):
def test_fib(self):
original_stdout = sys.stdout # guardamos a stream padrão
stream = StringIO() # criamos outra stream que armazena texto
sys.stdout = stream # trocamos a stream padrão pela nossa
fib(100) # rodamos nosso teste
result = stream.getvalue() # pegamos o resultado da nossa stream
sys.stdout = original_stdout # desfazemos a troca da stream padrão
self.assertEqual(result, '1 1 2 3 5 8 13 21 34 55 89 \n') # testamos o resultado de `fib(100)`
Coloque essa classe no arquivo test_fibonacci.py
- que deve estar na mesma pasta do arquivo fibonacci.py
- e rode os testes com o comando:
$ python -m unittest -v test_fibonacci.py
A saída deve ser igual a esta:
test_fib (exemplos.test_fib.TestFibonacci) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Acontece que essa não é a melhor maneira de implementar a captura da stdout
. Já vimos anteriormente uma forma de lidar com blocos de códigos que funcionam em contextos específicos (neste caso o contexto é o redirecionamento da saída padrão capturada para uma stream durante a execução da função fib()
).
A maneira correta de resolver esse problema seria criar um gerenciador de contexto (visto rapidamente na Aula 05 que fala sobre funções e arquivos). Talvez seja necessário consultar a documentação oficial para entender melhor os recursos utilizados no exemplo a seguir.
from io import StringIO
from contextlib import contextmanager
import sys
import unittest
from fibonacci import fib
@contextmanager
def capture_stdout(stream):
original_stdout = sys.stdout
sys.stdout = stream
# o código até aqui é executado ao entrar no gerenciador de contexto
yield
# daqui para baixo é executado ao sair do bloco do gerenciador de contexto
sys.stdout = original_stdout
class TestFibonacci(unittest.TestCase):
def test_fib(self):
stream = StringIO()
with capture_stdout(stream):
fib(100)
result = stream.getvalue()
self.assertEqual(result, '1 1 2 3 5 8 13 21 34 55 89 \n')
Como podemos ver o código do nosso teste fica muito mais simples utilizando o gerenciador de contexto, pois isolamos a lógica de redirecionamento de saída para o context manager.
Quando fui colocar o link da documentação do módulo contextlib
que oferece facilidades relacionadas à gerenciadores de contexto aproveitei para lê-la e acabei encontrando uma função que faz o redirecionamento da saída padrão chamada contextlib.redirect_stdout()
. Então a solução ideal é não reinventar a roda e utilizá-la:
from io import StringIO
from contextlib import redirect_stdout
import unittest
from fibonacci import fib
class TestFibonacci(unittest.TestCase):
def test_fib(self):
stream = StringIO()
with redirect_stdout(stream):
fib(100)
result = stream.getvalue()
self.assertEqual(result, '1 1 2 3 5 8 13 21 34 55 89 \n')