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'...

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 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'...

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_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.

Last updated