Uma visão geral do Desenvolvimento Orientado por Testes.
O desenvolvimento orientado por testes (TDD) refere-se a um estilo de programação em que se escrevem testes para orientar a implementação do design do programa no código.
Um ou mais testes (em particular, testes unitários) são escritos antes de se programar. Os testes destinam-se a cobrir um aspeto do comportamento do programa, que pode estar centrado numa única função ou método. Escrever os testes é uma forma de transformar os requisitos do programa e a arquitetura geral num design específico da implementação. Os testes são executados e devem falhar, porque o código ainda não foi implementado. O código é implementado e os testes são executados novamente. Se os testes passarem, ou a implementação desse comportamento está concluída ou talvez ainda seja preciso criar mais testes. Se os testes não passarem, o código é depurado e os testes são executados outra vez. O ciclo de testar e programar repete-se até que todos os testes necessários passem; nessa altura, a implementação desse aspeto do comportamento do programa está concluída... por agora.
A refatoração é a reescrita de código para melhorar o seu design. Não é simplesmente reescrever código para corrigir erros. Por vezes, chama-se "refatoração" ao ato de modificar o código para o fazer passar nos testes. Embora modificar o código possa incluir melhorar o design como forma de passar nos testes, limitar-se a depurar não é necessariamente melhorar o design do código e, por isso, não é necessariamente refatoração.
O seguinte é um exemplo de depuração sem refatoração:
# A function intended to return x added to y.
# x and y are bad parameter names, but we ignore that for now.
def add(x, y):
# used multiply operator by mistake. It fails the tests.
return x * y
# Function corrected. It passes the tests. It has been debugged, but not refactored.
def add(x, y):
return x + y
O seguinte é um exemplo de refatoração e, depois, de depuração:
# Function name and parameter names are modified to something more meaningful. This is refactoring.
def lot_inventory(old_cars, new_cars):
# Introduced multiply operator by mistake. It fails the tests. This is why we test.
return old_cars * new_cars
# Function corrected. It passes the tests. This is debugging.
def lot_inventory(old_cars, new_cars):
return old_cars + new_cars
A track de Python do Exercism utiliza a metodologia TDD nos seus exercícios. Os testes unitários já estão escritos. O estudante pode consultar os testes para obter uma compreensão mais detalhada do que é necessário para que uma solução passe. Pode ser disponibilizado ao estudante um esqueleto de solução.
Quando um ou mais testes falham numa solução de Python, a tarefa ou tarefas correspondentes não terão um fundo verde. A primeira área de tarefa que falhou será expandida e o seu cabeçalho será parecido com
Task 1 Extract coordinates -
Clicar no sinal de menos recolhe a tarefa, para podermos ver as outras tarefas, mas por agora ficamos com esta.
Por baixo, haverá uma área Test expandida, parecida com
Test 1 ⌄
FAILED TisburyTreasure > get coordinate
em que Tisbury Treasure indica o exercício e get_coordinate indica a função ou o método que falhou.
Normalmente, Test 1 é uma espécie de modelo com uma secção de código para preparar os testes.
Não tem informação sobre o teste ou testes específicos que falharam.
Perto do fim, dirá que
One or more variations of this test failed. Details can be found under each [variant#].
Clicar no ⌄ recolhe o teste.
Por baixo, haverá um teste recolhido parecido com:
Test 2 >
FAILED TisburyTreasure > get coordinate [variation #1] (item=
("Scrimshaw Whale's Tooth", '2A'), result='2A')
O aspeto varia conforme a largura definida para o painel da direita.
Clicar no > expande o teste.
Os dados de entrada e os dados do resultado esperado serão provavelmente apresentados numa secção de código.
Os dados podem ser relativos a todos os testes desta tarefa.
No fundo, na secção Test Failure, está o motivo específico pelo qual este teste falhou.
Pode ser parecido com isto:
AssertionError: ['2A'] != '2A'
Neste caso concreto, indica que o valor devolvido, ['2A'], não era igual ao valor esperado, '2A'.
Se olharmos para o código de get_coordinate, vemos que está implementado assim
def get_coordinate(record):
return [record[1]]
Se retirarmos os parênteses retos da lista (por exemplo, return record[1]) e voltarmos a executar os testes, os testes da Tarefa 1 passam.
Se uma ou mais tarefas continuarem a falhar, repete-se o processo acima com cada uma delas até todos os testes passarem.
Por vezes, os dados esperados e os dados devolvidos são demasiado extensos para caberem todos na secção Test Failure.
Pode ser parecido com isto:
AssertionError: '("Sc[67 chars]\')\n\n(\'Brass Spyglass\', \'Abandoned Lighth[952 chars]')\n' != '("Sc[67 chars]\')\n(\'Brass Spyglass\', \'Abandoned Lighthou[928 chars]')\n'
Diff is 970 characters long. Set self.maxDiff to None to see it.
Pode ainda haver dados suficientes para perceber qual é o problema.
No caso acima, são devolvidas duas quebras de linha (por exemplo, \n\n(\'Brass Spyglass) quando só se espera uma (por exemplo, \n(\'Brass Spyglass).
Parabéns! Todos os testes passaram. E agora? Podes publicar a solução já. Ou, agora que o código funciona, se quiseres refatorá-lo por algum motivo, podes modificar o código e submeter outra iteração. Se achas que o código podia estar melhor, mas não sabes como, podes pedir mentoria para a solução. Se houver um mentor disponível, ele pode contactar-te com ideias para outras abordagens à solução. Ao publicares a solução, podes permitir comentários, e outros estudantes podem aproveitar para publicar comentários ou fazer perguntas.
Embora "a otimização prematura seja a raiz de todo o mal" (um ditado atribuído tanto a Tony Hoare como a Donald Knuth), chega um momento em que, mesmo funcionando a solução, se quer melhorar o seu desempenho.
Um desses momentos pode ser quando a solução passa alguns testes mas excede o tempo limite noutros.
Pode ser útil saber exatamente quanto tempo um trecho de código demora.
O módulo timeit pode ser usado para medir o tempo de execução do código até durações muito pequenas.
A função timeit pode receber até cinco argumentos: timeit.timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000, globals=None).
O parâmetro stmt define o código que é efetivamente executado e cronometrado.
O parâmetro number determina quantas vezes o código de stmt é executado.
O parâmetro setup define o código que é executado apenas uma vez para preparar a execução do código de stmt.
O tempo de execução do código de setup está incluído no tempo total.
Quanto mais iterações o código de stmt executar, menos o tempo de setup contará por iteração.
O parâmetro timer permite passar um Timer diferente do predefinido.
O argumento predefinido do parâmetro timer é perf_counter, que deve ser suficiente para a maioria dos casos.
O argumento predefinido do parâmetro number é 1_000_000.
O parâmetro globals especifica um espaço de nomes no qual o código é executado.
O argumento predefinido do parâmetro globals é None.
O seguinte é um exemplo de utilização do timeit para ver quanto tempo demora a determinar se uma frase contém todas as vogais inglesas:
import timeit
# run one million times
loops = 1_000_000
# first positional argument is for stmt
# second positional argument is for setup
# third (named) argument is for number
print(timeit.timeit("""has_all_vowels('Another piggy digs up the truffles.')""",
"""
VOWELS = "AEIOU"
def has_all_vowels(sentence):
return all(letter in sentence.casefold() for letter in VOWELS)
""", number=loops) / loops)
Executar o código um milhão de vezes demorou, em média, 4.965089999896008e-07 segundos por chamada (cerca de 497 nanossegundos por chamada).
O exemplo seguinte serve para ver se retirar a chamada a casefold da compreensão de listas poupa algum tempo:
import timeit
loops = 1_000_000
print(timeit.timeit("""has_all_vowels('Another piggy digs up the truffles.')""",
"""
VOWELS = "AEIOU"
def has_all_vowels(sentence):
sentence = sentence.casefold()
return all(letter in sentence for letter in VOWELS)
""", number=loops) / loops)
Executar o código um milhão de vezes demorou, em média, 4.923898000270128e-07 segundos por chamada (cerca de 492 nanossegundos por chamada.)
Assim, retirar o casefold da compreensão de listas poupou cerca de 5 nanossegundos por chamada, ou cerca de 5 milissegundos no total para um milhão de chamadas.
O cProfile também pode ser usado para analisar o código; no entanto, não é tão granular, uma vez que só desce até durações de milissegundos.