やったーーーー!!
Djangoのアプリができたぞ〜〜〜〜!!!!
お、セコセコなんかやってると思ったら、個人開発か。
ああ、今度のアイデアは最高だ。見ろ!
チュートリアルアプリかな?
どうだ! いいだろ!
この日記アプリさえあれば、いちいちQiitaだのZennだのに書かなくても大丈夫。
どんどん機能追加していって、
ゆくゆくはQiitaを超えるプラットフォーマーになる!
そんで会社を売り払って早期Exit、早期リタイアって寸法よw
まだエンジニアで消耗してるのwww
根拠なき自信っていいよな。。。
テストコード書いた?
ところでこのアプリ、テストコードは書いたのか?
テストコード? うちみたいなベンチャー(1人)には必要ないさ。
市場に速くアプリを出さないといけないんだ。
ベンチャーはスピードが命。
テストコードなんて書いてる暇ないのさ!
コ○スゾ
え?
いや、そういうことを言うと
キレる輩もいるよなあって思っただけ。
テストコードは品質を担保する上で欠かせない。
やり方さえ覚えればすぐだから。
まあやってみようぜ。
今、めっちゃ怖かったよね?
自動テストのイメージ
まずはテストコードの完成形を見る。
お前のイケイケアプリのテストコードを実行してみよう。
こんなイメージだ。
$ python manage.py test -v 2
...
test_1 (diary.tests.TestIndexView)
一覧に日記コンテンツが表示されている ... ok
----------------------------------------------------------------------
Ran 1 test in 0.028s
OK
え? どう言うこと?
コマンド打ったら仕様に基づいたテストをしてくれてんの?
そうだ。
そして、なんらかのバグを埋め込んでテストが失敗した場合は…
FAIL: test_1 (diary.tests.TestIndexView) (message='2つ以上記事がある')
一覧に日記コンテンツが表示されている
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/yhei/github/base_diary/diary/tests.py", line 22, in test_1
self.assertGreater(len(posts), 1)
AssertionError: 1 not greater than 1
こんな感じになる
おお。なんかエラーっぽいのが出てる
2つ以上記事があるってテストでこけてるな。
記事が2つ以上取れていなけりゃならないのに、1つしか記事がない。
記事の取得条件を誰かがミスった可能性がある
なるほど、仕様に基づいてテストコードを書けば、デグレがすぐにわかるってわけか
そんじゃあ実際に書いてみるぜ
テスト対象の準備
リポジトリのクローン
まずは日記アプリのリポジトリをクローンする
$ git clone https://github.com/yheihei/base_diary.git
$ cd base_diary
サンプルデータを入れる
サンプルデータも用意しておいた。
下記コマンドでDBにデータを入れることができる。
$ python manage.py migrate
$ python manage.py loaddata initial.json
テストコードだけ見る場合
ちなみにテストコードを書いたブランチも用意しておいた。
答えだけ先に見たいやつは下記コマンドでブランチをチェックアウトしてくれ
# テストコードがすでに書いてあるブランチを見たい場合はこちら
$ git fetch && git checkout feature/#1
テスト対象の解説
もう一度この日記アプリの画面表示を見てみよう。
トップページにアクセスすると、こんな画面が出ている
仕様をまとめるとこんな感じだ。
- トップページにアクセスすると、日記一覧が表示される
- 各日記には下記が表示される
- タイトル
- 投稿者
- 本文
- 投稿日
- カテゴリー
モデル定義
モデルはこんな感じだな。
from django.db import models
from django.db.models.deletion import CASCADE
from django.contrib.auth.models import AbstractUser
# Create your models here.
class User(AbstractUser):
pass
class Post(models.Model):
id = models.AutoField(primary_key=True)
user = models.ForeignKey(User, on_delete=CASCADE)
title = models.CharField(max_length=2048)
body = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
categories = models.ManyToManyField(
'Category',
blank=True,
related_name="posts",
)
class Category(models.Model):
name = models.CharField(max_length=1024)
slug = models.CharField(max_length=1024)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self) -> str:
return f'{self.name}'
Postモデル(投稿)と、Userモデル(投稿者)が1対1で紐つく。
さらに、PostモデルとCategoryモデルが多対多で紐ついてる。
Viewとテンプレートの定義
views.pyはこんな感じ
from django.shortcuts import render
# Create your views here.
from .models import Post
# Create your views here.
def index(request):
return render(request, 'index.html', {
'posts': Post.objects.all(),
})
取得する記事をPost.objects.all()で取得して、
index.htmlのテンプレート側に渡し
<html>
<head>
<title>とあるエンジニアの日記帳</title>
</head>
<body>
{% load static %}
<h1>とあるエンジニアの日記帳</h1>
{% for post in posts %}
<div>
<h2>{{ post.title }}</h2>
<div>投稿 : {{ post.user }}</div>
<div>{{ post.body |safe }}</div>
<div>{{ post.updated_at }}</div>
<div>カテゴリー :
{% for category in post.categories.all %}
<span>{{ category }},</span>
{% endfor %}
</div>
</div>
{% empty %}
<p>No Diaries.</p>
{% endfor %}
</body>
</html>
トップページのテンプレートで表示するってだけの単純な構造だ
テストコードを書く
テストの実行コマンド
テストを実行するのはメチャクチャ簡単だ。
manage.pyがあるディレクトリで下記コマンドを打つだけ。
# 全テストをする場合
$ python manage.py test
# アプリのディレクトリ単位でテストをする場合
$ python manage.py test ディレクトリ名
# ファイル単位でテストをする場合
$ python manage.py test ディレクトリ名.ファイル名
# テストクラス単位でテストをする場合
$ python manage.py test ディレクトリ名.ファイル名.テストクラス名
# テストクラスの関数単位でテストをする場合
$ python manage.py test ディレクトリ名.ファイル名.テストクラス名.test_xxx
トップページにアクセスするテストを書いてみる
テストの流れは下記だ
- トップページにアクセスし、そのレスポンスを取得する
- レスポンスの中を見て、記事が取れているか確認する
まずはトップページにアクセスして、そのレスポンスを取得してみるぞ。
テストコードは全て、diary/tests.py
に書いていく。こんな感じだ。
from django.test import TestCase, Client
from django.urls import reverse
# Create your tests here.
class TestIndexView(TestCase):
'''#1 画面表示を自動テストする'''
def setUp(self):
pass
def test_1(self):
'''一覧に日記コンテンツが表示されている'''
c = Client()
response = c.get(reverse('diary:index'))
print(response.status_code)
print(response.content)
c = Client()
を使うと、仮想のブラウザ(みたいなもの)が起動される。
それを使って
response = c.get('任意のURL')
で任意のURLにgetリクエストした際のレスポンスを取得できる。
テスト実行すると、トップページにアクセスした際の
response
のステータスコード(response.status_code
)と
htmlの中身(response.content
)がprintされるはずだ。
やってみるぞ。
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
200
b'<html>\n <head>\n <title>\xe3\x81\xa8\xe3\x81\x82\xe3\x82\x8b\xe3\x82\xa8\xe3\x83\xb3\xe3\x82\xb8\xe3\x83\x8b\xe3\x82\xa2\xe3\x81\xae\xe6\x97\xa5\xe8\xa8\x98\xe5\xb8\xb3</title>\n </head>\n
<body>\n \n
<h1>\xe3\x81\xa8\xe3\x81\x82\xe3\x82\x8b\xe3\x82\xa8\xe3\x83\xb3\xe3\x82\xb8\xe3\x83\x8b\xe3\x82\xa2\xe3\x81\xae\xe6\x97\xa5\xe8\xa8\x98\xe5\xb8\xb3</h1>\n \n
<p>No Diaries.</p>\n \n </body>\n</html>'
.
----------------------------------------------------------------------
Ran 1 test in 0.024s
OK
おお! ステータスコード200とhtmlの中身が返ってきてる!
この状態で最も単純なテストコードを書くとしたら。
def test_1(self):
'''一覧に日記コンテンツが表示されている'''
c = Client()
response = c.get(reverse('diary:index'))
self.assertEqual(200, response.status_code)
self.assertEqual(200, response.status_code)
とやることで、ステータスコード(response.status_code)が200と等しいかのテストをしている。
では実際にテストを走らせてみる。
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.014s
OK
この通り、テストが通ればOKと表示されてテストが終了する。
試しにわざとテストエラーとなるコードを書いてみよう。
def test_1(self):
'''一覧に日記コンテンツが表示されている'''
c = Client()
response = c.get('/存在しないパス')
self.assertEqual(200, response.status_code)
c.get('/存在しないパス')
で存在しないパスを指定してみた。
レスポンスのステータスコードは404になるはずだ。
テストを実行してみる。
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_1 (diary.tests.TestIndexView)
一覧に日記コンテンツが表示されている
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/yhei/github/base_diary/diary/tests.py", line 19, in test_1
self.assertEqual(200, response.status_code)
AssertionError: 200 != 404
----------------------------------------------------------------------
Ran 1 test in 0.013s
FAILED (failures=1)
ちゃんと404になって テストがエラーになってるね
トップページの表示項目をテストする
ここからが本題だ。
もう一度トップページの画面を見てみるぞ。
我々はステータスコードのテストをしたいんじゃない。
日記がちゃんと表示されているかをテストしたいはずだ。
そこで、Client().get()
のレスポンスを見る。
Client().get()
のレスポンスではrender()
で渡したcontext
を確認することができる。
context
とはview側のrender
関数で渡している部分のやつだ。
def index(request):
return render(request, 'index.html', {
'posts': Post.objects.all(), # ★これがcontext部分
})
実際にcontext
の中のposts
をどのように取得できるか
↓のようにやると、posts
を取得できるようになる。
response = c.get(reverse('diary:index'))
posts = response.context['posts']
おお。response.context[‘取りたいcontextのkey’] で取れるんだね
これで日記コンテンツがちゃんと出ているかのテストができる。
例えば「記事が1件以上取れていること」とかだったら
def test_1(self):
'''一覧に日記コンテンツが表示されている'''
c = Client()
response = c.get(reverse('diary:index'))
self.assertEqual(200, response.status_code)
posts = response.context['posts']
# 記事が1件以上取れていること
self.assertEqual(True, len(posts) > 0)
こう書いてやるわけだ。
すげーー! テストできる気がしてきた!!!
自動テストのフロー
でもさ。実際このテストコードを走らせてみるとさ
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_1 (diary.tests.TestIndexView)
一覧に日記コンテンツが表示されている
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/yhei/github/base_diary/diary/tests.py", line 22, in test_1
self.assertEqual(True, len(posts) > 0)
AssertionError: True != False
----------------------------------------------------------------------
Ran 1 test in 0.012s
エラー出るんだよね。。。