Tempo estimado de leitura: 24 minutos
[WIP] Introdução ao mundo dos testes
Objetivos dessa aula:
- Introduzir os conceitos iniciais de testes
- Introduzir a palavra reservada
assert - Introduzir a anatomia de um teste
- Entrada e saída
- Arrange, Act e Assert
- Introduzir nomenclaturas básicas
- System Under Test (SUT)
- Dependent-on Component (DOC)
Antes de mais nada, quero parabenizar você por estar aqui e investir um tempo tão valioso para aprender sobre testes de software.
Nesta aula, vamos começar com os conceitos fundamentais de como escrever testes automatizados em Python. Ao final, você conseguirá escrever seu primeiro teste e entender o papel essencial de garantir que seu código se comporte corretamente.
Introdução aos testes
Quando começamos a escrever código, um dos principais desafios é garantir que ele funcione corretamente. Pode ser tentador escrever a solução para um problema e seguir em frente, mas a realidade é que, à medida que o código cresce, ele se torna mais complexo e mais difícil de garantir que esteja funcionando como esperado. Validar que algo funciona hoje, quando acabamos de fazer, é mais simples, estamos com aquilo fresco na cabeça. Lembramos como chamar, que valores validam casos importantes, o que fazer para passar por aquele if específico.
Mas, conforme o tempo vai passando, o software, por ser vivo, vai ganhando mais e mais funcionalidades, e antes o que era um script simples, acaba se tornando um módulo, depois um pacote. No fim, temos uma aplicação completa, ou até mesmo uma biblioteca. E quando as coisas vão crescendo, simular algo que escrevemos meses atrás, ou até mesmo anos, se torna inviável. Isso é... Quando fomos nós mesmos que escrevemos aquilo.
É aí que os testes automatizados entram. Testar é uma maneira sistemática de verificar se o seu código está se comportando como deveria. Os testes são como uma rede de segurança para o seu código: eles ajudam a identificar falhas rapidamente e oferecem uma maneira de garantir que, à medida que você faz alterações, o seu código atual, que está sendo feito agora, funciona. E à medida em que vamos interagindo com ele, podemos rodar os testes novamente. O que Kent Beck nomeia como ciclo positivo de feedback no seu livro de TDD.
Mas, mais do que isso. Além de saber sobre o código de hoje, eles garantem que tudo que foi feito antes continua funcionando corretamente. O que chamamos de regressão. Se nas novas alterações não mudamos comportamentos, se não fizemos caquinha, se não alteramos APIs que são consideradas estáveis no sistema, etc.
No mundo dos testes de software, existem diferentes abordagens e granularidades, mas todas têm um objetivo em comum: aumentar a confiança no código com qual estamos trabalhando. A ideia principal por trás de escrever testes não é só verificar se o código "funciona", mas garantir que ele se comporta da maneira que esperamos, especialmente quando são feitas mudanças ou novas funcionalidades são adicionadas.
Com isso em mente, nesta primeira aula, vamos conversar sobre os conceitos básicos de testes e como usá-los para garantir que o código que você escreve seja minimamente confiável. Vamos começar com o básico e entender como organizar as coisas e escrever nossos primeiros esboços de testes.
Mas afinal, o que é testar?
Sempre ouvimos coisas como "é preciso fazer testes", "você precisa aprender testes" e mesmo nessa introdução, acabei de enfatizar suas qualidades. Mas, uma grande questão para mim é o que realmente significa testar.
Se recorrermos a um dicionário como o Michaelis, temos o verbete teste:
- Submeter a teste, para avaliar o conhecimento, a habilidade etc.: O professor testava os alunos semanalmente.Testaram-no em várias disciplinas.
- Pôr à prova; experimentar: O técnico testou todos os aparelhos eletroeletrônicos da escola.
- Observar atentamente ou averiguar algo; sondar: Estava sempre testando a honestidade dos seus empregados.
Acho que a definição que melhor se enquadra no mundo do sotware é a segunda: Pôr à prova. No sentido de que aquilo que se fez como um bloco de código ou um software completo precisa ser colocado à prova. Garantir que aquilo que foi feito se comporta da maneira como deveria. Retorna o que esperamos, faz o que tem que fazer.
Um teste, então, seria uma validação de um comportamento do sistema quando colocado à prova. Por validação, entenda algo como executar um código e ver se o que ele fez é exatamente o que ele foi feito para fazer.
Tentando ser menos abstrato, podemos pensar em uma função qualquer de código. Algo como:
def inverte_string(texto: str) -> str:
"""Inverte qualquer string passada."""
return texto[::-1]
Como testar se isso funciona? Primeiramente, precisamos de um valor que será passado a essa função. Um texto. Vamos pensar em uma string como 'alface'. Esse será nosso dado de entrada.
Agora, para podermos testar, precisamos definir claramente qual é a nossa expectativa para o comportamento dessa função. Expectativa é o que esperamos que o código faça quando fornecemos uma entrada específica.
Nossa expectativa para a função inverte_string é que, ao receber 'alface', o resultado deve ser 'ecafla'. Ou seja, esperamos que a função inverta exatamente a string como esperado.
Se quisermos colocar inverte_string à prova, poderíamos fazer algo como:
entrada = 'alface' #(1)!
esperado = 'ecafla' #(2)!
resultado = inverte_string(entrada) #(3)!
assert resultado == esperado, f'Esperado "{esperado}", obteve "{resultado}"' #(4)!
- Entrada: a string que queremos inverter.
- Expectativa: sabemos que a string
'alface'invertida deve ser'ecafla', - Resultado: chamamos a função para inverter a string.
- Verificamos se o resultado da função corresponde à nossa expectativa
Comentários em blocos
Blocos de código costumam ter comentários com informações adicionais, como este:

