Programação e Desenvolvimento Dirigidor por Testes
  • Programação e Desenvolvimento Dirigidor por Testes em Python
  • Autores e Agradecimentos
  • Uso do Livro
  • Contribua com o Livro
  • Licença
  • Organização do Livro
  • 1 Introdução
    • 1.1 Considerações Iniciais
    • 1.2 Configuração Inicial do Ambiente
    • 1.3 TDD Básico
      • 1.3.1 Exemplo Simples
    • 1.4 Considerações Finais
  • 2 TESTE DE SOFTWARE
    • 2.1 Considerações Iniciais
    • 2.2 Terminologia e Conceitos Básicos
    • 2.3 Fases de Teste
    • 2.4 Técnicas e Critérios de Teste
    • 2.5 Considerações Finais
  • 3 Desenvolvimento Dirigido por Teste
    • 3.1 Configuração do Ambiente
    • 3.2 Verificando o Ambiente com TDD
    • 3.3 Controle de Versão do Projeto
    • 3.4 Teste Funcional com UnitTest
    • 3.5 Teste de Unidade e a Evolução do Sistema
      • 3.5.1 Teste de Unidade de uma View
    • 3.6 Evoluindo o Teste Funcional
    • 3.7 Revisando o Processo do TDD
  • 4 TDD E BANCO DE DADOS
    • 4.1 Envio e Processamento de Requisição POST
    • 4.2 Banco de Dados no Django
  • 5 MELHORANDO E ORGANIZANDO OS CONJUNTOS DE TESTE
    • 5.1 Isolamento dos Testes Funcionais
    • 5.2 Esperas Implícitas, Explícitas e Time Sleeps
  • 6 ATACANDO O MAIOR DESAFIO DE FORMA INCREMANTAL
    • 6.1 Separando URL no Estilo REST
    • 6.2 Iterando para um Novo Design da Aplicação
    • 6.3 Refatorar Casos de Teste
    • 6.4 Separando Templates
    • 6.5 URL para Nova Lista
    • 6.6 Alterando Modelos
    • 6.7 URL Próprio para Cada Lista
Powered by GitBook
On this page

Was this helpful?

  1. 6 ATACANDO O MAIOR DESAFIO DE FORMA INCREMANTAL

6.6 Alterando Modelos

Vamos caminhar agora para a solução do nosso problema mais complicado que é o de lidar com diferentes URLs para usuários distintos.

Iniciamos alterando nossos testes unitários para lidar com o que precisamos. O novo conjunto de teste unitário ficou conforme abaixo. Incluí um # ao final de cada linha que foi incluída.

from django.urls import resolve
from django.test import TestCase
from lists.views import home_page
from lists.models import Item, List #

class HomePageTest(TestCase):

	def test_root_url_resolves_to_home_page_view(self):
		found = resolve('/')
		self.assertEquals(found.func, home_page)

	def test_home_page_returns_correct_html(self):
		response = self.client.get('/')
		self.assertTemplateUsed(response, 'home.html')

	def test_only_saves_items_when_necessary(self):
		self.client.get('/')
		self.assertEquals(Item.objects.count(), 0)


class NewListTest(TestCase):

	def test_can_save_a_POST_request(self):
		self.client.post('/lists/new', data={'item_text': 'A new list item'})
		self.assertEquals(Item.objects.count(), 1)
		new_item = Item.objects.first()
		self.assertEquals(new_item.text, 'A new list item')

	def test_redirects_after_POST(self):
		response = self.client.post('/lists/new', data={'item_text': 'A new list item'})
		self.assertRedirects(response, '/lists/the-only-list-in-the-world/')


class ListViewTest(TestCase):

	def test_uses_list_template(self):
		response = self.client.get('/lists/the-only-list-in-the-world/')
		self.assertTemplateUsed(response, 'list.html')


	def test_displays_all_list_itens(self):
		Item.objects.create(text='itemey 1')
		Item.objects.create(text='itemey 2')

		response = self.client.get('/lists/the-only-list-in-the-world/')

		self.assertContains(response, 'itemey 1')
		self.assertContains(response, 'itemey 2')


