
やったーーーー!!
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クラスに行く。
テストデータを入れてみる
















