やったーーーー!!
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
エラー出るんだよね。。。
これは、記事が1件も入ってないってことだよね?
ねんのため、ローカルのDBをSQLで見ると、
$ sqlite3 db.sqlite3
sqlite> .header on
sqlite> select * from diary_post;
id|title|body|created_at|updated_at|user_id
1|初めての日記です|<p>Djangoで日記アプリを作りました!
<p>adminで日記投稿できるし、ユーザーも追加できる、カテゴリーも作れるし最高!|2021-06-07 13:11:23.615464|2021-06-15 04:03:54.439426|1
2|個人開発、忙しいです|<h3>カテゴリーをつけてみた</h3>
<p>楽しみにしているみなさん、申し訳ございません</p>
<p>個人開発が忙しく、手が回らない状態です
<p>画像表示機能もつけないといけないし、やることはたくさんあります
<p>充実? ってやつですね。
<p>#朝活 #ブログ書け #副業月収3桁|2021-06-07 13:12:19.457595|2021-06-15 04:11:55.771296|1
ちゃんと日記が入ってそうなんだけど。。。
ちゃんとテストできてるの?
テストってちょっと怪しくない??
お前みたいなやつがSQLを書けたことに驚いているが、解説しよう
さらりと悪口言われた??
実はDjangoの自動テストはテスト用の空っぽのDBを作ってテスト実行してるんだ。
簡単なフロー図を作ってみた。
./manage.py test
が実行された後のテストの流れはこの通りだ。
1. テスト用DB作成
最初に、お前がいじっているローカルDBとは別のテスト用DBが新規で作成される。
3. テストデータの読み込み(fixtures)
その後、fixturesを使って、DBの初期データを投入できる。
これはTestCaseクラス内だけで有効な初期データだ。
4-2. テスト実行 test_xxx()
4-2で実際にtest_xxx()のテストが行われる。
4.4 DBロールバック(setUp~tearDown内の変更分)
test_xxx()内で投入したデータはtest_xxx()が終わったら、4.4でロールバックされる。
test_xxx()が終わってtest_yyyに()に移ったら、test_xxx内で投入されたデータは破棄されているってわけ。
5. DBロールバック(fixtures分)
test_xxx()が全て終わったら、「3. テストデータの読み込み(fixtures)」で入れたテストデータを破棄して、次のTestCaseクラスに行く。
テストデータを入れてみる
うーん。ひとまずテスト用DBが作られることはわかったけど、
全然わからない
まあそうだな。実際やってみないとわからん。
試しに、日記コンテンツの「3. テストデータの読み込み(fixtures)」をやってみよう。
diary/fixtures/test/test_index_view.json
にDBのデータをjsonで記載する。
[
{
"model": "diary.user",
"pk": 1,
"fields": {
"username": "yhei",
"email": "yheihei0126@gmail.com"
}
},
{
"model": "diary.post",
"pk": 1,
"fields": {
"user": 1,
"title": "タイトル1",
"body": "<p>一つ目の日記本文",
"created_at": "2021-06-07T13:11:23.615Z",
"updated_at": "2021-06-07T13:11:23.615Z",
"categories": [
1
]
}
},
{
"model": "diary.post",
"pk": 2,
"fields": {
"user": 1,
"title": "2つめの記事",
"body": "<p>二つ目の日記本文",
"created_at": "2021-06-07T13:12:19.457Z",
"updated_at": "2021-06-07T13:13:12.822Z",
"categories": [
1
]
}
},
{
"model": "diary.category",
"pk": 1,
"fields": {
"name": "日記カテゴリ",
"slug": "diary",
"created_at": "2021-06-07T13:11:40.564Z",
"updated_at": "2021-06-07T13:11:40.564Z"
}
}
]
DBのデータをjsonで書き直しただけだ。
ええ。。。なんかめんどくさくない?
そういうと思った。
そういうボンクラ向けに、既存DBからjsonをDumpする方法もある。
下記コマンドだ。
$ python manage.py dumpdata アプリ名 --indent=4 > 出力先のjsonファイル名
# 例
python manage.py dumpdata diary --indent=4 > diary/fixtures/test/test_index_view.json
アプリ名を指定してdumpdata
コマンドを実行するだけだ。
まずはこれでテストすると良いかもしれないな。
次はテストデータを読み込んでみるぞ。
下記を追加してくれ。
class TestIndexView(TestCase):
'''#1 画面表示を自動テストする'''
# テスト用データを投入する
fixtures = ['diary/fixtures/test/test_index_view.json']
先ほど作ったjsonをfixturesという配列で指定するだけだ。
配列に複数のファイルパスを入れれば、複数のファイルからデータを入れることもできる。
fixtures = [
'diary/fixtures/test/user.json',
'diary/fixtures/test/post.json',
]
モデルごとにjsonを分けたりしたいやつは使うといい。
確かに一つのJSONにいろんなモデルのデータが入り乱れてたら大変かも
じゃあテストをやってみるぞ。
今のテストコードは下記だ。
from django.test import TestCase, Client
from django.urls import reverse
# Create your tests here.
class TestIndexView(TestCase):
'''#1 画面表示を自動テストする'''
# テスト用データを投入する
fixtures = ['diary/fixtures/test/test_index_view.json']
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).
.
----------------------------------------------------------------------
Ran 1 test in 0.045s
OK
通った〜〜〜〜〜!!
このまま日記の内容を確認するテストを追加してみるぞ。
# 記事にタイトル、本文、カテゴリが存在する
post = posts[0]
self.assertEqual('タイトル1', post.title)
self.assertEqual('<p>一つ目の日記本文', post.body)
self.assertEqual('diary', post.categories.first().slug)
テスト実行!
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.045s
OK
タイトル、本文、カテゴリーの内容が正しいかが確認できた!!!
表示項目をテストする方法まとめ
今回の自動テスト方法をまとめるぞ
from django.test import TestCase, Client
from django.urls import reverse
# Create your tests here.
class TestIndexView(TestCase):
'''#1 画面表示を自動テストする'''
# テスト用データを投入する
fixtures = ['diary/fixtures/test/test_index_view.json']
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)
Clientクラスを使って、表示したいURLにアクセスする
c = Client()
response = c.get(reverse('diary:index'))
self.assertEqualで期待のresponseが取れているか確認する
self.assertEqual(200, response.status_code)
views.pyから渡されているものはresponse.context['hoge']
で取得できる
posts = response.context['posts']
# 記事が1件以上取れていること
self.assertEqual(True, len(posts) > 0)
以上だ。これを覚えるだけで、死ぬほど開発が捗るようになる。
機能がデグレったときは、
このテストがエラーを吐いてくれるはずだ
手動テストしてる暇があったら、こういう簡単なものは
即テスト化するように。
わかったな?
これで自動テストを極めたと言っても過言ではないな!
アホか
まだめちゃくちゃ語ることあるからな!
次は関数とかクラスのテスト方法を解説するから。
ちゃんと復習しとけよ!!!?
こいつウサギのくせにめちゃくちゃ意識たけえな
\ 講座で学んだことを即アウトプットしよう /
次の講座
Djangoの基礎を学びたい方は
Djangoの基礎を固めたい方はこちら(セール時に買うのがおすすめ)
『Djangoパーフェクトマスター』〜インスタ映えを支えるPython超高速開発Webフレームワークを徹底解説!
動画講座で手を動かしながら、ほとんどのことが学べます。
ウサギもここから始めました。
コメント
[…] 画面の表示項目を自動テストするDjango自動テスト講座第一弾! Clientクラス… […]