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 resolvefrom django.test import TestCasefrom lists.views import home_pagefrom lists.models import Item, List #classHomePageTest(TestCase):deftest_root_url_resolves_to_home_page_view(self): found =resolve('/') self.assertEquals(found.func, home_page)deftest_home_page_returns_correct_html(self): response = self.client.get('/') self.assertTemplateUsed(response, 'home.html')deftest_only_saves_items_when_necessary(self): self.client.get('/') self.assertEquals(Item.objects.count(), 0)classNewListTest(TestCase):deftest_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')deftest_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/')classListViewTest(TestCase):deftest_uses_list_template(self): response = self.client.get('/lists/the-only-list-in-the-world/') self.assertTemplateUsed(response, 'list.html')deftest_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')classListAndItemModelTest(TestCase): #deftest_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
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.
Para corrigir, precisamos implementar a classe List em lists/models.py, conforme abaixo:
from django.db import modelsclassList(models.Model):passclassItem(models.Model): text = models.TextField(default='')
Ao reexecutar os testes obtemos o seguinte erro:
(superlists) auri@av:~/superlists/superlists$ python manage.py test listsCreating 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 _executereturn self.cursor.execute(sql, params) File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/sqlite3/base.py", line 423,in executereturn Database.Cursor.execute(self, query, params)sqlite3.OperationalError: no such table: lists_listThe 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 mainexecute_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_argvsuper().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_databasesreturn_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 serializefor count, obj inenumerate(queryset, start=1): File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/base/creation.py", line 133, in get_objectsyieldfrom queryset.iterator() File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/models/query.py", line 353, in _iteratoryieldfrom 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 executereturn 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_wrappersreturnexecutor(sql, params, many, context) File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _executereturn 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 _executereturn self.cursor.execute(sql, params) File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/backends/sqlite3/base.py", line 423, in executereturn 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.
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 https://docs.djangoproject.com/en/3.2/ref/models/fields/#django.db.models.ForeignKey.
from django.db import modelsclassList(models.Model):passclassItem(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 modelsclassList(models.Model):passclassItem(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 listsCreating 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 _executereturn 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 _executereturn 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 _executereturn self.cursor.execute(sql, params)...django.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id----------------------------------------------------------------------Ran 8 tests in0.034sFAILED (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 resolvefrom django.test import TestCasefrom lists.views import home_pagefrom lists.models import Item, List #classHomePageTest(TestCase):deftest_root_url_resolves_to_home_page_view(self): found =resolve('/') self.assertEquals(found.func, home_page)deftest_home_page_returns_correct_html(self): response = self.client.get('/') self.assertTemplateUsed(response, 'home.html')deftest_only_saves_items_when_necessary(self): self.client.get('/') self.assertEquals(Item.objects.count(), 0)classNewListTest(TestCase):deftest_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')deftest_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/')classListViewTest(TestCase):deftest_uses_list_template(self): response = self.client.get('/lists/the-only-list-in-the-world/') self.assertTemplateUsed(response, 'list.html')deftest_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')classListAndItemModelTest(TestCase): #deftest_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:
Com isso, ao executar os testes de unidade obtemos sucesso na execução.
(superlists) auri@av:~/superlists/superlists$ python manage.py test listsCreating test database for alias 'default'...System check identified no issues (0 silenced).........----------------------------------------------------------------------Ran 8 tests in0.019sOKDestroying test database for alias 'default'...
Novamente, mesmo tendo um resultado de sucesso nesse momento, Percival (2017) 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." (Pervcival, 2017)
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_testsCreating 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 in19.385sFAILED (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,63insertions(+),9deletions(-) create mode 100644 lists/migrations/0003_list.py create mode 100644 lists/migrations/0004_item_list.py(superlists) auri@av:~/superlists/superlists$ git pushUsername for'https://github.com': aurimrvPassword for'https://aurimrv@github.com':Enumerating objects:15, done.Counting objects:100% (15/15), done.Delta compression using up to 12 threadsCompressing 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 with4 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.