Browse Source

Working on the translation

master
Julio Biason 4 years ago
parent
commit
674cb6988c
  1. 156
      content/thoughts/reveries-about-testing.md
  2. 55
      content/thoughts/reveries-about-testing.pt.md

156
content/thoughts/reveries-about-testing.md

@ -37,3 +37,159 @@ What I want to discuss:
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
---
[^1]: Unless you want to use roman numerals, but anyway...

55
content/thoughts/reveries-about-testing.pt.md

@ -44,8 +44,7 @@ Agenda de coisas que eu vou comentar:
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 apenas duas
coisas:
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.
@ -60,15 +59,15 @@ 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) se referem ao
mesmo comportamento?
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. De novo, para fazer a transformação de uma
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?
@ -80,7 +79,7 @@ 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 como testar comportamento: No Subreddit do Django, foi criada
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
@ -90,20 +89,20 @@ 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 resultado é um número inteiro, eu ainda preciso escrever um
banco, que o valor do campo é um número inteiro. Eu ainda preciso escrever um
teste para isso?
A solução, no entanto, é 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 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[^1]; se o tipo foi alterado, o
_comportamento_ vai ser alterado também? Provavelmente não.
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.
De novo, "teste comportamentos, não 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,
@ -125,7 +124,7 @@ 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 views; testes de controllers sem conexão com os models ou
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
@ -152,16 +151,16 @@ 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 funcionando do sistema continuará sendo igual.
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.
produtividade dos desenvolvedores."
Sim, testes de integração são lentos, principalmente porque há um bom trabalho
em criar o estado esperado, todas as entradas conforme esperado pelo sistema
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.
@ -219,7 +218,7 @@ uma questão sobre qual dos dois testes deveria ser eliminado. Mas nós tínhamo
pelas duas validações.
Um exemplo mais prático. Imagine uma classe que guarde dados de clientes de
um serviço web de compras[^2]:
um serviço web de compras[^3]:
```python
class Client:
@ -232,7 +231,7 @@ 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[^3], o desenvolvedor altera o código para o seguinte:
Aí, pensando em SOLID[^4], o desenvolvedor altera o código para o seguinte:
```python
def _multiple_names(name):
@ -251,7 +250,7 @@ class Client:
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[^4].
nomes[^5].
Nova funcionalidade, precisamos de novos testes, certo?
@ -316,7 +315,7 @@ class Client:
self.name = name
```
Pronto, não tem mais validação[^5] e agora a Xuxa pode comprar. Mas ao rodar
Pronto, não tem mais validação[^6] e agora a Xuxa pode comprar. Mas ao rodar
os testes:
@ -424,18 +423,20 @@ estão sendo criados.
---
[^1]: O porque vai ser uma string pode ser variado: por causa de um plugin de
[^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...
[^2]: Uma classe de entidades de clientes deveria ser bem mais completa que
[^3]: Uma classe de entidades de clientes deveria ser bem mais completa que
essa, mas vamos deixar ela simples assim para fins de exemplo.
[^3]: E não, isso também não é SOLID de verdade, mas vamos deixar assim de
[^4]: E não, isso também não é SOLID de verdade, mas vamos deixar assim de
novo para fins de exemplo.
[^4]: E alguém vai me passar o link do "Falácias que Desenvolvedores Acreditam
[^5]: E alguém vai me passar o link do "Falácias que Desenvolvedores Acreditam
Sobre Nomes de Usuários" por causa disso.
[^5]: E sim, deveria alterar só o `_validate_name`, mas assim fica mais claro
[^6]: E sim, deveria alterar só o `_validate_name`, mas assim fica mais claro
onde está o problema.

Loading…
Cancel
Save