class ListAndItemModelTest(TestCase): #

	def test_saving_and_retriving_items(self):
		list_ = List() #
		list_.save() #

		first_item = Item()
		first_item.text = 'The first (ever) list item'
		first_item.list = list_ #
		first_item.save()

		second_item = Item()
		second_item.text = 'Item the second'
		second_item.list = list_
		second_item.save()

		saved_list = List.objects.first() #
		self.assertEquals(saved_list, list_) #

		saved_items = Item.objects.all()
		self.assertEquals(saved_items.count(),2)

		first_saved_item = saved_items[0]
		second_saved_item = saved_items[1]

		self.assertEquals(first_saved_item.text, 'The first (ever) list item')
		self.assertEquals(first_saved_item.list, list_)	#
		self.assertEquals(second_saved_item.text, 'Item the second')
		self.assertEquals(second_saved_item.list, list_) #

Outra forma de ver as alterações que Percival utilizou em seu livro, foi por meio do git diff, conforme ilustrado abaixo. Linhas com + foram adicionadas

diff --git a/lists/tests.py b/lists/tests.py
index 5a2f58d..a35f831 100644
--- a/lists/tests.py
+++ b/lists/tests.py
@@ -1,7 +1,7 @@
 from django.urls import resolve
 from django.test import TestCase
 from lists.views import home_page
-from lists.models import Item
+from lists.models import Item, List #
 
 class HomePageTest(TestCase):
 
@@ -48,17 +48,25 @@ class ListViewTest(TestCase):
 		self.assertContains(response, 'itemey 2')
 
 
-class ItemModelTest(TestCase):
+class ListAndItemModelTest(TestCase): #
 
 	def test_saving_and_retriving_items(self):
+		list_ = List() #
+		list_.save() #
+
 		first_item = Item()
 		first_item.text = 'The first (ever) list item'
+		first_item.list = list_ #
 		first_item.save()
 
 		second_item = Item()
 		second_item.text = 'Item the second'
+		second_item.list = list_
 		second_item.save()
 
+		saved_list = List.objects.first() #
+		self.assertEquals(saved_list, list_) #
+
 		saved_items = Item.objects.all()
 		self.assertEquals(saved_items.count(),2)
		saved_items = Item.objects.all()
		self.assertEquals(saved_items.count(),2)

		first_saved_item = saved_items[0]
		second_saved_item = saved_items[1]
 
 		self.assertEquals(first_saved_item.text, 'The first (ever) list item')
+		self.assertEquals(first_saved_item.list, list_)	#
		self.assertEquals(second_saved_item.text, 'Item the second')
+		self.assertEquals(second_saved_item.list, list_) #

Basicamente as alterações incluem a criação de um objeto List; a atribuição de cada item a esse objeto por meio da propriedade .list. Em seguida, fizemos o teste se a lista foi salva e se os dois itens salvaram seu relacionamento com a lista.

Elaborado o teste, podemos executá-lo para iniciar o ciclo teste de unidade/código.

