今日からDjangoで、Shoppingカートを作りましょう。
事前準備
まずは、「shopping-cart」フォルダをDesktop上に保存してください。
次に、Djangoアプリケーション開発 ~事前準備編~で、開発環境を準備してください。
以下のような構成になっていれば、OKです。
1 2 3 4 5 6 7 8 |
tree shopping-cart(事前準備では、django-lessonになっているが、名前を変更) L Dockerfile L Makefile L app L docker-compose.yml L requirements.txt |
環境が準備できたら、make djangoでプロジェクトを作成しましょう。
1 2 |
docker-compose build make django |
DB設定変更
次に、DjangoのDBをPostgreSQLに変更しましょう。デフォルトは、SQLite3なのですよ。
app/app/settings.pyを開いて、「DATABASES」の設定を変更してください。
1 2 3 4 5 6 7 8 9 10 11 12 |
app/app/settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'HOST': os.environ.get('DB_HOST'), 'NAME': os.environ.get('DB_NAME'), 'USER': os.environ.get('DB_USER'), 'PASSWORD': os.environ.get('DB_PASS'), } } |
Shopアプリケーションの作成
次にShopアプリケーションを作成しましょう。
1 |
make app app=shop |
app/app/settings.pyの「INSTALLED_APPS」に、shopを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
app/app/settings.py INSTALLED_APPS = [ # myapp 'shop', # new 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] |
この設定で、Djangoがshopを認識するようになりました。
テストコードの作成
今回は、テスト駆動開発にて、アプリケーション開発を行います。
Wikiによるとテスト駆動開発とは、次のことを指します。
テスト駆動開発 (てすとくどうかいはつ、test-driven development; TDD) とは、プログラム開発手法の一種で、プログラムに必要な各機能について、最初にテストを書き(これをテストファーストと言う)、そのテストが動作する必要最低限な実装をとりあえず行った後、コードを洗練させる、という短い工程を繰り返すスタイルである。
テスト駆動開発を詳しく知りたい方は、こちらをどうぞ。
テスト駆動開発では、テストコードを最初に書きます。
テストを書くためには「何を作るか」について、決める必要があります。
作成するアプリケーションが「Shopping Cart」なので、商品一覧が欲しいところですね。
ひとまず、「localhost:8000にアクセスしたとき、商品一覧が表示される」機能を作ってみましょう。
テストコードを書く前に、いくつかファイルを用意します。
1 2 3 4 5 6 7 8 |
# pytestの設定ファイル touch app/pytest.ini # もともとのファイルを削除する rm app/shop/tests.py mkdir app/shop/tests touch app/shop/tests/test_views.py |
私は、テスト対象ファイルごとにテストコードを分割するのが好きなので、元々のファイル(tests.py)を削除し、testsフォルダを作成後、個別のテストファイルを作成しています。
pytest.iniファイルは、テストフレームワークであるpytestの設定ファイルです。
requirements.txtの「pytest-django==3.5.1」でモジュールをインストールしてあります。
pytest.iniファイルに以下の設定をしておきましょう。
1 2 3 4 5 6 7 |
[pytest] DJANGO_SETTINGS_MODULE = app.settings python_classes = *Test python_functions = test_* python_files = tests.py test_*.py *_tests.py norecursedirs = static templates env |
一応、補足しておきます。
・DJANGO_SETTINGS_MODULE -> プロジェクトのsettings.pyを指定
・python_classes -> XXXXTestのクラス名をテスト対象として認識する
・python_functions -> test_XXXXの関数名をテスト対象として認識する
・python_files -> テスト実行ファイルの命名規則
・norecursedirs -> pytestがテスト対象を検索するとき読み込みたくないディレクトリを指定
詳しく知りたい方は、こちらをどうぞ。
準備が整ったので、テストコードを書いてみましょう。
最初は、「localhost:8000」にアクセスしたら、HTTPステータスが「200」になるかテストしてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
app/shop/tests/test_views.py from django.test import TestCase class ViewTest(TestCase): def test_first_page(self): # localhost:8000/を取得 response = self.client.get('/') # ステータスコードをチェック assert response.status_code == 200 |
テストを実行してみます。テストを実行するには、make testをterminal上で実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
make test =================================== FAILURES =================================== ___________________________ ViewTest.test_first_page ___________________________ self = <test_views.ViewTest testMethod=test_first_page> def test_first_page(self): response = self.client.get('/') print(dir(response)) > assert response.status_code == 200 E AssertionError: assert 404 == 200 E -404 E +200 response = <HttpResponseNotFound status_code=404, "text/html"> self = <test_views.ViewTest testMethod=test_first_page> shop/tests/test_views.py:11: AssertionError ------------------------------ Captured log call ------------------------------- WARNING django.request:log.py:228 Not Found: / |
「Not Found: /」となりました。
URLの設定がされていないので、エラーが出ているようですね。
URLの設定をしましょう。
1 2 3 4 5 6 7 8 9 10 |
app/app/urls.py from django.contrib import admin from django.urls import include, path # new urlpatterns = [ path('admin/', admin.site.urls), path('', include('shop.urls')), # new ] |
shopアプリケーション配下にurls.pyファイルを作成しましょう。
1 |
touch app/shop/urls.py |
1 2 3 4 5 6 7 8 9 10 11 |
app/shop/urls.py from django.urls import path from . import views app_name = 'shop' urlpatterns = [ path('', views.all_products, name='all_product'), ] |
ここまで設定できたらテストをしてみましょう。
1 2 3 4 |
make test E AttributeError: module 'shop.views' has no attribute 'all_products' |
views.pyに「all_products」が設定されていないため、エラーが発生しました。
app/shop/blogs.pyにall_products関数を追加してみましょう。
1 2 3 4 5 6 7 8 |
app/shop/views.py from django.shortcuts import render from django.http import HttpResponse def all_products(request): return HttpResponse('This is a Top page') |
もう一度、テストを実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
make test PASSEDDestroying test database for alias 'default'... ============================== 1 passed in 1.08s =============================== ./app/settings.py:94:80: E501 line too long (91 > 79 characters) ./app/settings.py:97:80: E501 line too long (81 > 79 characters) ./app/settings.py:100:80: E501 line too long (82 > 79 characters) ./app/settings.py:103:80: E501 line too long (83 > 79 characters) ./shop/admin.py:1:1: F401 'django.contrib.admin' imported but unused ./shop/models.py:1:1: F401 'django.db.models' imported but unused ./shop/urls.py:9:1: W391 blank line at end of file ./shop/views.py:1:1: F401 'django.shortcuts.render' imported but unused ./shop/views.py:4:1: E302 expected 2 blank lines, found 1 ./shop/views.py:5:46: W291 trailing whitespace ./shop/views.py:6:1: W391 blank line at end of file ./shop/tests/views_test.py:1:1: W391 blank line at end of file ./shop/tests/test_views.py:1:1: F401 'pytest' imported but unused ./shop/tests/test_views.py:7:1: W293 blank line contains whitespace ./shop/tests/test_views.py:12:1: W391 blank line at end of file ./shop/tests/test_views.py:12:1: W293 blank line contains whitespace |
今度は、テストがPASSしましたね。
しかし、テストがPASSした後、何やらエラーがでています。
これは、「flake8」でチェックしたPythonの文法チェックです。PEP8に遵守したコーディングを行えているか確認しているわけですね。
今のままだとチェックしたくないファイルまでチェックしてしまうので、設定ファイルを追加しましょう。
1 |
touch app/.flake8 |
1 2 3 4 5 6 7 8 9 10 |
app/.flake8 [flake8] exclude = migrations, __pycache__, manage.py, settings.py, venv, .idea, |
もう一度テストを実行してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
make test ============================== 1 passed in 1.14s =============================== ./shop/admin.py:1:1: F401 'django.contrib.admin' imported but unused ./shop/models.py:1:1: F401 'django.db.models' imported but unused ./shop/urls.py:9:1: W391 blank line at end of file ./shop/views.py:1:1: F401 'django.shortcuts.render' imported but unused ./shop/views.py:4:1: E302 expected 2 blank lines, found 1 ./shop/views.py:5:46: W291 trailing whitespace ./shop/views.py:6:1: W391 blank line at end of file ./shop/tests/views_test.py:1:1: W391 blank line at end of file ./shop/tests/test_views.py:1:1: F401 'pytest' imported but unused ./shop/tests/test_views.py:7:1: W293 blank line contains whitespace ./shop/tests/test_views.py:12:1: W391 blank line at end of file ./shop/tests/test_views.py:12:1: W293 blank line contains whitespace |
先ほどとは違って、settings.pyのチェックなどが外れているのがわかります。
時間があったらメッセージ内容に従って、修正してみて下さい。修正しなくてもこの後の開発には影響はありません。
Model作成
最初のテストはパスしました。
次は、Modelの実装に移りましょう。
Shopping Cartなので、カテゴリーとプロダクトのModelが必要そうです。
まずは、カテゴリーからテストしてみましょう。
1 |
touch app/shop/tests/test_models.py |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
app/shop/tests/test_models.py from django.test import TestCase from shop.models import Category class ModelTest(TestCase): def test_create_category(self): name = 'Black Urban Cushion' slug = 'black-urban-cushion' description = 'This is a category for black urban cushion' # create category object Category.objects.create( name=name, slug=slug, description=description) c = Category.objects.all()[0] assert str(c) == c.name |
Categoryモデルを作成後、データベースからCategoryモデルを取得し、データのチェックをします。
テストを実行してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
make test ==================================== ERRORS ==================================== __________________ ERROR collecting shop/tests/test_models.py __________________ ImportError while importing test module '/app/shop/tests/test_models.py'. Hint: make sure your test modules/packages have valid Python names. Traceback: shop/tests/test_models.py:3: in <module> from .models import Category E ImportError: attempted relative import with no known parent package !!!!!!!!!!!!!!!!!!! Interrupted: 1 errors during collection !!!!!!!!!!!!!!!!!!!! =============================== 1 error in 0.38s =============================== make: *** [test] エラー 2 |
エラーになりました。
Categoryモデルの読み込みに失敗してますね。作成していないので、当然です。
models.pyにCategoryモデルを追加してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
app/shop/models.py from django.db import models class Category(models.Model): name = models.CharField(max_length=250, unique=True) slug = models.SlugField(max_length=250, unique=True) description = models.TextField(blank=True) image = models.ImageField(upload_to='category', blank=True) class Meta: ordering = ('name',) verbose_name = 'category' verbose_name_plural = 'categories' def __str__(self): return '{}'.format(self.name) |
models.pyに変更を加えたので、マイグレーションをしておきましょう。
1 |
make migrate |
テストしてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
make test =================================== FAILURES =================================== ________________________ ModelTest.test_create_category ________________________ self = <test_models.ModelTest testMethod=test_create_category> def test_create_category(self): name = 'Black Urban Cushion' slug = 'black-urban-cushion' description = 'This is a category for black urban cushion' # create category object Category.objects.create( name=name, slug=slug, > description=descriptoin) E NameError: name 'descriptoin' is not defined description = 'This is a category for black urban cushion' name = 'Black Urban Cushion' self = <test_models.ModelTest testMethod=test_create_category> slug = 'black-urban-cushion' shop/tests/test_models.py:17: NameError |
おっと!タイポしてしまったようです。descriptionが、descriptoinになっていますね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
app/shop/models.py class ModelTest(TestCase): def test_create_category(self): name = 'Black Urban Cushion' slug = 'black-urban-cushion' description = 'This is a category for black urban cushion' # create category object Category.objects.create( name=name, slug=slug, description=description) # fix c = Category.objects.all()[0] assert str(c) == c.name |
もう一度テストしてみましょう。
1 2 3 |
make test ============================== 2 passed in 1.12s =============================== |
今度は、パスしましたね。
同じ要領でプロダクトも作成しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
app/shop/tests/test_models.py from django.test import TestCase from shop.models import Category, Product # new class ModelTest(TestCase): def test_create_category(self): name = 'Black Urban Cushion' slug = 'black-urban-cushion' description = 'This is a category for black urban cushion' # create category object Category.objects.create( name=name, slug=slug, description=description) c = Category.objects.all()[0] assert str(c) == c.name # new def test_create_product(self): c = Category.objects.create( name='Black Urban Cushion', slug='black-urban-cushion', description='This is a category for black urban cushion') Product.objects.create( name='pag dog', slug='pag-dog', description='Pag dog is as big as cats', category=c, price=30.2, stock=30, available=True), p = Product.objects.all()[0] assert str(p) == p.name |
テストを実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
make test ==================================== ERRORS ==================================== __________________ ERROR collecting shop/tests/test_models.py __________________ ImportError while importing test module '/app/shop/tests/test_models.py'. Hint: make sure your test modules/packages have valid Python names. Traceback: shop/tests/test_models.py:3: in <module> from shop.models import Category, Product E ImportError: cannot import name 'Product' from 'shop.models' (/app/shop/models.py) !!!!!!!!!!!!!!!!!!! Interrupted: 1 errors during collection !!!!!!!!!!!!!!!!!!!! =============================== 1 error in 0.33s =============================== make: *** [test] エラー 2 |
予想通りエラーになりましたね。今度は、プロダクトをmodels.pyに追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
app/shop/models.py ... # new class Product(models.Model): name = models.CharField(max_length=250, unique=True) slug = models.SlugField(max_length=250, unique=True) description = models.TextField(blank=True) category = models.ForeignKey(Category, on_delete=models.CASCADE) price = models.DecimalField(max_digits=10, decimal_places=2) image = models.ImageField(upload_to='product', blank=True) stock = models.IntegerField() available = models.BooleanField(default=True) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) class Meta: ordering = ('name',) verbose_name = 'product' verbose_name_plural = 'products' def __str__(self): return '{}'.format(self.name) |
マイグレーションを実行しましょう。
1 |
make migrate |
テストを実行します。
1 2 3 |
make test ============================== 3 passed in 1.20s =============================== |
テストがパスしましたね。
商品一覧の作成
モデルがパスしたので、データの格納場所は確保できました。
続いては、画面の表示機能部分を作成しましょうか。
ユーザーが「localhost:8000」にアクセスしたときに、商品一覧が画面上に表示されるように実装していきます。
まずは、テストコードからですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
app/shop/tests/test_views.py from django.test import TestCase class ViewTest(TestCase): def test_first_page(self): response = self.client.get('/') assert response.status_code == 200 # new def test_productlist_used_template(self): response = self.client.get('/') self.assertTemplateUsed(response, 'shop/product_list.html') |
localhost:8000にアクセスしたとき、product_list.htmlテンプレート利用する想定なので、ここからまずはテストします。
1 2 3 |
make test E AssertionError: No templates used to render the response |
想定通りのエラーがでましたね。テンプレートを使用するようにviews.pyを書き換えましょう。
1 2 3 4 5 6 7 8 |
app/shop/views.py from django.shortcuts import render def all_products(request): return render(request, 'shop/product_list.html', {}) |
テストを実行します。
1 |
django.template.exceptions.TemplateDoesNotExist: shop/product_list.html |
「TemplateDoesNotExist」が表示されましたね。テンプレートを作成しましょう。
1 2 3 |
mkdir -p app/shop/templates/shop touch app/shop/templates/shop/product_list.html |
テストを実行しましょう。
1 2 3 |
make test ============================== 4 passed in 1.19s =============================== |
テストがパスしましたね。
長くなってきたので、本日は一旦終了にします。
次回
商品一覧の作成の続きを開発していきましょう。
コメントを残す
コメントを投稿するにはログインしてください。