色々なテストコードを作成してみよう の講座の5レッスン目。
今回はUI操作の自動テスト(E2Eテスト)を勉強します。
前回のレッスンはこちら↓
前回まででサーバーサイドのテストはほぼできるようになった
次はUIのテストだ。いわゆるフロントエンドのテスト方法を学んでいくぞ
前回も言ってたけど、フロントエンドって・・・??
百聞は一見にしかず。まずは下記を見てくれ
これは、今回作る「いいねするボタン」の動作だ。
「いいねする」が押されると、その投稿にいいねがつく。
逆に「いいね済」が押されると、その投稿のいいねが削除される。
画面リロードはされず、
JavaScriptで、いいね!APIを叩いて、動的にDB更新を行なっている。
また、APIのレスポンス結果をもとにボタンの表示も変更している。
要するに、ブラウザ上のJavaScriptだけで機能を作っちゃうやつ。
こういうのをフロントエンドと言う
フロントエンド、つまりJavaScriptが絡んでくるようなテストは、
今までのテスト方法では実施できない
そこで、Seleniumを使ってテストを実施していくぞ
テスト対象の準備
ではいつも通り、準備をしていくぞ。
リポジトリのクローン
ベースのアプリをcloneする
$ git clone https://github.com/yheihei/base_diary.git
$ cd base_diary
テストコードだけ見る場合
機能とテストコードを書いたブランチを用意しておいた。
答えだけ先に見たいやつは下記コマンドでチェックアウトしてくれ
# テストコードがすでに書いてあるブランチを見たい場合はこちら
$ git fetch && git checkout feature/#6
テスト対象の解説
もう一度この日記アプリの画面表示を見てみよう。
トップページにアクセスすると、こんな画面が出てくる
仕様は下記。
- トップページにアクセスすると、日記一覧が表示される
- 各日記には下記が表示される
- タイトル
- 投稿者
- 本文
- 投稿日
- カテゴリー
今回実装する機能
ではいいね機能を作っていく。
完成イメージは下記だ。
ログイン状態で日記一覧に行くと、「いいねする」ボタンが表示される。
「いいねする」ボタンを押すと、いいねしたユーザーと、その投稿が、いいねデータベースに保存される。
「いいねする」ボタンは、非同期通信を行い、いいねの追加、削除APIを使ってブラウザ上でデータベースを更新する。
method | エンドポイント | 名前 | どんな動作か |
---|---|---|---|
POST | /api/stars/ | いいね追加 | いいねしたユーザーIDと投稿IDを受け取って、いいねを保存する |
DELETE | /api/stars/:id/ | いいね削除 | いいねのIDを受け取って、そのいいねを削除する |
うへえ〜、APIも作るのか
機能追加する
いいねModelを作成する
まずはいいねのModelを作成する。
diary/models.py
に下記を追加する。
...
...
class Star(models.Model):
'''いいねモデル'''
class Meta:
constraints = [
models.UniqueConstraint(
fields=["post", "user"],
name="post_user_unique",
),
]
post = models.ForeignKey(Post, on_delete=CASCADE)
user = models.ForeignKey(User, on_delete=CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
投稿(Post)とUser(投稿者)だけあれば十分だ。
ただし、1人の投稿者が1つの投稿に何回もいいねできたら微妙なので、models.UniqueConstraint
を設定して、投稿(Post)とUser(投稿者)で複合ユニークにしておく。
ついでにadminに表示するためにdiary/admin.py
に下記を追加しておく。
from django.contrib import admin
from .models import User, Post, Category, Star # 追加
# Register your models here.
admin.site.register(User)
admin.site.register(Post)
admin.site.register(Category)
admin.site.register(Star) # 追加
テーブル定義が追加されたので、一度マイグレーションを行なっておく。下記コマンドだ。
$ python manage.py makemigrations
$ python manage.py migrate
いいねのAPIを用意する
フロントエンドからいいね追加/削除できるように、APIを作る。
ここではAPIを自動テストするのレッスンでも使った、django-rest-framework
を使うぞ。
まずはdjango-rest-frameworkをインストールする。
$ pip install djangorestframework
次にsettings.py
でdjango-rest-frameworkのアプリを読み込むようにする。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'diary',
'rest_framework', # 追加
]
最後に/api/stars/
のエンドポイントを実装する。
diary/views.py
に下記を追加。
from rest_framework import viewsets, serializers
from diary.models import Star
...
...
class StarSerializer(serializers.ModelSerializer):
class Meta:
model = Star
fields = ('id', 'post', 'user', 'created_at', 'updated_at',)
class StarViewSet(viewsets.ModelViewSet):
queryset = Star.objects.all()
serializer_class = StarSerializer
diary/urls.py
を下記のようにする。
from django.urls import path
from django.conf.urls import url, include
from . import views
from rest_framework import routers
from rest_framework.authtoken import views as auth_views
app_name = "diary"
router = routers.DefaultRouter()
router.register(r'stars', views.StarViewSet)
urlpatterns = [
path("", views.index, name="index"),
url(r'^api/', include(router.urls)),
]
これでいいねに関するAPIのCRUDが全て作成された
前のレッスンでも言ったが、django-rest-frameworkの詳しい説明は省く。
テスト方法のレッスンだから
この状態でhttp://127.0.0.1:8000/api/stars/
にアクセスすると
いいねの一覧取得ができるようになった。
改めて確認はしないが、下記のAPIもすでに作られている。
method | エンドポイント | 名前 | どんな動作か |
---|---|---|---|
POST | /api/stars/ | いいね追加 | いいねしたユーザーIDと投稿IDを受け取って、いいねを保存する |
DELETE | /api/stars/:id/ | いいね削除 | いいねのIDを受け取って、そのいいねを削除する |
フロントエンドの実装を行う
次はいよいよ、いいねボタンを押したら、いいねを追加する実装をする
でもさ、僕JavaScriptのことなんもわからんのだけど。。。
そういうと思った。
例によって、このレッスンはテストのレッスンなので
フロントエンド部分は走り抜けるぞ。(コピペで)
JavaScriptやCSSを格納するフォルダを用意する
まずはJavaScriptを格納するためのフォルダを用意する。
basediary/settings.py
に下記を追加してくれ。
...
...
STATICFILES_DIRS = (
os.path.join(BASE_DIR, "static"),
)
これで、staticフォルダ配下に何かを置くと、テンプレート側から参照できるようになる。
JavaScript、CSSを作成する
ここからはJavaScriptやCSSを知らなければ意味不明だ
が、特に解説はしない。コピペで走り抜けてくれ
解説するのに3レッスンぐらいかかるような気がするから
まずは、static/css/style.css
を作って、下記のように記述する。
.star--disabled {
pointer-events: none;
}
.star--enabled {
pointer-events: initial;
}
次に、static/js/app.js
を作って、下記のように記述する。
class starButton {
constructor(csrftoken, postId, userId, starId=0) {
this.csrftoken = csrftoken;
this.postId = postId;
this.userId = userId;
this.starId = starId;
this.element = document.querySelector(`.star.star--post-id-${postId}`);
/**
* 初期化
*/
this.init = () => {
if (this.starId) {
// いいね済の場合
this.element.textContent = 'いいね済';
this.element.removeEventListener('click', this.addStar)
this.element.addEventListener('click', this.removeStar)
} else {
// まだいいね!していない
this.element.textContent = 'いいねする';
this.element.removeEventListener('click', this.removeStar)
this.element.addEventListener('click', this.addStar)
}
}
/**
* 通信中にいいね!ボタン押下不可にする
*/
this.disabled = () => {
this.element.classList.remove('star--enabled');
this.element.classList.add('star--disabled');
}
/**
* いいね!ボタン押下可能にする
*/
this.enabled = () => {
this.element.classList.remove('star--disabled');
this.element.classList.add('star--enabled');
}
/**
* 非同期通信でいいね!する
*/
this.addStar = async () => {
this.disabled();
let response = await fetch('/api/stars/', {
method: 'POST',
headers: {
'X-CSRFToken': this.csrftoken,
'Content-Type': 'application/json'
},
body:JSON.stringify({
post: this.postId,
user: this.userId
})
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
if (response.status !== 201) {
throw new Error(response.status);
}
const data = await response.json();
this.starId = data['id']
this.init();
this.enabled();
}
/**
* 非同期通信でいいね!を取り消す
*/
this.removeStar = async () => {
this.disabled();
let response = await fetch(`/api/stars/${this.starId}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': this.csrftoken,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
throw new Error('Network response was not ok');
}
if (response.status !== 204) {
throw new Error(response.status);
}
this.starId = 0;
this.init();
this.enabled();
}
}
}
全然分からないけど!?!?
だから分からないって言っただろ。
ひとまず、使い方だけ軽く教えておく。
このJavaScriptはテンプレート側で下記のように記述して使う。
<!-- いいねする場合の実装 -->
<!-- JavaScriptとCSSを読み込む -->
<script src="/static/js/app.js"></script>
<link rel="stylesheet" href="/static/css/style.css" %}">
...
<!-- いいねするボタンを用意 -->
<a href="#" class="star star--post-id-投稿のID">いいねする</a>
<!-- JavaScript側で定義したstarButtonクラスのインスタンスを作り、initメソッドを呼ぶ -->
<script>
document.addEventListener('DOMContentLoaded', function() {
new starButton(CSRFトークン, 投稿のID, ユーザーのID).init();
});
</script>
「いいねする」ボタンを押すと、いいね追加のAPIが叩かれる。投稿のIDとユーザーのIDで誰がどの記事にいいねしたかを指定している。
JavaScript的にはnew starButton()
の引数に、「CSRFトークン」と「いいねしたい投稿のID」、
「いいねしたユーザーのID」を指定するだけだ。
これだけでstarButton
クラスが勝手に「いいねする」ボタンを押した際の処理をやってくれる。
いいね追加のAPIのレスポンスが正常の場合、ボタンの名称が「いいねする」から「いいね済」に変わる。これもstarButton
クラスが勝手にやってくれる。
一方で、いいねを解除する場合は下記のようになる。
<!-- いいねを解除する場合の実装 -->
<!-- JavaScriptとCSSを読み込む -->
<script src="/static/js/app.js"></script>
<link rel="stylesheet" href="/static/css/style.css" %}">
...
<!-- いいね済ボタンを用意 -->
<a href="#" class="star star--post-id-投稿のID">いいね済</a>
<!-- JavaScript側で定義したstarButtonクラスのインスタンスを作り、解除したいいいねのIDを指定する -->
<script>
document.addEventListener('DOMContentLoaded', function() {
new starButton(CSRFトークン, 投稿のID, ユーザーのID, 解除したいいいねのID).init();
});
</script>
new starButton()
の第三引数に「解除したいいいねのID」を指定すればOK。
views.pyの実装
ではいよいよstarButton
クラスを使って、いいねボタンを実装してみる。
まずはdiary/views.py
のindex
関数を下記のように修正する。
from django.db.models import Prefetch
from diary.models import Star
...
...
def index(request):
posts = Post.objects.all()
if request.user.is_authenticated:
posts = posts.prefetch_related(
Prefetch(
'star_set',
queryset=Star.objects.filter(user=request.user),
to_attr='stars'
)
)
return render(request, 'index.html', {
'posts': list(posts),
})
prefetch_related
を使って、PostからStarを逆参照で取って来ている。
Starの中でも
queryset=Star.objects.filter(user=request.user)
をすることで、ログインユーザーのStarだけを取って来れる。
例えば以下のような感じで、そのユーザーがそのpostにいいねしているかどうか判定できる。
for post in posts:
if len(post.stars) > 0
print('postに紐つくstarがある。つまりユーザーはこのpostにいいねしている')
# starの中身を取りたい時は例えばこう
print(post.stars[0].id)
else:
print('いいねしてない')
これでどの投稿でログインユーザーがいいねしているか、わかるようになった。
index.htmlの実装
index.html側ではpostsのループの中で↑のpost.starsの情報をもとに、JavaScriptの処理を変える。
こんな感じだ。
<html>
<head>
<title>とあるエンジニアの日記帳</title>
{% load static %}
<script src="{% static 'js/app.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<h1>とあるエンジニアの日記帳</h1>
{% csrf_token %}
{% 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>
{% if user.is_authenticated %}
<a href="#" class="star star--post-id-{{ post.id }}">
{% if post.stars|length > 0 %}
いいね済
{% else %}
いいねする
{% endif %}
</a>
<script>
document.addEventListener('DOMContentLoaded', function() {
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
{% if post.stars|length > 0 %}
new starButton(csrftoken, {{ post.id }}, {{ user.id }}, {{ post.stars.0.id }}).init();
{% else %}
new starButton(csrftoken, {{ post.id }}, {{ user.id }}).init();
{% endif %}
});
</script>
{% endif %}
</div>
{% empty %}
<p>No Diaries.</p>
{% endfor %}
</body>
</html>
ポイントは、
{% for post in posts %}
のループの中で、JavaScriptのstarButton
クラスを下記のように使い分けていることだ。
<script>
document.addEventListener('DOMContentLoaded', function() {
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
{% if post.stars|length > 0 %}
new starButton(csrftoken, {{ post.id }}, {{ user.id }}, {{ post.stars.0.id }}).init();
{% else %}
new starButton(csrftoken, {{ post.id }}, {{ user.id }}).init();
{% endif %}
});
</script>
{% if post.stars|length > 0 %}
の部分で、その投稿がいいね済みかどうかを判定している。
いいね済であれば↓でいいね解除の処理を。
new starButton(csrftoken, {{ post.id }}, {{ user.id }}, {{ post.stars.0.id }}).init();
まだいいねしていなければ、↓でいいね追加の処理をしている。
new starButton(csrftoken, {{ post.id }}, {{ user.id }}).init();
完成品の確認
この状態で、ログインし、http://127.0.0.1:8000/
にアクセスしてみよう。
いいねするボタンが出てきた〜〜〜!! 長かった〜〜〜!!
テストコードを書く
いよいよUIのテストコードを書いてみる
実現したいテストは下記だ。
- ログインして
- トップページを開き
- 「いいねする」ボタンを押したら
- 「いいねする」ボタンが「いいね済」ボタンに変わること
- いいねがDBに保存されること
え〜っと、テストコードでUI操作って全然イメージがつかないんだけど。。。
簡単だ。いつも通りpython manage.py test
と叩くと、
勝手にブラウザが立ち上がってテストが行われる
え!?
テストの完成形を確認する
見た方が早い。実際に、テスト実行するとこんな感じになる
すげ〜〜〜〜!!!
DjangoがSeleniumを使ってブラウザを起動、そのブラウザを操作して、テストしている。
それじゃあ、早速やってみるぞ。
Seleniumをインストールする
まずは必要なパッケージをインストールしていくぞ。
$ pip install selenium webdriver-manager
selenium
はブラウザを起動して操作するモジュール。
webdriver-manager
はChromeブラウザを動かすために必要なモジュールだ。
テストデータを入れる
次は恒例のテストデータ入れだ。
テストデータのfixturesをdiary/fixtures/test/test_index_view.json
に入れる。
[
{
"model": "diary.user",
"pk": 1,
"fields": {
"password": "pbkdf2_sha256$180000$v5YxxVKCObi7$SrCWUES4Qedh+fsHJIx2az1cMgS6wC4O7JapYTbj1/4=",
"last_login": "2021-06-07T13:09:31.834Z",
"is_superuser": true,
"username": "yhei",
"first_name": "",
"last_name": "",
"email": "yheihei0126@gmail.com",
"is_staff": true,
"is_active": true,
"date_joined": "2021-06-07T13:05:56.903Z",
"groups": [],
"user_permissions": []
}
},
{
"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"
}
}
]
これを読み込めば、「yhei」という投稿者が作成され、日記2件が読み込まれる。
ブラウザを起動する
テストコードを書いていくぞ。流れは↓だ。
- seleniumを使うためのモジュールをimport
- selenium用のクラス
StaticLiveServerTestCase
を継承してテストクラスを作る setUpClass
内でブラウザを起動tearDownClass
内でブラウザを落とす
# 1. seleniumを使うためのモジュールをimport
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
# 2. selenium用のクラスStaticLiveServerTestCaseを継承してテストクラスを作る
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
class UiTest(StaticLiveServerTestCase):
fixtures = ['diary/fixtures/test/test_index_view.json']
@classmethod
def setUpClass(cls):
super().setUpClass()
options = Options()
# 3. setUpClass内でブラウザを起動
# options.add_argument('--no-sandbox') # UIのないOSの場合首開けすること
# options.add_argument('--headless') # UIのないOSの場合首開けすること
cls.selenium = webdriver.Chrome(ChromeDriverManager().install(), options=options)
@classmethod
def tearDownClass(cls):
# 4. tearDownClass内でブラウザを終了させる
cls.selenium.quit()
super().tearDownClass()
def test_1(self):
pass
この状態でテストを実行すると。
$ python manage.py test
おお! ブラウザが起動した!
地味に重要な点として
StaticLiveServerTestCase
を継承して作るのがポイントだ。
LiveServerTestCase
を継承して作る方法がよくWeb上に出回っているが、
LiveServerTestCaseの場合、static配下のjsとかcssにアクセスできない。
staticフォルダ配下のリソースをもろに使う今回のテストケースでは、StaticLiveServerTestCase
を使うようにしてくれ。
2. StaticLiveServerTestCase
Selenium demands the test class to be either a LiveServerTestCase or a StaticLiveServerTestCase, that’s because it needs the server running to test the site.
https://elennion.wordpress.com/2018/11/23/django-automated-testing-with-selenium/
Both classes are similar, the difference is that the latter will load the static content (custom css and javascript files for instace) while the former won’t.
I prefer StaticLiveServerTestCase because one of the reasons for using selenium is to test the javascript functions, so, static content is necessary.
ログインする
OK! ブラウザも起動するようになったし、いいねを押すコードを…
待て。ログインしないといいねボタンが出てこない。
まずはログイン操作をseleniumにやらせてみるぞ。
テスト関数の前に必ず実行される、setUp
関数内でログイン操作をやってみる。
def setUp(self) -> None:
# ログイン
self.selenium.get(self.live_server_url + '/admin/login/')
username_input = self.selenium.find_element_by_name('username')
username_input.send_keys('yhei')
password_input = self.selenium.find_element_by_name('password')
password_input.send_keys('password')
self.selenium.find_element_by_css_selector('[type=submit]').click()
一個ずつ解説する。
まずは下記でログイン画面に遷移する。
self.selenium.get(self.live_server_url + '/admin/login/')
self.selenium.get
で指定したURLに遷移する。self.live_server_url
でURLのホスト部分が取得できる。localhostのport番号がテスト実行する度に変化するので、self.live_server_url
を使うのが必須だ。
次に、ユーザー入力の要素をfind_element_by_name
を使ってname指定で取得する。
username_input = self.selenium.find_element_by_name('username')
取得した要素に下記でテキストを入力する。
username_input.send_keys('yhei')
これでユーザー名のフォームにテキストが入る。
同じようにPasswordの部分にもテキスト入力する。
password_input = self.selenium.find_element_by_name('password')
password_input.send_keys('password')
さらにセレクター指定で、Log inボタンの要素を取得してそのままclick()
でクリックする。
self.selenium.find_element_by_css_selector('[type=submit]').click()
いいねボタンを押す
ログインできたらトップに遷移し、いいねするボタンを押してみる。
...
from diary.models import Star
from django.urls import reverse_lazy
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
...
...
def test_1(self):
self.selenium.get('%s%s' % (self.live_server_url, str(reverse_lazy('diary:index'))))
star_button_element = self.selenium.find_element_by_class_name('star--post-id-1')
star_button_element.click()
find_element_by_class_name
でいいねするボタンのCSSのclass名を指定して取得、その後click()
でクリックする。
さっきとほぼ同じだな。
いいね追加APIの通信が終わったらassertを書く
あとはいいねがDBに保存されたか。
ボタンのテキストがちゃんと変わったかを確認する〜っと。
def test_1(self):
'''
いいねするボタンを押したら、ボタンの名称が「いいね済」に変わること、いいねがDBに保存されること
'''
self.selenium.get('%s%s' % (self.live_server_url, str(reverse_lazy('diary:index'))))
star_button_element = self.selenium.find_element_by_class_name('star--post-id-1')
star_button_element.click()
self.assertEqual('いいね済', star_button_element.text)
self.assertEqual(
True,
Star.objects.filter(post__id=1, user__id=1).exists()
)
できた!!!
(ニヤニヤ)
え? なんかダメだった??
一見できたように見えるだろ? 実はこのテストを走らせるとエラーになるはずだ。
なぜなら「いいねする」ボタンを押した後、すぐにDBを確認しにいっている。
すると、早すぎてAPIの処理が終わる前にassertEqualをしてしまう。
======================================================================
FAIL: test_1 (diary.tests.UiTest)
いいねするボタンを押したら、ボタンの名称が「いいね済」に変わること、いいねがDBに保存されること
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/yhei/github/base_diary/diary/tests.py", line 49, in test_1
self.assertEqual('いいね済', star_button_element.text)
AssertionError: 'いいね済' != 'いいねする'
- いいね済
+ いいねする
ほんとだ。「いいねする」ボタンのままになってる!
つまり、APIの通信がちゃんと終わったかどうかを待つ必要がある。
ということで、下記★を追加してみる。
def test_1(self):
'''
いいねするボタンを押したら、ボタンの名称が「いいね済」に変わること、いいねがDBに保存されること
'''
self.selenium.get('%s%s' % (self.live_server_url, str(reverse_lazy('diary:index'))))
star_button_element = self.selenium.find_element_by_class_name('star--post-id-1')
star_button_element.click()
# ★★★ 「APIの通信が完了する」=「ボタンにstar--enabledのクラスがつく」まで待つ
WebDriverWait(self.selenium, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, '.star--post-id-1.star--enabled'))
)
self.assertEqual('いいね済', star_button_element.text)
self.assertEqual(
True,
Star.objects.filter(post__id=1, user__id=1).exists()
)
WebDriverWait
を使うと、until
内の条件が満たされるまで処理をストップすることができる。
EC.presence_of_element_located((By.CSS_SELECTOR, '.star--post-id-1.star--enabled'))
これは、.star--post-id-1.star--enabled
のセレクターを持つ要素が出てくるまで待つ、という記述だ。
ここで「いいねする」ボタンのCSSクラスがどのように変化するかを解説しておく。
「いいねする」ボタンはAPIの通信状態によって下記のようにCSSクラスが変化する。
状態 | CSSクラス | 説明 |
---|---|---|
初期状態 | .star--post-id-1 | |
API通信中 | .star--post-id-1.star--disabled | .star–disabledが付いている間はクリック不可 (連打対策) |
API通信完了 | .star--post-id-1.star--enabled | APIのレスポンスが返ってきたら、.star--disabled を外し、.star--enabled を付与している |
つまり、.star--post-id-1.star--enabled
になっていれば、API通信が完了したことがわかる。
ではもう一度テストをやってみるぞ。
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 38.385s
OK
Destroying test database for alias 'default'...
おお〜。テスト通った!!
UIのテストでは「〇〇がXXするまで待つ」ってのがよくある。
実装側の内部設計を理解しつつテストを組み立てることになる。注意してくれ。
また、「待つ」というのを単純に下記のようにやることもできる。
import time
...
# 3秒待機
time.sleep(3)
秒数指定で待つ方法だ。
ただ、この方法は全くオススメしない。
APIの通信が完了するのは3秒後とは限らない。
今後いいね追加の処理が重くなり、
レスポンスを返すのに3秒以上かかってテストが失敗したら。
エラー内容だけでは、レスポンスが遅くてエラーになったのか、実装をミスってエラーになったのかが判別つかないはずだ。
t
ime.sleep
を使うのは最終手段だと認識してくれ。
いいね済ボタンのテストコードを書いてみよう
では、最後にやってみようのコーナーだ
下記のテストを実装してみてくれ。
- 「いいね済」ボタンを押したら
- 「いいね済」ボタンの表示が「いいねする」に変わることを確認する
- いいねがDBから削除されることを確認する
答えは下記に書いてあるので、どうしてもわからなければ見てやっても良い。
UI操作の自動テストする方法まとめ
今回は、UI操作を自動テスト化する方法を学んだな
Seleniumをインストールした後、ブラウザを起動するコードを書く
# 1. seleniumを使うためのモジュールをimport
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
# 2. selenium用のクラスStaticLiveServerTestCaseを継承してテストクラスを作る
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
class UiTest(StaticLiveServerTestCase):
fixtures = ['diary/fixtures/test/test_index_view.json']
@classmethod
def setUpClass(cls):
super().setUpClass()
options = Options()
# options.add_argument('--no-sandbox') # UIのないOSの場合首開けすること
# options.add_argument('--headless') # UIのないOSの場合首開けすること
cls.selenium = webdriver.Chrome(ChromeDriverManager().install(), options=options)
@classmethod
def tearDownClass(cls):
# 4. tearDownClass内でブラウザを終了させる
cls.selenium.quit()
super().tearDownClass()
def test_1(self):
pass
ログインするときは要素を取得してから、send_keys。
クリックするときはclick()
を使う〜っと。
def setUp(self) -> None:
# ログイン
self.selenium.get(self.live_server_url + '/admin/login/')
username_input = self.selenium.find_element_by_name('username')
username_input.send_keys('yhei')
password_input = self.selenium.find_element_by_name('password')
password_input.send_keys('password')
self.selenium.find_element_by_css_selector('[type=submit]').click()
処理待ちはtime.sleep()
ではなく、WebDriverWait
を使って、処理待ちを行う
# ★★★ APIの通信が完了する=ボタンにstar--enabledのクラスがつくまで待つ
WebDriverWait(self.selenium, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, '.star--post-id-1.star--enabled'))
)
これで大体のテストができるはずだ
今回は以上!
次が最後だ
おお、ついにテストの講座が終わるんだね
うむ。
最後はこの自動テストたちをCI/CDに組み込む方法を伝授するぞ
最後まで読んで、即現場で活かしてくれ
\ 講座で学んだことを即アウトプットしよう /
次の講座
Djangoの基礎を学びたい方は
Djangoの基礎を固めたい方はこちら(セール時に買うのがおすすめ)
『Djangoパーフェクトマスター』〜インスタ映えを支えるPython超高速開発Webフレームワークを徹底解説!
動画講座で手を動かしながら、ほとんどのことが学べます。
ウサギもここから始めました。
コメント
[…] […]