(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
System check identified no issues (0 silenced).
E
======================================================================
ERROR: lists.tests (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: lists.tests
Traceback (most recent call last):
  File "/usr/lib/python3.8/unittest/loader.py", line 436, in _find_test_path
    module = self._get_module_from_name(name)...
    from lists.models import Item, List #
ImportError: cannot import name 'List' from 'lists.models' 
(/home/mlptdd/superlists/superlists/lists/models.py)

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)

Para corrigir, precisamos implementar a classe List em lists/models.py, conforme abaixo:

from django.db import models

class List(models.Model):
	pass

class Item(models.Model):
	text = models.TextField(default='')

Ao reexecutar os testes obtemos o seguinte erro:

(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
Traceback (most recent call last):
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/sqlite3/base.py", line 423, in execute
    return Database.Cursor.execute(self, query, params)
sqlite3.OperationalError: no such table: lists_list

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "manage.py", line 22, in <module>
    main()
  File "manage.py", line 18, in main
    execute_from_command_line(sys.argv)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line
    utility.execute()
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/core/management/__init__.py", line 413, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/core/management/commands/test.py", line 23, in run_from_argv
    super().run_from_argv(argv)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/core/management/base.py", line 354, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/core/management/base.py", line 398, in execute
    output = self.handle(*args, **options)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/core/management/commands/test.py", line 55, in handle
    failures = test_runner.run_tests(test_labels)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/test/runner.py", line 725, in run_tests
    old_config = self.setup_databases(aliases=databases)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/test/runner.py", line 643, in setup_databases
    return _setup_databases(
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/test/utils.py", line 179, in setup_databases
    connection.creation.create_test_db(
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/base/creation.py", line 90, in create_test_db
    self.connection._test_serialized_contents = self.serialize_db_to_string()
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/base/creation.py", line 136, in serialize_db_to_string
    serializers.serialize("json", get_objects(), indent=None, stream=out)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/core/serializers/__init__.py", line 129, in serialize
    s.serialize(queryset, **options)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/core/serializers/base.py", line 90, in serialize
    for count, obj in enumerate(queryset, start=1):
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/base/creation.py", line 133, in get_objects
    yield from queryset.iterator()
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/models/query.py", line 353, in _iterator
    yield from self._iterable_class(self, chunked_fetch=use_chunked_fetch, chunk_size=chunk_size)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/models/query.py", line 51, in __iter__
    results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/models/sql/compiler.py", line 1169, in execute_sql
    cursor.execute(sql, params)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/utils.py", line 66, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/utils.py", line 75, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/utils.py", line 90, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/sqlite3/base.py", line 423, in execute
    return Database.Cursor.execute(self, query, params)
django.db.utils.OperationalError: no such table: lists_list

Pela mensagem é um erro relacionado com a base de dados. Lembre-se que quando mudamos o modelo é necessário que façamos o migration para que as informações das tabelas do banco de dados sejam atualizadas a contento.

(superlists) auri@av:~/superlists/superlists$ python manage.py makemigrations
Migrations for 'lists':
  lists/migrations/0003_list.py
    - Create model List

Feito isso o resultados dos testes passam a ser conforme abaixo:

(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...E....
======================================================================
ERROR: test_saving_and_retriving_items (lists.tests.ListAndItemModelTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/mlptdd/superlists/superlists/lists/tests.py", line 77, in test_saving_and_retriving_items
    self.assertEquals(first_saved_item.list, list_)	#
AttributeError: 'Item' object has no attribute 'list'

----------------------------------------------------------------------
Ran 8 tests in 0.017s

FAILED (errors=1)
Destroying test database for alias 'default'...

O erro indica que não temos ainda o atributo .list na classe Item. Podemos incluí-lo da mesma forma que fizemos com o atributo .text.

from django.db import models

class List(models.Model):
	pass

class Item(models.Model):
	text = models.TextField(default='')
	list = models.TextField(default='')

Como alteramos novamente o modelo, nossos testes, se forem executados, iram nos lembrar que precisamos fazer uma migração.

(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
Traceback (most recent call last):
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/sqlite3/base.py", line 423, in execute
    return Database.Cursor.execute(self, query, params)
sqlite3.OperationalError: no such column: lists_item.list

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "manage.py", line 22, in <module>
    main()
  File "manage.py", line 18, in main
    execute_from_command_line(sys.argv)
...
django.db.utils.OperationalError: no such column: lists_item.list
(superlists) auri@av:~/superlists/superlists$ python manage.py makemigrations
Migrations for 'lists':
  lists/migrations/0004_item_list.py
    - Add field list to item

Executada a migração agora nossos testes apresentam o erro abaixo:

(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...F....
======================================================================
FAIL: test_saving_and_retriving_items (lists.tests.ListAndItemModelTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/mlptdd/superlists/superlists/lists/tests.py", line 77, in test_saving_and_retriving_items
    self.assertEquals(first_saved_item.list, list_)	#
AssertionError: 'List object (1)' != <List: List object (1)>

----------------------------------------------------------------------
Ran 8 tests in 0.017s

FAILED (failures=1)
Destroying test database for alias 'default'...
from django.db import models

class List(models.Model):
	pass

class Item(models.Model):
	text = models.TextField(default='')
	list = models.ForeignKey(List,on_delete=models.SET_DEFAULT,default=None)

Alterado o modelo devemos fazer a migração novamente. Entretanto, como a anterior não foi bem sucedida podemos removê-la antes. Cuidado, entretanto, ao remover migrações. Só o faça se as mesmas ainda não foram usadas.

from django.db import models

class List(models.Model):
	pass

class Item(models.Model):
	text = models.TextField(default='')
	list = models.ForeignKey(List,on_delete=models.SET_DEFAULT,default=None)

Adaptando o Restante do Código ao Novo Modelo

Feita a correção e a migração acima, ao executar os testes novamente temos as seguintes mensagens de erro:

(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....E.EE
======================================================================
ERROR: test_displays_all_list_itens (lists.tests.ListViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
...
django.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id

======================================================================
ERROR: test_can_save_a_POST_request (lists.tests.NewListTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
...
django.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id

======================================================================
ERROR: test_redirects_after_POST (lists.tests.NewListTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
...
django.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id

----------------------------------------------------------------------
Ran 8 tests in 0.034s

FAILED (errors=3)
Destroying test database for alias 'default'...

Ao todo, temos três erros de violação de restrição do tipo NOT_NULL. Como incluímos uma dependência entre Items e Lists, agora cada item tem que ter uma lista-pai associada a ele e, por isso, três de nossos testes unitários falharam.

O primeiro passo então é corrigir nossos testes unitários conforme abaixo. Veja as linhas 42 a 44 na qual é feita a associação entre o item e a lista.

from django.urls import resolve
from django.test import TestCase
from lists.views import home_page
from lists.models import Item, List #

class HomePageTest(TestCase):

	def test_root_url_resolves_to_home_page_view(self):
		found = resolve('/')
		self.assertEquals(found.func, home_page)

	def test_home_page_returns_correct_html(self):
		response = self.client.get('/')
		self.assertTemplateUsed(response, 'home.html')

	def test_only_saves_items_when_necessary(self):
		self.client.get('/')
		self.assertEquals(Item.objects.count(), 0)


class NewListTest(TestCase):

	def test_can_save_a_POST_request(self):
		self.client.post('/lists/new', data={'item_text': 'A new list item'})
		self.assertEquals(Item.objects.count(), 1)
		new_item = Item.objects.first()
		self.assertEquals(new_item.text, 'A new list item')

	def test_redirects_after_POST(self):
		response = self.client.post('/lists/new', data={'item_text': 'A new list item'})
		self.assertRedirects(response, '/lists/the-only-list-in-the-world/')


class ListViewTest(TestCase):

	def test_uses_list_template(self):
		response = self.client.get('/lists/the-only-list-in-the-world/')
		self.assertTemplateUsed(response, 'list.html')


	def test_displays_all_list_itens(self):
		list_ = List.objects.create()
		Item.objects.create(text='itemey 1', list=list_)
		Item.objects.create(text='itemey 2', list=list_)

		response = self.client.get('/lists/the-only-list-in-the-world/')

		self.assertContains(response, 'itemey 1')
		self.assertContains(response, 'itemey 2')


class ListAndItemModelTest(TestCase): #

	def test_saving_and_retriving_items(self):
		list_ = List() #
		list_.save() #

		first_item = Item()
		first_item.text = 'The first (ever) list item'
		first_item.list = list_ #
		first_item.save()

		second_item = Item()
		second_item.text = 'Item the second'
		second_item.list = list_
		second_item.save()

		saved_list = List.objects.first() #
		self.assertEquals(saved_list, list_) #

		saved_items = Item.objects.all()
		self.assertEquals(saved_items.count(),2)

		first_saved_item = saved_items[0]
		second_saved_item = saved_items[1]

		self.assertEquals(first_saved_item.text, 'The first (ever) list item')
		self.assertEquals(first_saved_item.list, list_)	#
		self.assertEquals(second_saved_item.text, 'Item the second')
		self.assertEquals(second_saved_item.list, list_) #
		

Com isso, nossos testes avanças e ficam restando dois com falha, ambos relacionados ao POST para a inclusão de um novo item e nossa função de view que faz o tratamento a requisição também não faz a associação do item com um lista. A correção é dada nas linhas 13 e 14 abaixo:

from django.shortcuts import redirect, render
from lists.models import Item, List

# Create your views here.
def home_page(request):
	return render(request, 'home.html')

def view_list(request):
	items = Item.objects.all()
	return render(request, 'list.html', {'items': items})

def new_list(request):
	list_ = List.objects.create()
	Item.objects.create(text=request.POST['item_text'], list=list_)
	return redirect('/lists/the-only-list-in-the-world/')

Com isso, ao executar os testes de unidade obtemos sucesso na execução.

(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
........
----------------------------------------------------------------------
Ran 8 tests in 0.019s

OK
Destroying test database for alias 'default'...

Desse modo, para nos assegurar que está tudo bem, ao executar nossos testes funcionais podemos observar que chagamos a um estado já conhecido e consistente.

(superlists) auri@av:~/superlists/superlists$ python manage.py test functional_tests
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.F
======================================================================
FAIL: test_multiple_users_can_start_lists_at_different_urls (tests.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/mlptdd/superlists/superlists/functional_tests/tests.py", line 117, in test_multiple_users_can_start_lists_at_different_urls
    self.wait_for_row_in_list_table('1: Buy milk')
...
AssertionError: '1: Buy milk' not found in ['1: Buy peacock feathers', 
'2: Buy milk']

----------------------------------------------------------------------
Ran 2 tests in 19.385s

FAILED (failures=1)
Destroying test database for alias 'default'...

Após as mudanças e a chegada a um ponto consistente com o que já tínhamos é um bom momento para colocarmos as mudanças sob controle de versão.

(superlists) auri@av:~/superlists/superlists$ git add lists

(superlists) auri@av:~/superlists/superlists$ git commit -am "Test case updated to check connection between items and lists. Models updated to include List."
[master 20f8359] Test case updated to check connection between items and lists. Models updated to include List.
 5 files changed, 63 insertions(+), 9 deletions(-)
 create mode 100644 lists/migrations/0003_list.py
 create mode 100644 lists/migrations/0004_item_list.py

(superlists) auri@av:~/superlists/superlists$ git push
Username for 'https://github.com': aurimrv
Password for 'https://aurimrv@github.com': 
Enumerating objects: 15, done.
Counting objects: 100% (15/15), done.
Delta compression using up to 12 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 1.58 KiB | 1.58 MiB/s, done.
Total 9 (delta 4), reused 0 (delta 0)
remote: Resolving deltas: 100% (4/4), completed with 4 local objects.
To https://github.com/aurimrv/superlists.git
   89328a3..20f8359  master -> master

Ao verificar nossa lista de itens a cumprir, podemos riscar mais um.

Previous6.5 URL para Nova ListaNext6.7 URL Próprio para Cada Lista

Last updated 4 years ago

Was this helpful?

Aparentemente, o tipo de dado do atributo .list. Como queremos estabelecer um relacionamento entre duas classes, o Django oferece um tipo de dado denominado ForeignKey e podemo mudar o tipo do atributo .list conforme mostrado abaixo. Mais informações sobre esse tipo de dado pode ser encontrada em .

Novamente, mesmo tendo um resultado de sucesso nesse momento, nos lembra de que o desenvolvimento, da forma como estamos fazendo, parece ser contraintuitiva. Criar uma lista a cada item sendo incluído parece estranho. Entretanto, ele nos alerta que mesmo dando vontade fazer tudo de uma única vez e já chegar ao código funcional que consideramos correto, é necessário seguir os passos do TDD. Ele nos lembra que devemos seguir o "Testing Goat!" ou nosso Bode dos Testes.

"Quando você estiver no alto de uma montanha, vai querer pensar cuidadosamente no lugar em que colocará cada pé, e dará um passo de cada vez, verificando, em cada etapa, se o local em que pisou não fará você cair de um penhasco." ()

https://docs.djangoproject.com/en/3.2/ref/models/fields/#django.db.models.ForeignKey
Percival (2017)
Pervcival, 2017