Ao clicar em um bloco de comentário se abrirá, exibindo mais informações:

Perceba que, ao escrevermos o teste, não estamos somente verificando se a função "funciona", mas estamos validando um comportamento específico que queremos que o código tenha. Em outras palavras, o teste não é para ver se o código roda, mas para garantir que o comportamento dele seja o que esperamos. Ou seja, a expectativa se concretiza.
Se o valor retornado pela função não for o esperado, o assert vai lançar um erro e nos avisar disso. Por exemplo, se a função retornar 'abc' ao invés de 'ecafla', o erro será lançado, e a mensagem que definimos ajudará a entender onde a falha aconteceu.
A palavra assert
Um dos pontos importantes da escrita de testes é pensar em como as coisas serão verificadas. Python tem uma palavra reservada, que usamos na sessão anterior, sendo raramente usada no código de produção. A palavra assert.
Ela tem o objetivo de dizer se o resultado de uma expressão é verdadeiro ou falso.
Como, por exemplo:
Nesse exemplo, nada aconteceu, pelo fato de a expressão True ser obviamente verdadeira.
Dica: Como abrir o terminal interativo (REPL)
Para abrir o terminal interativo do python, tudo que você precisa fazer é abrir o shell e digitar python:
Porém, se essa expressão for falsa, o python vai levantar uma exceção do tipo AssertionError:
>>> assert False
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
assert False
^^^^^
AssertionError
Identificando que o resultado da expressão resultou em um valor falso.
assert com tuplas
Tudo bem, levantou uma exceção, mas não diz exatamente nada, somente que alguma coisa deu erro no assert.
Se passar uma tupla para instrução, sendo o segundo valor uma str, a mensagem descrita será usada na mensagem de erro:
>>> assert False, 'Deu ruim...'
Traceback (most recent call last):
File "<python-input-1>", line 1, in <module>
assert False, 'Deu ruim...'
^^^^^
AssertionError: Deu ruim...
Embora esse exemplo não diga muita coisa, agora conhecemos mais uma forma sintática do assert.
Comparação rica
Um dos lugares mais interessantes para usar assert é durante uma comparação entre dois objetos python, formando a expressão obj <op> obj. Algo como:
Uma comparação entre dois objetos sempre retorna uma afirmação do tipo bool. Dizendo se o resultado da comparação é verdadeiro ou falso.
Sabendo disso, podemos usar os comparadores junto ao assert:
>>> a = 1
>>> b = 2
>>> assert a == b, f'{a} e {b} são diferentes'
Traceback (most recent call last):
File "<python-input-5>", line 1, in <module>
assert a == b, f'{a} e {b} são diferentes'
^^^^^^
AssertionError: 1 e 2 são diferentes
E partir disso para comparações realmente complexas. Como comparar resultados de chamadas de funções. Algo parecido com isso:
>>> def soma(x, y):
... return x - y #(1)!
...
>>> assert soma(1, 2) == 3, 'deu ruim na soma'
Traceback (most recent call last):
File "<python-input-11>", line 1, in <module>
assert soma(1, 2) == 3
^^^^^^^^^^^^^^^
AssertionError: 'deu ruim na soma'
- Note que aqui foi trocado o operador
+pelo de-.
O que nos possibilita fazer validações incríveis durante a execução do código.
Operadores
Python conta com uma gama de 8 operadores básicos para comparação:
| Operador | Descrição | Exemplo |
|---|---|---|
== |
Verifica se os valores são iguais. | 5 == 5 retorna True |
!= |
Verifica se os valores são diferentes. | 5 != 3 retorna True |
> |
Verifica se o valor à esquerda é maior que o à direita. | 5 > 3 retorna True |
< |
Verifica se o valor à esquerda é menor que o à direita. | 3 < 5 retorna True |
>= |
Verifica se o valor à esquerda é maior ou igual ao à direita. | 5 >= 3 retorna True |
<= |
Verifica se o valor à esquerda é menor ou igual ao à direita. | 3 <= 5 retorna True |
Você pode usá-los para comparar quaisquer objetos, incluindo os criados por você.1
Como todas as comparações resultam em um tipo bool, qualquer uma delas pode ser usada:
Criando uma grande gama de variações em expressões para serem usadas em conjunto com o assert.
Como colocar à prova nossa função inicial:
def inverte_string(texto: str):
"""Inverte qualquer string passada."""
return texto[::-1]
assert inverte_string('alface') == 'ecafla', 'alface invertido não é ecafla'
assert inverte_string('ecafla') == 'alface', 'ecafla invertido não é alface'
Altere os valores e/ou a função e veja se os resultados continuam sendo positivos.
Blocos executáveis
Todas as vezes que você encontrar com um bloco de texto como esse no material, que mostra o bloco run:

Você pode alterar o código e executar no próprio navegador. Os resultados aparecerão abaixo, no bloco de output.
Esse blocos são gerados com pyodide. Embora existam algumas limitações, quase toda a biblioteca padrão pode ser executada com esses blocos.2
Operações booleanas
Claro que não são somente as comparações que resultam no tipo bool em Python. Qualquer objeto considerado verdadeiro em uma chamada da função embutida bool() poderia ser usada nesse contexto.
Como, por exemplo, os números:
Quando esses valores são usados como a expressão do assert, é como se bool() fosse chamado por baixo dos panos:
assert 1 assert 0, 'Zero é False!'
E como esse valor é encarado como False, a instrução de assert levantará uma exceção.
Teste do valor verdade
Isso quer dizer que a operação de assert executa o que chamamos de Teste do valor verdade.
Todo objeto é considerado verdadeiro por padrão. Ao menos que o resultado de bool() retorne False ou que o resultado de len() retorne 0. Por exemplo:
- zero de qualquer tipo numérico:
0,0.0,0j,Decimal(0),Fraction(0, 1) - sequências e coleções vazias:
'',(),[],{},set()
Somente alguns valores padrão são considerados falsos por definição, como None ou False.
Truthy e Falsy
Nesse contexto de valores verdadeiros e falsos e seu teste de verdade, encontramos dois termos que são importantes de mencionar: truthy e falsy.
Ao usar o assert sem um objetivo de expectativa de resposta, sem uma comparação clara, caímos em um caso interessante de analisar:
Esse assert sempre retornará verdade. Pois o valor resultante da função é uma sequência não vazia. Ou seja, o len(inverte_string('alface')) é sempre maior que 0.
Por ser sempre verdade, dizemos ser truthy. Um valor que é considerado verdadeiro. Note que a tentação de usar isso no teste é grande. Pois isso valida que a função de fato funciona, mas... vamos nos aprofundar nisso.
Se usarmos a função com uma string vazia:
Isso não necessariamente confirma se a função funciona como deveria. Em teoria, inverter uma string vazia vai retornar uma nova string vazia. Por vazio, temos a questão onde o len(inverte_string('alface')) é sempre igual a 0. Um valor que é considerado falso, um falsy.
Embora o comportamento em teoria esteja correto, não comparar o valor de resposta com nada vai levantar uma exceção. Um falso positivo.
Por esse motivo, entender a estrutura de um teste é de extrema importância.
Caso de teste
Um caso de teste é uma unidade específica de verificação do comportamento de um sistema. Ele define um cenário particular com uma entrada específica, um comportamento esperado e uma verificação do resultado obtido. O objetivo de um caso de teste é garantir que uma parte do sistema funciona como esperado.
O ato de testar é colocar o código à prova em condições controladas, de modo a verificar se ele se comporta conforme o esperado em diferentes cenários. Portanto, um cenário de teste consiste em fornecer um conjunto de entradas ao código e observar se o comportamento retornado corresponde ao valor esperado.
Quando falamos sobre o pensamento por trás de um teste, uma das coisas interessantes a considerar é a estrutura que ele deve ter. Um teste contém geralmente os seguintes elementos:
- Valor de entrada: O que usaremos para colocar à prova nosso código
- Valor esperado: O valor que esperamos que o código retorne, caso o comportamento esteja correto.
- Código a ser testado: O bloco de código que o teste colocará à prova (SUT).
- Comparação: A verificação entre o valor de entrada e o valor esperado.
A função do teste é colocar o código à prova usando o input e observar o output:
flowchart LR
Input -- valor de entrada --> Código -- valor de saída --> Output
Fazendo a comparação, se o que for retornado para o valor de entrada pré-determinado, quando passar pelo código, resultará exatamente no valor esperado:
flowchart LR
Teste -- Determina --> Input
Teste -- Compara com o esperado --> Output
Teste -- Chama --> Código
Input --> Código --> Output
Descrevemos a forma de pensar como o teste é montado, a anatomia do teste.
Anatomia de um teste
Sobre esse tópico
Embora esse tópico seja mais avançado, voltaremos nele ao decorrer das aulas muitas vezes para expandir esse conceito. Entender a anatomia de um teste é extremamente importante. Vamos começar de uma forma simplória por agora.
A anatomia de um teste diz respeito à estrutura que um teste deve seguir. A forma como organizamos e dividimos o código em uma sequência lógica e clara facilita tanto a leitura quanto a manutenção dos testes. Existem várias abordagens, e discutiremos algumas delas ao longo das aulas. Entre as mais comuns, temos o Given-When-Then, o modelo AAA, o modelo de 4 fases, etc.
AAA - As 3 fases de um teste
Dentro desse nosso contexto inicial, podemos dizer que um teste tem exatamente três responsabilidades, o que chamamos de um teste AAA3:
Arrange: Ajustar os dados de entrada e os resultados esperadosAct: Fazer uma chamada ao código que queremos testar, o SUTAssert: Garantir que o resultado retornado peloActé igual ao resultado esperado elencado peloArrange
De maneira simplória, nós já fizemos isso:
# --- Arange
entrada = 'alface'
esperado = 'ecafla'
# --- Act
resultado = inverte_string(entrada)
# --- Assert
assert resultado == esperado, f'Esperado "{esperado}", obteve "{resultado}"'
O que não havíamos conversado até agora eram os nomes que cada uma dessas coisas tem.
Com a separação clara de cada etapa e suas responsabilidades, a escrita do teste se torna fluida e organizada. Isso nos ajuda a estruturar eficientemente o que estamos verificando e facilita a manutenção de testes ao longo do tempo.
AAA com outro nome
Assim como "Arange-Act-Assert", podemos encontrar a sigla como GWT (Given-When-Then [Dado-Quando-Então]). Particularmente, prefiro GWT, mas acredito que o AAA seja a forma mais comum de aparecer na literatura.
SUT - O sistema em teste
Um dos conceitos fundamentais ao escrever testes é saber o que estamos testando. O termo SUT, System Under Test4 é utilizado para se referir à "coisa" que está sendo testada. Esse "sistema" pode ser uma função, um método, uma classe ou até mesmo um conjunto de componentes que interagem entre si.
Falando mais diretamente, o SUT é simplesmente o código ou a parte do sistema que você está colocando à prova. Quando você escreve um teste, está criando uma situação em que você valida o comportamento de um componente específico do sistema. Esse componente é o SUT.
No nosso exemplo até aqui, o SUT tem sido a função inverte_string. Essa função é o código que está sendo testado, pois o objetivo do teste é garantir que ela se comporte conforme esperado em relação aos dados que colocamos na fase de arrange.
Mas por que nomear isso?
A nomeação e a definição clara do SUT são essenciais, especialmente quando estamos lidando com sistemas mais complexos, que podem envolver múltiplos componentes interagindo entre si. Quando escrevemos um teste, queremos garantir que estamos validando somente o comportamento de um componente específico e não o comportamento de toda a aplicação ou de partes indesejadas do sistema.
Vamos expandir nosso com mais duas funções:
def inverte_string(texto: str):
"""Inverte qualquer string passada."""
return texto[::-1]
def coloca_string_em_caixa_alta(texto: str):
"""Coloca uma string em caixa alta."""
return text.upper()
def inverte_string_e_coloca_em_caixa_alta(texto: str):
"""Inverte qualquer string passada e a coloca em caixa alta."""
return coloca_string_em_caixa_alta(inverte_string(texto))
Olhando para esse módulo agora, saber o que está sendo testado, no momento em que está sendo testado, é extremamente essencial. Nesse exemplo fornecido, temos três funções, e cada uma delas pode ser considerada um SUT isolado. Deveríamos escrever testes que testam o escopo único de cada função.
Porém, temos uma função um pouco diferente: inverte_string_e_coloca_em_caixa_alta, ela é meio que um SUT composto, pois ele executa as outras duas funções internamente. E garantir seu funcionamento depende de que os outros componentes do código funcionem como esperado.
DOC - Componente dependente
No contexto de testes de software, o termo DOC (Dependent Object Component)5 é utilizado para descrever um componente que depende de outro ou de outros para funcionar corretamente.
Quando temos funções ou métodos que dependem de outros componentes, essas dependências precisam ser consideradas para garantir que o comportamento do sistema seja validado de corretamente.
flowchart LR
SUT -- tem um --> DOC
Test -- chama --> SUT
SUT -- valida a resposta --> Test
Nesse caso, dizemos que estamos garantindo o comportamento dos DOCs indiretamente. Contudo, o nosso SUT sofre interferências externas nesse caso:
Caso o resultado dessa comparação dê False, não podemos garantir que o SUT nesse caso é quem não está funcionando corretamente.
Existem diversas técnicas para lidar com isso, que veremos no futuro. O importante desse tópico é entender que nem todo SUT é isolado e com comportamentos extremamente previsíveis. Que é mais próximo do que geralmente acontece na vida real.
Nossos primeiros testes, em resumo
Agora que já entendemos os conceitos básicos de testes, vamos aplicá-los escrevendo nossos primeiros testes! O objetivo aqui é criar um arquivo que contenha as funções a serem testadas e, logo em seguida, escrever os testes usando o comando assert para garantir que cada função se comporte como esperamos.
Aqui estão as funções que vamos testar:
Estas três funções realizam operações simples e já as usamos durante todo o andar da aula:
inverte_string: Inverte a ordem dos caracteres de uma string.coloca_string_em_caixa_alta: Converte todos os caracteres de uma string para caixa alta.inverte_string_e_coloca_em_caixa_alta: Inverte a string e, em seguida, coloca o resultado em caixa alta.
Agora, vamos escrever os testes para validar se as funções estão se comportando como esperado. Para isso, usaremos o comando assert para comparar o valor obtido pela execução da função com o valor esperado.
Cenários de testes
1 Teste da função inverte_string:
- Objetivo: Verificar se a função de inversão de string está funcionando corretamente.
- Cenário: A entrada será a string
'alface'e esperamos que a saída seja'ecafla'após a inversão.
| aula_01.py | |
|---|---|
2 Teste da função coloca_string_em_caixa_alta:
- Objetivo: Verificar se a função de conversão para caixa alta está funcionando corretamente.
- Cenário: A entrada será a string
'alface'e esperamos que a saída seja'ALFACE', com todos os caracteres em maiúsculo.
| aula_01.py | |
|---|---|
3 Teste da função inverte_string_e_coloca_em_caixa_alta:
- Objetivo: Verificar se a função composta está funcionando corretamente, ou seja, se ela inverte a string e depois coloca a string invertida em caixa alta.
- Cenário: A entrada será a string
'alface'e esperamos que a saída seja'ECAFLA', pois a string é invertida para'ecafla'e, em seguida, transformada para'ECAFLA'.
| aula_01.py | |
|---|---|
Ao escrever nossos primeiros testes, conseguimos aplicar os conceitos básicos de verificação e validação de código. Usando o comando assert, garantimos que o comportamento das funções esteja conforme esperado, sem precisar de uma interface gráfica ou outras ferramentas complexas.
Esses testes simples podem ser a base para uma estratégia de testes mais robusta e abrangente à medida que o sistema cresce. Eles ajudam a garantir que o código funciona corretamente desde o início e tornam mais fácil identificar problemas à medida que o código evolui. Se um teste falhar, podemos rapidamente identificar o erro e corrigir o comportamento indesejado.
Conclusão
Nesta aula, tivemos nosso primeiro contato com a prática de escrever testes automatizados para validar o funcionamento do nosso código. Com o uso do assert, aprendemos como garantir que uma função se comporte de acordo com as expectativas e como detectar possíveis erros logo no início do processo de desenvolvimento.
Também exploramos conceitos fundamentais, como o SUT (System Under Test), que representa a função ou componente que estamos validando; o DOC (Dependent Object Component), sendo os componentes dos quais o SUT depende para funcionar corretamente; e o padrão AAA (Arrange, Act, Assert), que nos ajuda a organizar nossos testes de maneira clara e eficiente. Com esses conceitos, conseguimos estruturar nossos testes de forma que validem o comportamento esperado do sistema e nos ajudem a identificar falhas mais rapidamente.
Exercícios
- Escreva mais três cenários de teste para a função
inverte_string. - Escreva mais três cenários de teste para a função
coloca_string_em_caixa_alta - Escreva mais três cenários de teste para a função
inverte_string_e_coloca_em_caixa_alta
Para a próxima aula
Embora esse formato que aprendemos seja útil para pensar sobre os testes, em um projeto real, o uso de ferramentas de teste automatizado como unittest, pytest ou doctest pode ser muito mais eficiente. Essas ferramentas oferecem funcionalidades incríveis, que adicionam uma camada mais simples e também estruturas prontas para fazer testes melhores.
Pensando nisso, gostaria de recomendar alguns materiais também produzidos por mim, para que você, caso esteja com sede, possa consultar:
- O mínimo que você deveria saber sobre testes unitários.
- Uma introdução aos testes: Como fazer? | Live de Python #232
- Pytest: Uma introdução | Live de Python #167
Agora que a aula acabou, é um bom momento para você relembrar alguns conceitos e fixar melhor o conteúdo respondendo ao questionário referente a ela.
-
Com sobrecarga de operadores relacionados a comparação. ↩
-
Esses blocos são inseridos no mkdocs via markdown-exec. Que é capaz de executar outras coisas além do pyodide em blocos de markdown. ↩
-
Se lê "triplo A" ou em inglês "triple A". ↩
-
Termo cunhado por Gerard Meszaros em seu livro xUnit Patterns. ↩
-
Termo cunhado por Gerard Meszaros em seu livro xUnit Patterns. ↩