4 changed files with 876 additions and 1 deletions
@ -0,0 +1,4 @@
|
||||
+++ |
||||
transparent = true |
||||
title = "Thoughts" |
||||
+++ |
@ -0,0 +1,431 @@
|
||||
+++ |
||||
title = "Reveries About Testing" |
||||
date = 2020-01-13 |
||||
|
||||
[taxonomies] |
||||
tags = ["tests", "testing", "integration tests", "unit tests"] |
||||
+++ |
||||
|
||||
Today, a large number of developers use some testing methodology. But what are |
||||
tests? What are they for? What is the purpose of writing testes, anyway? Are |
||||
we testing the right things? |
||||
|
||||
<!-- more --> |
||||
|
||||
{% note() %} |
||||
This is a companion post for one my presentations, [Filosofando Sobre |
||||
Testes](https://presentations.juliobiason.me/filosofando-testes.html), which |
||||
is in Portuguese. |
||||
{% end %} |
||||
|
||||
Before we start, let me give you some disclaimers: |
||||
|
||||
1. **I'm non-orthodox about tests**. What I mean by that is that some of stuff |
||||
I'll mention here are exactly the opposite of what everyone says and the |
||||
opposite of the way people work with tests. |
||||
|
||||
2. In no way, consider this a set of rules. What I really want is to stop |
||||
people from writing tests without knowing why they are writing those tests. |
||||
|
||||
3. You don't need to agree with anything here. Again, the idea is to sotp and |
||||
think what is being tested before writing tests. |
||||
|
||||
What I want to discuss: |
||||
|
||||
1. TDD, Kent Beck Style; |
||||
2. "Fast Tests, Slow Tests"; |
||||
3. The Explosion of Slow Tests; |
||||
4. Coverage; |
||||
5. Mocks. |
||||
|
||||
## TDD, Kent Beck Style |
||||
|
||||
What made me rethink the way I wrote tests was a video by Ian Cooper, called |
||||
["TDD, where it all go wrong"](https://vimeo.com/68375232). In this video, |
||||
Cooper points out that the Beck's book (which brought the whole TDD |
||||
revolution) says two things: |
||||
|
||||
1. Tests should run in an isolated way, nothing more, nothing less; |
||||
2. Avoid testing implementation details, test behaviors. |
||||
|
||||
The first point is what it means for a "unit test", meaning "run in |
||||
isolation", in the sense that the test does not depend on others. This way, |
||||
"unit tests" should be seen as "the test is a unit", not "testing units" -- |
||||
there are no "units", the test itself is a unit that doesn't depend on |
||||
anything else. |
||||
|
||||
The second point is that one should test behaviors, not the implementation. |
||||
This is a point that I see we fail a lot when we talk about testing every |
||||
class/function: What if the expected behavior is the combination of two |
||||
classes? Is it worth writing tests for both, if splitting the classes (or |
||||
functions) is just a matter of implementation/way to simplify the code? |
||||
|
||||
Also, another question for writing tests for every function and every class: |
||||
What we have know an application are their input channels -- which could be a |
||||
button in a graphical interface, an option passed in the command line or a web |
||||
request -- and the output channels; that way, the behavior is "given that |
||||
input in the input channel, I want this in the output channel", and everything |
||||
in the middle is implementation. And, for that transformation of something in |
||||
the input channel to the output channel, I may need a combination of |
||||
classes/functions; if I test every class/function, am I really testing the |
||||
expected behavior or the implementation? |
||||
|
||||
"But this looks like BDD!", you must be thinking. Cooper says this in the |
||||
video above: the idea of "test every class/function" became the norm, the |
||||
point of checking behavior had to be used somewhere else, what gave us ATDD |
||||
(Acceptance-Test Driven Development) and BDD (Behavior Driven Development). |
||||
|
||||
An example of testing behaviors: In the Django subreddit, there was a |
||||
question: [Should I write unit tests for Django's built in types?](https://www.reddit.com/r/django/comments/5bearg/should_i_write_unit_tests_for_djangos_built_in/) |
||||
The question is basically this: Django allows defining the database model, and |
||||
from that model create a form that I can put on my templates and validate the |
||||
incoming data; that way, if I defined that there is a field in my model called |
||||
"Year of Birth" -- which can only receive numbers -- and I create a form based |
||||
on the model, put it on my template, send the data back and Django will make |
||||
sure, from the type in the model, that the incoming request will have a number |
||||
in that field. Should I still write a test for that? |
||||
|
||||
The answer, though, is in take a step back and do the following question: Why |
||||
the year is a number? Obviously, 'cause years are defined as numbers[^1] and |
||||
the behavior of the field was defined way before we added the field in the |
||||
model. Also, supposed that for some reason, I need to store the field as a |
||||
string[^2]; if the type changes, the behavior should also change? Probably |
||||
not. |
||||
|
||||
When I ignored that the year should be a number 'cause "the framework will |
||||
take care of it", I ignored the expected behavior due the implemtantation. |
||||
|
||||
And "test behaviors, not the implementation". |
||||
|
||||
Non-factual, but an anecdote: In a project, we had an "alarm manager" where, |
||||
from an event, it should generate only a log entry, generate a log entry and |
||||
send a SNMP signal or, if the user set it, generate a log, send a SNMP signal |
||||
and turn on a LED in the front of the device. With that, we created a module |
||||
for the log, a module for sending SNMP signals and a module to turn on/off the |
||||
LEDs. Every module had tests, but we didn't feel comfortable with it yet. |
||||
That's when I suggested we write a test that would bring the service up and |
||||
send events to it, just like any other application in the system, and check |
||||
what would happen. That's when the tests finally made sense. (I'll still talk |
||||
about these tests in the coverage part of this post.) |
||||
|
||||
## Fast Tests, Slow Tests |
||||
|
||||
The counterpoint of the points above can be something similar to what Gary |
||||
Bernhardt says in his presentation [Fast Test, Slow |
||||
Test](https://www.youtube.com/watch?v=RAxiiRPHS9k). In it, Bernhardt mentions |
||||
that they changed the way the tests work, and that now they could run |
||||
hundreds of tests in less than a second (an example shows around 600 tests |
||||
being run in 1.5 seconds). |
||||
|
||||
What Bernhardt suggest is to write tests that checks online the models, with |
||||
no connection to the database or the views; tests for controllers with no |
||||
connection to the models or views; and tests for the views without the |
||||
controllers. |
||||
|
||||
Does that sound familiar (specially if you use a MVC framework, which puts |
||||
each of those layers in different classes)? |
||||
|
||||
Still about those tests, Bernhardt points that those "quick runs" help the |
||||
developers to test their to test their changes quickly (does that still sound |
||||
familiar?) but those tests do not replace the "integration tests". |
||||
|
||||
In that point, I have to ask: If the tests are written to check if a |
||||
controller can be run without being connected to the rest of the system, but |
||||
one still have to write the (so called) integration tests to verify that the |
||||
project is delivering whatever was promised it would deliver, what is really |
||||
being tested here? The impression I have from the type of test Bernhardt |
||||
proposes is more to check _architectural adherence_ than a quality test: Does |
||||
this controller follow the structure of not having any connections to the |
||||
database itself? Does this model has only functions related to the storage and |
||||
retrieval of data, without any logic? If that's it, what is the value for my |
||||
user if a controller doesn't access the database directly? |
||||
|
||||
It's not that I don't believe those tests have no value, but they give the |
||||
impression that, in the long run, they tend to become structurally very |
||||
similar while the (so called) integration tests tend to give more returns to |
||||
the quality of the project: Tests that defined an input and an expected result |
||||
tend to make sure that, in the long run, the functionality of the project will |
||||
still be the same. |
||||
|
||||
## The Explosion of Slow Tests |
||||
|
||||
The first thing that may pop up with a point like the above is that |
||||
"integration tests are slow and that will make the tests slow and make |
||||
developers less productive." |
||||
|
||||
Yes, integration tests are slow, specially 'cause there is a good leg of work |
||||
in creating the expected initial state, all the inputs as expected by the |
||||
I/O system (again, graphical interface, command line, web), run the whole |
||||
processing stack and verify the result. And yes, waiting all this time may end |
||||
up breaking the developer's flow. |
||||
|
||||
On the other hand, when a developer is working with some input, if it is a new |
||||
functionality/expected behavior, then there should be a test for this |
||||
behavior; if there is a change in the expected behavior, there should be a |
||||
test for the old behavior that needs to be changed. Running _just_ this test is |
||||
enough? No, but it should give a very good indication if the functionality is |
||||
working as expected. After making sure the behavior is correct, the developer |
||||
may execute the suite of tests for the thing being changed and let everything |
||||
else to the CI. |
||||
|
||||
For example, if I'm working in a new functionality to show an error message |
||||
when there is an invoice when the product is not in stock, I have to write a |
||||
test that creates the product, let it with no pieces in stock, make an invoice |
||||
and check for the error message. Once this test checks the behavior is |
||||
correct, I can run all the other invoice tests, and then let the CI validate |
||||
that I didn't break anything else else in the stock management module or even |
||||
the customer module (for some reason). |
||||
|
||||
And plugging with the first point, in order to do all the checks, I'd probably |
||||
need lots of functions/classes and test every single one of them will not make |
||||
sure the expected behavior is correct, but I'll get back to this later in the |
||||
coverage part. |
||||
|
||||
I have the impression that we don't use this kind of stuff due two different |
||||
problems: the first is that testing tools have a very bad interface for |
||||
running suite of tests (for example, running all the invoice tests and _only_ |
||||
the invoice tests); the second is that developers are lazy, and it's a lot |
||||
easier to run all tests than picking a single suite (not to mention organizing |
||||
said tests in suites to be run that way). |
||||
|
||||
## Coverage |
||||
|
||||
Against what most people claim, I do believe that you can reach 100% coverage |
||||
pretty easily: You just need to delete code. |
||||
|
||||
The idea is quite simple, actually: If your tests check the system behavior, |
||||
and I'm proving that all tests pass, everything that doesn't have coverage |
||||
point to code that isn't necessary and, thus, can be deleted. |
||||
|
||||
It is not every code that can be removed. In the alarm manager example, even |
||||
when our "unit tests" cover all functionalities of each module, we got a block |
||||
in the "integration tests" that didn't have any coverage. This block was |
||||
responsible for checking the input of a module (for example, it won't allow |
||||
sending a SNMP message without a text). But, when we checked the code, we |
||||
realized that the base module (the one calling the others) was already doing |
||||
that validation and that this check was unnecessary. This lead into the |
||||
discussion of which test (and code block) should be removed. But we did have a |
||||
piece of "dead code" that was being marked as "alive" because we had unit |
||||
tests for both blocks. |
||||
|
||||
A more practical example. Imagine there is a class that keeps customer data in |
||||
a web shop[^3]: |
||||
|
||||
```python |
||||
class Client: |
||||
def __init__(self, name): |
||||
self.name = name |
||||
``` |
||||
|
||||
After awhile, comes a new requirement: A certain "Dewey" keeps creating |
||||
accounts non-stop, without doing any purchases, just to put trash in the |
||||
database; we need to block any new customers that make their name as just one |
||||
name. |
||||
|
||||
Then, thinking about SOLID[^4], the developer changes teh code to this: |
||||
|
||||
```python |
||||
def _multiple_names(name): |
||||
split_names = name.split(' ') |
||||
return len(split_names) > 1 |
||||
|
||||
def _validate_name(name): |
||||
if not _multiple_names(name): |
||||
raise Exception("Invalid name") |
||||
return name |
||||
|
||||
class Client: |
||||
def __init__(self, name): |
||||
self.name = _validate_name(name) |
||||
``` |
||||
|
||||
Now, when there is any service trying to create a new customer, the name is |
||||
validated against a certain rules and one of those is that the name must have |
||||
multiple names[^5]. |
||||
|
||||
New funcionality, new tests, right? |
||||
|
||||
```python |
||||
import pytest |
||||
|
||||
def test_single_name(): |
||||
"""'Cher' não tem multiplos nomes.""" |
||||
assert not _multiple_names('Cher') |
||||
|
||||
def test_multiple_name(): |
||||
"""'Julio Biason' tem múltiplos nomes.""" |
||||
assert _multiple_names('Julio Biason') |
||||
|
||||
def test_valid_name(): |
||||
"""'Julio Biason' é um nome válido.""" |
||||
_validate_name('Julio Biason') |
||||
|
||||
def test_invalid_name(): |
||||
"""'Cher' não é um nome válido e por isso levanta uma exceção.""" |
||||
with pytest.raises(Exception): |
||||
_validate_name('Cher') |
||||
|
||||
def test_client_error(): |
||||
"""Se tentar criar uma conta com 'Cher', deve dar erro.""" |
||||
with pytest.raises(Exception): |
||||
Client(name='Cher') |
||||
|
||||
def test_client(): |
||||
"""Uma conta com nome 'Julio Biason' deve funcionar.""" |
||||
Client(name='Julio Biason') |
||||
``` |
||||
|
||||
Running the tests: |
||||
|
||||
``` |
||||
$ pytest --cov=client client.py |
||||
==== test session starts ==== |
||||
plugins: cov-2.4.0 |
||||
collected 6 items |
||||
|
||||
client.py ...... |
||||
|
||||
---- coverage: platform linux, python 3.4.3-final-0 ---- |
||||
Name Stmts Miss Cover |
||||
------------------------------- |
||||
client.py 25 0 100% |
||||
|
||||
==== 6 passed in 0.11 seconds ==== |
||||
``` |
||||
|
||||
100% coverage and new functionality done! The developer give themselves some |
||||
pats in the back and go home. |
||||
|
||||
But, in the middle of the night, one of the managers who is also a friend of |
||||
Björk, gets a call from her telling that she tried to buy something and just |
||||
got an error message. The developer gets in the office next morning, sees the |
||||
manager email complaining about their famous friend being blocked and goes |
||||
into fixing the code: |
||||
|
||||
```python |
||||
class Client: |
||||
def __init__(self, name): |
||||
self.name = name |
||||
``` |
||||
|
||||
There, no more validation[^6] e now Björk can buy whatever she wants. But |
||||
running the tests: |
||||
|
||||
``` |
||||
==== FAILURES ==== |
||||
____ test_client_error ____ |
||||
|
||||
def test_client_error(): |
||||
with pytest.raises(Exception): |
||||
> Client(name='Cher') |
||||
E Failed: DID NOT RAISE <class 'Exception'> |
||||
|
||||
client.py:37: Failed |
||||
==== 1 failed, 5 passed in 0.63 seconds ==== |
||||
``` |
||||
|
||||
Oh, sure! Cher is now a valid name and that behavior being tested is invalid. |
||||
We need to change the test to accept Cher: |
||||
|
||||
```python |
||||
def test_client_error(): |
||||
"""Se tentar criar uma conta com 'Cher', deve funcionar.""" |
||||
Client(name='Cher') |
||||
``` |
||||
|
||||
And running the tests once again: |
||||
|
||||
``` |
||||
$ pytest --cov=client client.py |
||||
==== test session starts ==== |
||||
rootdir: /home/jbiason/unitt, inifile: |
||||
plugins: cov-2.4.0 |
||||
collected 6 items |
||||
|
||||
client.py ...... |
||||
|
||||
---- coverage: platform linux, python 3.4.3-final-0 ---- |
||||
Name Stmts Miss Cover |
||||
------------------------------- |
||||
client.py 24 0 100% |
||||
|
||||
==== 6 passed in 0.12 seconds ==== |
||||
``` |
||||
|
||||
Wonderful! Everything is working with the expected behaviors and we still have |
||||
100% coverage. |
||||
|
||||
But can you spot the problem in the code? |
||||
|
||||
The problem is now that `_multiple_names` is not being used anywhere, but it |
||||
is shown as "alive" 'cause there is a lost test that keeps saying that the |
||||
code is being used. If we had started with just the behavior tests -- |
||||
using just the system inputs and outputs -- right out of the bat we would see |
||||
that the function is not used anymore -- and if we need it in the future... |
||||
well, that's what version control systems are for. |
||||
|
||||
Although this looks like a silly example, there are some cases in which the |
||||
processing flow can be changed by the environment itself. For example, in |
||||
Django, you can add "middleware" classes, which are capable of intercepting |
||||
Requests or Responses and change their result. The most common example of |
||||
middleware is the Authentication, which adds the logged user information in |
||||
the Request; but those changes can be more deep, like changing some form |
||||
information. In those cases, the input (or the output, or both) is changed |
||||
and writing tests that ignore the middleware will not be a correct |
||||
representation of the input (or output, or both) of the system. And there we |
||||
can ask if the test is valid 'cause it is using a state that should not exist |
||||
in the normal use of the system. |
||||
|
||||
## Mocks |
||||
|
||||
Some time ago, I used to say that mocks should be used for things external to |
||||
the system. But I realized that definition is not quite correct -- there are |
||||
external things that shouldn't be mocked -- and that a better definition for |
||||
what should be mocked is "anything that you have no control". |
||||
|
||||
For example, if you're writing a system that uses IP geolocation using an |
||||
external service, you probably will mock the call for that service, as it is |
||||
out of your control. But a call to a database, when you're using a |
||||
system/framework that abstracts all the calls for the database (like Django |
||||
does), then the database, even being an external resource, is still under your |
||||
control and, thus, shouldn't be mocked -- but since the system/framework |
||||
offers a database abstraction, using any database shouldn't affect the |
||||
results. |
||||
|
||||
Another example are microservices. Even microservices inside the same company |
||||
or steam are external and out of control of the project and, thus, should be |
||||
mocked. "But they are from the same team!" Yes, but they are not part of the |
||||
same project and a) the idea behind microservices is to uncouple those |
||||
services and/or b) are in different directory trees. One of the advantages of |
||||
microservices from the same team is that the expected contract from one is |
||||
know by the team and that could be easily mocked (the team knows that, calling |
||||
a service with X inputs, it should receive an Y response -- or error). |
||||
|
||||
# Conclusion |
||||
|
||||
Again, the idea is not to rewrite every test that you have 'cause "the right |
||||
way is my way". On the other hand, I see a lot of tests being written in any |
||||
way, just using the context of "one test for each function/class" and, in some |
||||
cases, that doesn't make any sense and should get a little more thinking. By |
||||
exposing those "impure thoughts" about tests, I hope that would make people |
||||
rethink the way they are writing their tests |
||||
|
||||
--- |
||||
|
||||
[^1]: Unless you want to use roman numerals, but anyway... |
||||
|
||||
[^2]: The reason for being changed to a string can be anything: due some |
||||
security plugin, 'cause we are using a database that doesn't work properly |
||||
with integers, 'cause we are plugging this system with a legacy one... |
||||
|
||||
[^3]: A class that keeps customer information should be way more complex that |
||||
this, but let's keep it simple just for this example. |
||||
|
||||
[^4]: No, that's not really SOLID, but that's keep it simple again for this |
||||
example. |
||||
|
||||
[^5]: Someone will send me the "Fallacies Developers Believe About User Names" |
||||
links for this, right? |
||||
|
||||
[^6]: Sure, I should change just `_validate_name`, but this way it makes it |
||||
even more clear what the problem is. |
@ -0,0 +1,440 @@
|
||||
+++ |
||||
title = "Devaneios Sobre Testes" |
||||
date = 2020-01-13 |
||||
|
||||
[taxonomies] |
||||
tags = ["testes", "testes de integração", "testes unitários", "companion post"] |
||||
+++ |
||||
|
||||
Hoje em dia, boa parte dos desenvolvedores utiliza alguma metodologia de |
||||
testes. Mas o que são os testes? Para que servem? Qual o objetivo de se |
||||
testar? Estamos testando as coisas certas? |
||||
|
||||
<!-- more --> |
||||
|
||||
{% note() %} |
||||
Esse post acompanha a minha apresentação de [Filosofando Sobre |
||||
Testes](https://presentations.juliobiason.me/filosofando-testes.html). |
||||
{% end %} |
||||
|
||||
Antes de começar, alguns avisos: |
||||
|
||||
1. **Eu sou não ortodoxo com relação a testes**. Com isso eu quero dizer que |
||||
muitas das coisas que eu vou comentar aqui são exatamente contrárias do que |
||||
todo mundo fala e da forma como muitos trabalham com testes. |
||||
|
||||
2. De forma alguma, considere esse conteúdo como regras. O que eu quero é que |
||||
as pessoas parem de sair criando testes sem saber porque estão fazendo |
||||
esses testes. |
||||
|
||||
3. Ainda, de forma alguma você precisa concordar com alguma coisa aqui. De |
||||
novo, a ideia é parar para pensar no que está sendo testado antes de sair |
||||
testando. |
||||
|
||||
Agenda de coisas que eu vou comentar: |
||||
|
||||
1. TDD no estilo Kent Beck; |
||||
2. "Fast Tests, Slow Tests"; |
||||
3. Explosão de Testes Lentos; |
||||
4. Coverage; |
||||
5. Mocking. |
||||
|
||||
## TDD no Estilo Kent Beck |
||||
|
||||
O que me levou a repensar a forma como eu escrevia testes foi um vídeo do Ian |
||||
Cooper chamado ["TDD, where it all go wrong"](https://vimeo.com/68375232) |
||||
("TDD, aonde é que a coisa deu errado"). No vídeo, Cooper coloca que o livro que |
||||
Beck escreveu (que deu origem a toda a revolução do TDD) diz duas coisas: |
||||
|
||||
1. Testes devem ser executados de forma isolada, nada mais, nada menos. |
||||
2. Evite testar detalhes de implementação, teste comportamentos. |
||||
|
||||
O primeiro ponto é o que fala sobre "unit tests", significando "rodam de forma |
||||
isolada", no sentido em que um teste não depende de outro. Dessa forma, "unit |
||||
tests" seriam traduzidos como "testes unitários", não "testes de unidade" -- |
||||
não há "unidade", o teste em si é uma unidade única que não depende de outras |
||||
coisas. |
||||
|
||||
O segundo ponto é que deve ser testado o comportamento, não a implementação. |
||||
Esse é um ponto que eu vejo falhar um bocado quando pensamos em testar todo e |
||||
qualquer classe e/ou função: E se o comportamento esperado é a combinação de |
||||
duas classes? Vale a pena escrever testes para as duas, sendo que a questão de |
||||
separar em duas classes diferentes (ou duas funções diferentes) é apenas uma |
||||
questão de implementação/simplicidade de código? |
||||
|
||||
Ainda, outro questionamento sobre testar todas as funções e todas as classes: |
||||
o que sabemos de uma aplicação são os canais de entrada -- que pode ser por um |
||||
botão em uma interface gráfica, um texto digitado na linha de comando ou uma |
||||
requisição web -- e os canais de saída; assim, o _comportamento_ esperado é |
||||
"dado essa entrada pelo canal de entrada, quero ter essa saída", e qualquer |
||||
coisa no meio é implementação. E para fazer a transformação de uma |
||||
entrada para uma saída específica, eu posso precisar de mais de uma função |
||||
e/ou classe; se eu estiver testando cada uma das funções, eu estou realmente |
||||
testando o comportamento ou a implementação? |
||||
|
||||
"Mas isso é muito parecido com BDD!", você deve estar pensando. Cooper coloca |
||||
isso no vídeo acima: Como a ideia de "testar cada função/classe" se tornou a |
||||
norma do TDD, a questão do comportamento teve que ser colocado em outro |
||||
formato, o que deu origem ao ATDD (Acceptance-Test Driven Development, |
||||
Desenvolvimento Guiado por Testes de Aceitação) e BDD (Behaviour Driven |
||||
Development, Desenvolvimento Guiado por Comportamentos). |
||||
|
||||
Um exemplo de testes de comportamento: No Subreddit do Django, foi criada |
||||
uma pergunta: [Devo Escrever Testes Para os Tipos Built-In do |
||||
Django?](https://www.reddit.com/r/django/comments/5bearg/should_i_write_unit_tests_for_djangos_built_in/) |
||||
A questão se resume ao seguinte: Sabendo que no Django eu tenho tipos |
||||
definidos para meus dados no banco, e a partir dessas definições eu posso |
||||
criar formulários para colocar nos meus templates e esses formulários também |
||||
servem para validar os dados de entrada; assim, se eu defini que há um campo |
||||
no meu banco chamado "Ano de nascimento" -- que só pode receber números |
||||
inteiros -- e eu crio o formulário a partir do banco, coloco no meu template, |
||||
recebo os dados de volta e o próprio Django vai garantir, pelo tipo do dado no |
||||
banco, que o valor do campo é um número inteiro. Eu ainda preciso escrever um |
||||
teste para isso? |
||||
|
||||
A resposta, no entanto, está em dar um passo atrás e fazer a seguinte |
||||
pergunta: _Por que_ o ano é um inteiro? Obviamente, porque anos são definidos |
||||
como números[^1] e, portanto, o comportamento do campo foi definido bem antes do |
||||
campo ser adicionado na tabela. Ainda, imagine que, por algum acidente do |
||||
destino, eu precise guardar o ano como uma string[^2]; se o tipo foi alterado, |
||||
o _comportamento_ vai ser alterado também? Provavelmente não. |
||||
|
||||
Quando eu ignorei que ano deve ser um número porque "o framework cuida disso |
||||
pra mim", eu ignorei o comportamento esperado por culpa da implementação. |
||||
|
||||
E "teste comportamentos, não implementação". |
||||
|
||||
Embora não factual, uma anedota: Num projeto, tínhamos um "gerenciador de |
||||
alarmes" onde, a partir de um evento, poderia ser gerado simplesmente um log, |
||||
ser gerado um log e enviado um sinal SNMP ou, dependendo da configuração do |
||||
usuário, log, SNMP e ativação de um LED no painel frontal do equipamento. |
||||
Assim, criamos um módulo com a geração do log, um módulo com o envio do SNMP e |
||||
um módulo com a ativação/desativação do LED. Embora tudo tivesse testes, nós |
||||
ainda não nos sentíamos tranquilos com aquilo. Foi quando sugeri que |
||||
escrevêssemos um teste que levantasse o serviço e enviasse os eventos e ver o |
||||
que acontecia. E, finalmente, os testes fizeram sentido. (Eu ainda vou fazer |
||||
referências a esses testes na parte de cobertura.) |
||||
|
||||
## Fast Tests, Slow Tests |
||||
|
||||
O contraponto do que eu comentei acima pode ser algo parecido com [Fast Tests, |
||||
Slow Tests](https://www.youtube.com/watch?v=RAxiiRPHS9k), do Gary Bernhardt. |
||||
Em resumo, nessa apresentação, Bernhardt comenta que mudaram os testes e que |
||||
agora era possível executar centenas de testes em menos de 1 segundo (um |
||||
exemplo mostra aproximadamente 600 testes em 1.5 segundos). |
||||
|
||||
O que o Bernhardt sugere é escrever testes apenas para os models, sem conexão |
||||
com o banco ou controllers; testes de controllers sem conexão com os models ou |
||||
views; e testes de views sem controllers. |
||||
|
||||
Soa familiar (principalmente em frameworks MVC que separam cada um destes em |
||||
classes separadas)? |
||||
|
||||
Ainda sobre esses testes, Bernhardt aponta que essas "execuções rápidas" |
||||
ajudam os desenvolvedores a testar suas alterações rapidamente (ainda soa |
||||
familiar?) mas que esses testes não substituem os testes de integração. |
||||
|
||||
Nesse ponto é preciso se perguntar: se são escritos testes que verificam a |
||||
execução de um controller de forma isolada do resto do sistema, mas ainda é |
||||
necessário escrever os (chamados) testes de integração para garantir que o |
||||
projeto está entregando o que foi prometido entregar, o que é que está |
||||
realmente sendo testado? A impressão que deixa é que o estilo de teste pregado |
||||
por Bernhardt está mais para _aderência à arquitetura_ do que um teste de |
||||
qualidade do sistema: Esse controller segue a estrutura de não ter conexões de |
||||
banco em si? Esse model tem apenas as funções relacionadas com o |
||||
armazenamento e recuperação dos dados, sem qualquer lógica? Se é isso, qual o |
||||
valor para meu usuário se o controller não faz nenhuma gravação no banco de |
||||
dados? |
||||
|
||||
Não que eu acredite que testes desse tipo sejam desnecessários, mas eles |
||||
deixam a impressão que, a longo prazo, eles tendem a se parecerem, |
||||
estruturalmente, muito parecidos, enquanto que (assim chamados) testes de |
||||
integração tendem a dar mais retorno a longo prazo para a qualidade do |
||||
projeto: Testes que definem uma entrada e um resultado esperado tendem a |
||||
garantir que, a longo prazo, o funcionamento do sistema continuará sendo igual. |
||||
|
||||
## Explosão de Testes Lentos |
||||
|
||||
A primeira consideração que surge numa declaração como a de cima é que "testes |
||||
de integração são lentos e isso vai tornar os testes lentos e reduzir a |
||||
produtividade dos desenvolvedores." |
||||
|
||||
Sim, testes de integração são lentos, principalmente porque há um bom trabalho |
||||
em criar o estado inicial esperado, todas as entradas conforme esperado pelo sistema |
||||
de I/O (de novo, interface gráfica, linha de comando, web), percorrer todo o |
||||
percurso do processamento e verificar a saída. E sim, esperar esse tempo de |
||||
execução pode acabar distraindo o desenvolvedor. |
||||
|
||||
Entretanto, quando um desenvolvedor está trabalhando num tratamento de algum |
||||
dado, se for uma nova funcionalidade/comportamento esperado, obviamente um |
||||
teste desse comportamento deve ser criado; se é uma alteração de |
||||
comportamento, deve haver um teste do comportamento esperado e esse deve ser |
||||
corrigido. Executar _apenas_ esse teste é o suficiente? Não, mas já dá boas |
||||
indicações de que a funcionalidade está funcionando como prevista. Depois de |
||||
garantir que a funcionalidade está correta, o desenvolvedor pode executar a |
||||
suite de testes do elemento sendo alterado e deixar o resto para o CI. |
||||
|
||||
Por exemplo, se eu estiver trabalhando numa funcionalidade nova de mostrar uma |
||||
mensagem de erro caso seja feito um pedido quando o produto pedido não exista |
||||
no estoque, eu tenho que escrever um novo teste que crie um produto, deixe-o |
||||
com uma quantidade 0 em estoque, faça o pedido de compra e verifique que houve |
||||
erro. Uma vez que esse teste confirme a funcionalidade, eu posso rodar os |
||||
demais testes de pedidos, e depois disso deixar o CI validar que eu não |
||||
quebrei nada no gerenciamento de estoque ou cadastro de clientes (por algum |
||||
motivo). |
||||
|
||||
E note que provavelmente para fazer todas essas validações, eu ainda vou |
||||
precisar de várias funções/classes e testar cada uma em separado não garante a |
||||
funcionalidade, mas eu vou voltar a esse tópico quando tiver falando de |
||||
cobertura. |
||||
|
||||
Isso me parece o mais complicado pois parece haver, ao mesmo tempo, uma |
||||
interface muito ruim das ferramentas de testes para executar suites de testes |
||||
(somente os testes de pedidos, no exemplo anterior) e preguiça em executar |
||||
apenas os testes da suite (é mais fácil chamar o comando que roda todos os |
||||
testes que lembrar do caminho específico da suite -- sem contar organização de |
||||
suites para isso). |
||||
|
||||
## Coverage |
||||
|
||||
Ao contrário de que muitos comentam por aí, eu realmente acredito que seja bem |
||||
viável chegar a 100% de cobertura de testes: Basta apagar código. |
||||
|
||||
A ideia é bem simples, na verdade: Se meus testes testam o comportamento do |
||||
sistema, e eu estou garantindo que esses testes passam, qualquer coisa que não |
||||
tenha cobertura indica que o código não é necessário e que, portanto, pode ser |
||||
removido. |
||||
|
||||
Entretanto, não é qualquer código que possa ser apagado. No exemplo do |
||||
gerenciador de alarmes, apesar dos "testes unitários" cobrirem todas as |
||||
funcionalidades, aconteceu que no "teste de integração" surgiu um bloco de |
||||
código que nunca era executado. Esse bloco era responsável por validar a |
||||
entrada de um dos módulos (garantindo que não seria possível enviar um SNMP |
||||
sem mensagem, por exemplo). Entretanto, ao examinar o código durante a |
||||
execução, nós percebemos que o módulo base já estava fazendo essa validação e |
||||
que o código de proteção do módulo jamais seria chamado. Obviamente, essa é |
||||
uma questão sobre qual dos dois testes deveria ser eliminado. Mas nós tínhamos |
||||
"código morto", considerado "vivo" porque um "teste unitário" estava passando |
||||
pelas duas validações. |
||||
|
||||
Um exemplo mais prático. Imagine uma classe que guarde dados de clientes de |
||||
um serviço web de compras[^3]: |
||||
|
||||
```python |
||||
class Client: |
||||
def __init__(self, name): |
||||
self.name = name |
||||
``` |
||||
|
||||
Depois de um tempo, surge um novo requisito: Um tal de "Zézinho" |
||||
está criando usuários sem parar, sem fazer compras, só pra sujar o banco; |
||||
devemos bloquear todos os cadastros que tenham como nome do usuário apenas um |
||||
nome. |
||||
|
||||
Aí, pensando em SOLID[^4], o desenvolvedor altera o código para o seguinte: |
||||
|
||||
```python |
||||
def _multiple_names(name): |
||||
split_names = name.split(' ') |
||||
return len(split_names) > 1 |
||||
|
||||
def _validate_name(name): |
||||
if not _multiple_names(name): |
||||
raise Exception("Invalid name") |
||||
return name |
||||
|
||||
class Client: |
||||
def __init__(self, name): |
||||
self.name = _validate_name(name) |
||||
``` |
||||
|
||||
Agora o que acontece é que quando um cliente é criado, são passadas as |
||||
validações sobre o nome, e uma dessas é que o nome deve ter múltiplos |
||||
nomes[^5]. |
||||
|
||||
Nova funcionalidade, precisamos de novos testes, certo? |
||||
|
||||
```python |
||||
import pytest |
||||
|
||||
def test_single_name(): |
||||
"""'Cher' não tem multiplos nomes.""" |
||||
assert not _multiple_names('Cher') |
||||
|
||||
def test_multiple_name(): |
||||
"""'Julio Biason' tem múltiplos nomes.""" |
||||
assert _multiple_names('Julio Biason') |
||||
|
||||
def test_valid_name(): |
||||
"""'Julio Biason' é um nome válido.""" |
||||
_validate_name('Julio Biason') |
||||
|
||||
def test_invalid_name(): |
||||
"""'Cher' não é um nome válido e por isso levanta uma exceção.""" |
||||
with pytest.raises(Exception): |
||||
_validate_name('Cher') |
||||
|
||||
def test_client_error(): |
||||
"""Se tentar criar uma conta com 'Cher', deve dar erro.""" |
||||
with pytest.raises(Exception): |
||||
Client(name='Cher') |
||||
|
||||
def test_client(): |
||||
"""Uma conta com nome 'Julio Biason' deve funcionar.""" |
||||
Client(name='Julio Biason') |
||||
``` |
||||
|
||||
E ao rodar os testes: |
||||
|
||||
``` |
||||
$ pytest --cov=client client.py |
||||
==== test session starts ==== |
||||
plugins: cov-2.4.0 |
||||
collected 6 items |
||||
|
||||
client.py ...... |
||||
|
||||
---- coverage: platform linux, python 3.4.3-final-0 ---- |
||||
Name Stmts Miss Cover |
||||
------------------------------- |
||||
client.py 25 0 100% |
||||
|
||||
==== 6 passed in 0.11 seconds ==== |
||||
``` |
||||
|
||||
100% de cobertura e funcionalidade implantada! O desenvolvedor se dá uns |
||||
tapinhas nas costas e vai pra casa. |
||||
|
||||
Entretanto, durante a noite, acontece de um dos gerentes ser amigo da Xuxa, |
||||
que tentou fazer uma compra e não conseguiu. O desenvolvedor chega de manhã e |
||||
vê o email do gerente e sai a corrigir o código: |
||||
|
||||
```python |
||||
class Client: |
||||
def __init__(self, name): |
||||
self.name = name |
||||
``` |
||||
|
||||
Pronto, não tem mais validação[^6] e agora a Xuxa pode comprar. Mas ao rodar |
||||
os testes: |
||||
|
||||
``` |
||||
==== FAILURES ==== |
||||
____ test_client_error ____ |
||||
|
||||
def test_client_error(): |
||||
with pytest.raises(Exception): |
||||
> Client(name='Cher') |
||||
E Failed: DID NOT RAISE <class 'Exception'> |
||||
|
||||
client.py:37: Failed |
||||
==== 1 failed, 5 passed in 0.63 seconds ==== |
||||
``` |
||||
|
||||
A, é claro! Agora Cher é um nome válido e o comportamento testado é invalido. |
||||
Vamos mudar o teste para o comportamento esperado para a Cher: |
||||
|
||||
```python |
||||
def test_client_error(): |
||||
"""Se tentar criar uma conta com 'Cher', deve funcionar.""" |
||||
Client(name='Cher') |
||||
``` |
||||
|
||||
E rodando os testes de novo: |
||||
|
||||
``` |
||||
$ pytest --cov=client client.py |
||||
==== test session starts ==== |
||||
rootdir: /home/jbiason/unitt, inifile: |
||||
plugins: cov-2.4.0 |
||||
collected 6 items |
||||
|
||||
client.py ...... |
||||
|
||||
---- coverage: platform linux, python 3.4.3-final-0 ---- |
||||
Name Stmts Miss Cover |
||||
------------------------------- |
||||
client.py 24 0 100% |
||||
|
||||
==== 6 passed in 0.12 seconds ==== |
||||
``` |
||||
|
||||
Maravilhoso, tudo está funcionando com o comportamento esperado e ainda temos |
||||
100% de cobertura. |
||||
|
||||
Mas você consegue ver onde está o problema desse código? |
||||
|
||||
O problema é que `_multiple_names` não é mais usado em lugar algum, mas o |
||||
mesmo se mostra "vivo" porque um teste perdido continua indicando que o código |
||||
está vivo. Se tivéssemos começado com os testes de comportamento desde o |
||||
começo -- considerando entradas e saídas -- de cara veríamos que a função não |
||||
é mais necessária -- e se, num futuro, ela for... bom, é pra isso que sistemas |
||||
de controle de versão existem. |
||||
|
||||
Embora esse possa parecer um exemplo bobo, existem outros casos em que o fluxo |
||||
de processamento dos dados pode ser alterado pelo próprio ambiente. Por |
||||
exemplo, no Django, é possível ter classes "middleware", que são capazes de |
||||
interceptar Requisições ou Respostas e alterar o resultado o mesmo. O exemplo |
||||
mais comum é o middleware de Autenticação, que adiciona informações do usuário |
||||
logado na Requisição; mas essas alterações podem ser mais profundas, como |
||||
alterar os dados do formulário. Nesses casos, a entrada (ou a |
||||
saída, ou ambos) é afetada e, portanto, escrever testes que ignorem os |
||||
middlewares não vão representar a entrada (ou saída, ou ambos) do sistema |
||||
corretamente. E aí podemos perguntar se o teste é válido por usar estados que |
||||
não existem naturalmente no sistema. |
||||
|
||||
## Mocks |
||||
|
||||
Há um tempo, eu indicava que "mocks" deveriam ser usados para coisas externas |
||||
ao sistema. Entretanto, eu percebi que essa definição não é bem precisa -- |
||||
existem coisas externas que não devem ser mockadas -- e que uma definição |
||||
melhor para o que deve ser mockado é "qualquer coisa que esteja fora do seu |
||||
controle". |
||||
|
||||
Por exemplo, se você está escrevendo um sistema que faz geolocalização de IPs |
||||
através de um serviço externo, você provavelmente irá mockar a chamada para o |
||||
serviço, já que ele está fora do seu controle. Mas uma chamada para o banco de |
||||
dados, quando você já utiliza um sistema/framework de abstrai toda a parte de |
||||
banco de dados (por exemplo, Django), então o banco, apesar de ser uma entidade |
||||
externa, ainda está sob seu controle e, portanto, não deveria ser mockada -- |
||||
mas como o sistema/framework oferece uma abstração do banco, então usar |
||||
qualquer banco não deve afetar o resultado. |
||||
|
||||
Outro exemplo são microserviços. Mesmo microserviços dentro da mesma empresa |
||||
ou mesmo grupo são externos e fora do controle do projeto e, portanto, |
||||
mockados. "Mas são da mesma equipe!" Sim, mas não são do mesmo projeto, já que |
||||
a) a ideia de microserviços é justamente desacoplar esses serviços e/ou b) |
||||
estão em árvores de diretórios separados. Uma das vantagens de microserviços |
||||
da mesma equipe é que o contrato esperado por um é conhecido pela equipe e |
||||
isso pode ser mockado de forma fácil (a equipe sabe que, chamando um serviço |
||||
com dados X, haverá a resposta Y -- ou erro). |
||||
|
||||
# Conclusão |
||||
|
||||
De novo, a ideia não é reescrever todos os testes que você tem porque |
||||
"o jeito certo, que é o meu jeito". Entretanto, eu realmente vejo muitos |
||||
testes sendo escritos "a revelia", considerando a simples métrica de "um teste |
||||
por função/classe" e, em muitos casos, isso não faz sentido e que precisariam |
||||
ser repensados. Expondo esses "pensamentos impuros" sobre testes, minha ideia |
||||
era realmente fazer com que as pessoas repensassem a forma como os testes |
||||
estão sendo criados. |
||||
|
||||
--- |
||||
|
||||
[^1]: A não ser que você use anos com números romanos. |
||||
|
||||
[^2]: O porque vai ser uma string pode ser variado: por causa de um plugin de |
||||
segurança, porque é feito um armazenamento num banco que não trabalha bem |
||||
com inteiros, por causa de uma integração com sistema legado... |
||||
|
||||
[^3]: Uma classe de entidades de clientes deveria ser bem mais completa que |
||||
essa, mas vamos deixar ela simples assim para fins de exemplo. |
||||
|
||||
[^4]: E não, isso também não é SOLID de verdade, mas vamos deixar assim de |
||||
novo para fins de exemplo. |
||||
|
||||
[^5]: E alguém vai me passar o link do "Falácias que Desenvolvedores Acreditam |
||||
Sobre Nomes de Usuários" por causa disso. |
||||
|
||||
[^6]: E sim, deveria alterar só o `_validate_name`, mas assim fica mais claro |
||||
onde está o problema. |
Loading…
Reference in new issue