【Flask】TODOアプリ作成その5:単体テスト

概要

Pythonのflaskフレームワークを使用して、TODOリストを管理するWEBアプリを作成したのでまとめた。
flaskのGET/POSTの方法やセッション等の扱い、Jinjaテンプレートの使い方について、当アプリを作成しながら振り返ることが目的。

当記事ではルーティングファイル(views.py)の単体テストについて扱う。

 

前提

flaskフレームワークを使用して作成したviews.pyに対して、テストを行っている。

あわせて読みたい

概要 Pythonのflaskフレームワークを使用して、TODOリストを管理するWEBアプリを作成したのでまとめた。 flaskのGET/POSTの方法やセッション等の扱いについて、当アプリを作成しながら振り返ることが目的。 当記[…]

あわせて読みたい

概要 Pythonのflaskフレームワークを使用して、TODOリストを管理するWEBアプリを作成したのでまとめた。 flaskのGET/POSTの方法やセッション等の扱いについて、当アプリを作成しながら振り返ることが目的。 当記[…]

あわせて読みたい

概要 Pythonのflaskフレームワークを使用して、TODOリストを管理するWEBアプリを作成したのでまとめた。 flaskのGET/POSTの方法やセッション等の扱い、Jinjaテンプレートの使い方について、当アプリを作成しながら[…]

あわせて読みたい

概要 Pythonのflaskフレームワークを使用して、TODOリストを管理するWEBアプリを作成したのでまとめた。 flaskのGET/POSTの方法やセッション等の扱い、Jinjaテンプレートの使い方について、当アプリを作成しながら[…]

単体テストはunittestモジュールを使用している。
基本的な使用方法については以下を参照。

あわせて読みたい

概要 unittestモジュールを使用して単体テストを作成/実施する方法についてまとめた。   unittestモジュールのポイント   ・unittestモジュールをインポートする[…]

 

ファイル構成

テストファイルとテスト対象ファイルのパスについて。

 

テスト対象ファイル

ルーティングファイル

ルーティングファイル。
ブラウザからHTTPリクエストを受け取り、様々処理を呼び出してHTTPレスポンスを返却するレイヤ。

views.py


from flask import Flask, render_template, request, redirect, url_for, session
import business_logic
import validate_views
import consts

app = Flask(__name__)
app.secret_key = "yiYKQmFC6MTVKs5THpKkD"

@app.route("/")
def index():
    return redirect(url_for("show_login"))

@app.route("/todo_apps")
def show_login():
    """ ログイン画面表示 """
    return render_template("login.html")

@app.route("/todo_apps/login", methods=["POST"])
def login():
    """ ログイン処理 """
    # 入力値検証
    errors = []
    if not validate_views.validate_login(request.form, errors):
        return render_template("login.html", errors=errors)
    
    # ログイン確認
    name = request.form["name"]
    password = request.form["password"]
    user = business_logic.find_user(name, password)
    if user:
        # ログイン成功
        session["name"] = user.name
        session["u_id"] = user.id
        return redirect(url_for("top"))
    else:
        # ログイン失敗
        errors.append("名前またはパスワードが正しくありません。")
        return render_template("login.html", errors=errors)

@app.route("/todo_apps/top")
def top():
    """ TODOタスク一覧画面表示 """
    # タスク一覧を取得
    todos = business_logic.find_todo_all(session["u_id"])
    return render_template("top.html", todos=todos)

@app.route("/logout")
def logout():
    """ ログアウト """
    session.clear()
    return redirect(url_for("show_login"))

@app.route("/todo_apps/top/task/")
def update_todo_done(t_id):
    """ タスクを完了にする """
    is_valid = business_logic.update_todo_status(t_id)
    if not is_valid:
        errors = ["更新に失敗しました。時間を空けて再度実行してください。"]
        # タスク一覧を取得
        todos = business_logic.find_todo_all(session["u_id"])

        return render_template("top.html", todos=todos, errors=errors)
    else:
        return redirect(url_for("top"))

@app.route("/todo_apps/create_input")
def show_create():
    """ タスク新規登録画面を表示する """
    categories = business_logic.get_category_all()
    return render_template("create.html", categories=categories)

@app.route("/todo_apps/create_todo", methods=["POST"])
def create_todo():
    """ タスクを新規登録する """
    # 入力値検証
    error_dict = {}
    is_valid = validate_views.validate_input_todo(request.form, error_dict)
    if not is_valid:
        # 入力エラーの場合
        categories = business_logic.get_category_all()
        return render_template("create.html", categories=categories, error_dict=error_dict) 
    else:
        # タスク登録
        is_valid = business_logic.insert_todo(request.form, session["u_id"])
        if not is_valid:
            # DBエラーの場合
            categories = business_logic.get_category_all()
            error_msg = "タスクの登録に失敗しました。時間を空けて再度実行してください。"
            return render_template("create.html", categories=categories, error_msg=error_msg) 
        else:
            return redirect(url_for("top"))

@app.route("/todo_apps/detail/")
def show_detail(t_id):
    """ タスク詳細画面を表示する """
    todo = business_logic.get_todo(t_id)
    return render_template("detail.html", todo=todo)

@app.route("/todo_apps/edit/")
def show_edit(t_id):
    """ 編集画面を表示する """
    categories = business_logic.get_category_all()
    todo = business_logic.get_todo(t_id)
    return render_template("edit.html", categories=categories, todo=todo)

@app.route("/todo_apps/edit/done", methods=["POST"])
def edit_todo():
    """ タスクを編集する """
    # 入力値検証
    error_dict = {}
    is_valid = validate_views.validate_input_todo(request.form, error_dict)
    if not is_valid:
        # 入力エラーの場合
        categories = business_logic.get_category_all()
        todo = get_todo(request.form)
        return render_template("edit.html", categories=categories, error_dict=error_dict, todo=todo) 
    else:
        # タスク更新
        is_valid = business_logic.update_todo(request.form)
        if not is_valid:
            # DBエラーの場合
            categories = business_logic.get_category_all()
            error_msg = "タスクの更新に失敗しました。時間を空けて再度実行してください。"
            todo = get_todo(request.form)
            return render_template("edit.html", categories=categories, error_msg=error_msg, todo=todo) 
        else:
            return redirect(url_for("show_detail", t_id=request.form["id"]))

@app.route("/todo_apps/delete/")
def delete_todo(t_id):
    """ タスクを削除する """
    is_valid = business_logic.delete_task(t_id)
    if not is_valid:
        # 削除エラーの場合
        return redirect(url_for("show_error", error_id=consts.DB_ERROR))
    else:
        return redirect(url_for('top'))

@app.route("/todo_apps/error")
def show_error():
    """ エラー画面を表示する """
    session.clear()
    error_id = request.args.get("error_id", "")
    if error_id == consts.DB_ERROR:
        # DBエラーの場合
        error_msg = "DBを更新中にエラーが発生しました。時間を空けて、再度ログインからやりなおしてください。"
    else:
        error_msg = "エラーが発生しました。時間を空けて、再度ログインからやりなおしてください。"
    
    return render_template("error.html", error_msg=error_msg)

def get_todo(form):
    """ タスク情報を返却する """
    todo = {
        "id": form.get("id"),
        "title": form.get("title", ""),
        "category_id": form.get("category", ""),
        "content": form.get("content", ""),
        "memo": form.get("memo", ""),
        "due_date": form.get("due_date", "")
    }

    return todo


if __name__ == '__main__':
    # 8080ポートで起動
    app.run(port=8080, debug=True)

 

単体テスト手順

モジュールのインポート

test_views.py


import unittest
from http import HTTPStatus
import os
import sys
# 上の階層のファイルをインポートする場合、以下を先に記載する必要あり
# 追加した上の階層のファイルを参照できるようになる
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from views import app, get_todo
from models import TodoCategory
from unittest.mock import patch

 

unittest

unittestモジュールをインポートすることで、unittestの機能を使用できる。
unittestモジュールは基本的に以下の流れで使用する。

①モジュールのインポート
②テストするためのメソッド群をもつTestCaseを継承する
③各テストメソッドにて、想定した結果と実際にコードを動かした値を比較しテストを行う

 

HTTPStatus

HTTPレスポンスのステータスを定数で保持しているクラス。
ステータスを確認する際に使用している。
詳細は下記の公式ドキュメンテーションを参照。
Python documentation

Source code: Lib/http/__init__.py http is a package that col…

テスト対象フォルダ参照

import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
テスト対象となるviews.pyは親フォルダと同じパスに存在する。
親ファイルと同じパスを相対パスで参照する場合、上記の方法があったため採用した。

テスト資材

from views import app

views.pyに定義しているappインスタンスで、ルーティングやリクエスト処理などを行う。
上記をインポートしないと、ルーティングファイルのテストができない。

from unittest.mock import patch

モックを定義するライブラリ。
ビジネスロジックの処理を意識せず、指定した結果を返すモックを定義できる。

 

事前準備

TestCaseの継承

test_views.py


class TestViews(unittest.TestCase):
    """ ルーティングファイルのテストクラス """

unittestモジュールのTestCaseを継承する。
これにより、assertXXXといったテスト結果を比較するメソッドを使用できる。

 

テスト前処理の定義

test_views.py


def setUp(self):
    """ セットアップ """
    # テスト用のクライアントを準備
    self.app = app.test_client()
    # flaskをテストモードで実行する
    # エラーが発生すると、即座に例外をスローする
    self.app.testing = True
ユニットテストではテストごとに環境が初期化されてしまう。
setUpメソッドの中に処理を記述することで、ユニットテスト共通の事前準備処理を実行できる。
self.app = app.test_client()

テストコードから直接flaskのWebアプリケーションにHTTPリクエストを送信し、HTTPレスポンスを受け取ることができる設定。
これにより、各テストコードから指定のURLへリクエストして、URLに紐づくメソッドを検証できる。

self.app.testing = True

flaskがテストモードになる設定。
これにより、想定しない結果となった場合に即座に例外がスローされるため、テスト結果が正しくないことに気付ける。

※テストモードにしないと、エラーになってもテストが正常に終了しているように見えるケースがある模様(おそらくリダイレクトまでの確認としていて、その先の画面描画でエラーになるケースなど)

 

テスト実施

GETリクエストの方法

パラメータなしの場合

GETリクエストに紐づいたメソッドをテストしたい場合、以下のように記述する。

test_views.py


def test_show_login_route(self):
    """ show_loginメソッドの遷移先確認 """
    # リクエスト
    res = self.app.get("/todo_apps")
    (略)

appインスタンス.getメソッドを使用して、指定したURLに対してGETリクエストを送信できる。

response = self.app.get("URLパス")

 

パラメータありの場合

パラメータを取得するGETリクエストをテストする場合、以下のように記述することでパラメータを送信できる。

test_views.py


def test_show_error_db_error(self):
    """ エラー画面表示確認(DBエラー) """
    # リクエスト
    res = self.app.get("/todo_apps/error?error_id=1")
response = self.app.get("/todo_apps/error?error_id=1")

“?”以降にクエリパラメータを設定することで、GETリクエストのパラメータを設定することができる。

 

POSTリクエストの方法

POSTリクエストに紐づいたメソッドをテストしたい場合、以下のように記述する。

test_views.py


def test_login_success(self):
    """ ログイン成功確認 """
    # POSTデータ
    post_data = dict(name="ヤマダ", password="1234")
    # リクエスト
    res = self.app.post("/todo_apps/login", data=post_data, follow_redirects=True)

 

response = self.app.post("URLパス", 辞書型パラメータ)

appインスタンス.postメソッドを使用して、指定したURLに対してPOSTリクエストを送信できる。

 

# POSTデータ
post_data = dict(name="ヤマダ", password="1234")

また、パラメータは辞書型で定義して、postメソッドの引数に設定する。

 

セッションを設定する方法

セッションから値を取得しているメソッドをテストする場合、以下のように記述する。

test_views.py


def test_top(self, mock_find_todo_all):
    """ TODOトップ画面表示確認 """
    (略)
    with self.app.session_transaction() as session:
        session["u_id"] = 5

 

モックを使用する方法

views.pyは基本的にビジネスロジックレイヤ(business_logic.py)に処理を委譲して、その結果を判定している。
テストを実施する際は、ビジネスロジックレイヤの中身は意識する必要がなく、views.pyが期待する結果を返せばいい。

そんなときに、モックを使用する。
※複数人で開発していて、まだビジネスロジックファイルが実装されていないといった場合でもモックを使用すれば問題なし

モックを使用する場合、以下のように記述する。

test_views.py


@patch("business_logic.get_category_all")
def test_edit_todo_input_error(self, mock_get_category_all):
    """ タスクを編集する確認(入力エラー) """
    # モック定義
    categories = [
        TodoCategory(id=1, category_name="娯楽"),
        TodoCategory(id=2, category_name="余暇")
    ]
    mock_get_category_all.return_value = categories

 

@patch("モジュール名.モジュールのメソッド名")
def test_XXXXX(self, モックを定義する変数名):

モックとして定義したいメソッドをデコレータに定義する。
また、テストメソッドの引数にデコレータで指定したメソッドに対応する変数名を設定する。

 

モックを定義する変数名.return_value = モックの戻り値
return_valueにモックの戻り値として欲しい値を設定する
以上の流れでモックを定義することができる。
注意点

モックを複数定義する場合、デコレータと引数に順序があるため要注意。
これを知らなくてハマってしまった。

例えば以下のように複数モックを定義するケース。

test_views.py


@patch("business_logic.get_category_all")
@patch("validate_views.validate_input_todo")
@patch("business_logic.insert_todo")
def test_create_todo_db_error(self, mock_insert_todo, mock_validate_input_todo, mock_get_category_all):
    """ タスクの新規登録失敗確認(DB登録エラー) """
    # モック定義
    categories = [
        TodoCategory(id=1, category_name="娯楽"),
        TodoCategory(id=2, category_name="余暇")
    ]
    mock_get_category_all.return_value = categories
    mock_validate_input_todo.return_value = True
    mock_insert_todo.return_value = False

 

複数モックを定義する場合、デコレータと引数の位置を上記のように対応させる必要がある。

 

テスト観点

ルーティングファイルの基本的なテスト観点は以下としている。

①ステータスコード:200
②ステータスコード:302
③レスポンスの画面

 

ステータスコード:200

リクエストとレスポンスが正常に疎通できているかを確認する。

test_views.py


def test_index_route(self):
    """ indexメソッドのリダイレクト先確認 """
    # リクエスト
    res = self.app.get("/", follow_redirects=True)
    # 結果確認
    self.assertEqual(res.status_code, HTTPStatus.OK)

 

self.assertEqual(res.status_code, HTTPStatus.OK)

レスポンスのステータスコードが【200】(正常)であることを確認している。

尚、以下をリクエスト時に「follow_redirects=True」を引数で指定すると、リダイレクト先まで追跡する。

 

res = self.app.get(“/”, follow_redirects=True)

本来は「/」を指定すると、views.pyの該当メソッドではリダイレクトする処理で終わっているが、上記を指定することでリダイレクト先のメソッドまで確認できる。

 

ステータスコード:302

テスト対象がリダイレクトしているかを確認する。

test_views.py


def test_index_route_redirect(self):
    """ indexメソッドのリダイレクト確認 """
    # リクエスト
    res = self.app.get("/")
    # 結果確認
    self.assertEqual(res.status_code, HTTPStatus.FOUND)

 

self.assertEqual(res.status_code, HTTPStatus.FOUND)

レスポンスのステータスコードが【302】(リダイレクト)であることを確認している。

 

レスポンスの画面

想定した画面をレスポンスとして返却しているか確認したい場合、画面内に指定したキーワードが存在するかを判定している。
尚、レスポンス内の画面詳細を参照する場合、バイト型になる。

test_views.py


def test_index_route(self):
    """ indexメソッドのリダイレクト先確認 """
    # リクエスト
    res = self.app.get("/", follow_redirects=True)
    # 結果確認
    self.assertEqual(res.status_code, HTTPStatus.OK)
    self.assertIn('利用者ログイン'.encode('utf-8'), res.data)

 

self.assertIn(‘利用者ログイン’.encode(‘utf-8’), res.data)

上記はレスポンスとして返却される画面内に、「利用者ログイン」というキーワードが存在するかを判定している。

 

テストファイル

最終的なテストファイルは以下となった。

import unittest
from http import HTTPStatus
import os
import sys
# 上の階層のファイルをインポートする場合、以下を先に記載する必要あり
# 追加した上の階層のファイルを参照できるようになる
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from views import app, get_todo
from models import TodoCategory
from unittest.mock import patch

class TestViews(unittest.TestCase):
    """ ルーティングファイルのテストクラス """

    def setUp(self):
        """ セットアップ """
        # テスト用のクライアントを準備
        self.app = app.test_client()
        # flaskをテストモードで実行する
        # エラーが発生すると、即座に例外をスローする
        self.app.testing = True

    def test_index_route(self):
        """ indexメソッドのリダイレクト先確認 """
        # リクエスト
        res = self.app.get("/", follow_redirects=True)
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.OK)
        self.assertIn('利用者ログイン'.encode('utf-8'), res.data)
    
    def test_index_route_redirect(self):
        """ indexメソッドのリダイレクト確認 """
        # リクエスト
        res = self.app.get("/")
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.FOUND)

    def test_show_login_route(self):
        """ show_loginメソッドの遷移先確認 """
        # リクエスト
        res = self.app.get("/todo_apps")
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.OK)
        self.assertInResData('利用者ログイン', res)

    def test_login_validate_error(self):
        """ 入力チェックエラー確認 """
        # POSTデータ作成
        post_data = dict(name="", password="")
        # リクエスト
        res = self.app.post("/todo_apps/login", data=post_data)
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.OK)
        self.assertInResData('名前またはパスワードを入力してください。', res)
    
    def test_login_failure(self):
        """ ログイン失敗確認 """
        # POSTデータ作成
        post_data = dict(name="ありえないケース", password="ありえないケース")
        # リクエスト
        res = self.app.post("/todo_apps/login", data=post_data)
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.OK)
        self.assertInResData("名前またはパスワードが正しくありません。", res)

    def test_login_success(self):
        """ ログイン成功確認 """
        # POSTデータ
        post_data = dict(name="ヤマダ", password="1234")
        # リクエスト
        res = self.app.post("/todo_apps/login", data=post_data, follow_redirects=True)
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.OK)
        self.assertInResData("ヤマダ さんのタスク一覧", res)

    @patch("business_logic.find_todo_all")
    def test_top(self, mock_find_todo_all):
        """ TODOトップ画面表示確認 """
        # モック定義
        mock_find_todo_all.return_value = [{"id": 1, "title": "テスト用タスクタイトル", "content": "タスク内容", "category": "カテゴリ名","status": "未完了", "due_date": "2023-07-03"}]
        with self.app.session_transaction() as session:
            session["u_id"] = 5

        # リクエスト
        res = self.app.get("/todo_apps/top")
        # 結果確認
        self.assertEqual(res._status_code, HTTPStatus.OK)
        self.assertInResData("テスト用タスクタイトル", res)

    def test_logout(self):
        """ ログアウト確認 """
        # リクエスト
        res = self.app.get("/logout", follow_redirects=True)
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.OK)
        self.assertInResData("利用者ログイン", res)
        with self.app.session_transaction() as session:
            self.assertNotIn("u_id", session)

    def test_update_todo_done_failure(self):
        """ タスク完了更新の失敗確認 """
        # モック定義
        with self.app.session_transaction() as session:
            session["u_id"] = 1

        # ありえないタスクID
        t_id = 9999
        # リクエスト
        res = self.app.get(f"/todo_apps/top/task/{t_id}")
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.OK)
        self.assertInResData("更新に失敗しました。時間を空けて再度実行してください。", res)

    @patch("business_logic.update_todo_status") 
    def test_update_todo_done_success(self, mock_update_todo_status):
        """ タスク完了更新の成功確認 """
        # モック定義
        mock_update_todo_status.return_value = True
        # リクエスト
        t_id = 50
        res = self.app.get(f"/todo_apps/top/task/{t_id}", follow_redirects=False)
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.FOUND)

    @patch("business_logic.get_category_all")
    def test_show_create(self, mock_get_category_all):
        """ TODO登録画面の表示確認 """
        # モック定義
        categories = [
            TodoCategory(id=1, category_name="娯楽"),
            TodoCategory(id=2, category_name="余暇")
        ]
        mock_get_category_all.return_value = categories
        # リクエスト
        res = self.app.get("/todo_apps/create_input")
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.OK)
        self.assertInResData("娯楽", res)
        self.assertInResData("余暇", res)
        self.assertInResData("タスク登録", res)

    @patch("business_logic.get_category_all")
    def test_create_todo_input_error(self, mock_get_category_all):
        """ タスクの新規登録失敗確認(必須チェックエラー) """
        # モック定義
        categories = [
            TodoCategory(id=1, category_name="娯楽"),
            TodoCategory(id=2, category_name="余暇")
        ]
        mock_get_category_all.return_value = categories
        # POSTデータ
        post_data = dict(category="", title="", content="", due_date="")
        # リクエスト
        res = self.app.post("/todo_apps/create_todo", data=post_data)
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.OK)
        self.assertInResData("娯楽", res)
        self.assertInResData("余暇", res)
        self.assertInResData("カテゴリを選択してください。", res)
        self.assertInResData("タイトルを入力してください。", res)
        self.assertInResData("タスク内容を入力してください。", res)
        self.assertInResData("タスク期日を入力してください。", res)

    @patch("business_logic.get_category_all")
    @patch("validate_views.validate_input_todo")
    @patch("business_logic.insert_todo")
    def test_create_todo_db_error(self, mock_insert_todo, mock_validate_input_todo, mock_get_category_all):
        """ タスクの新規登録失敗確認(DB登録エラー) """
        # モック定義
        categories = [
            TodoCategory(id=1, category_name="娯楽"),
            TodoCategory(id=2, category_name="余暇")
        ]
        mock_get_category_all.return_value = categories
        mock_validate_input_todo.return_value = True
        mock_insert_todo.return_value = False
        with self.app.session_transaction() as session:
            session["u_id"] = 5

        # POSTデータ
        post_data = ""
        # リクエスト
        res = self.app.post("/todo_apps/create_todo", data=post_data)
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.OK)
        self.assertInResData("娯楽", res)
        self.assertInResData("余暇", res)
        self.assertInResData("タスクの登録に失敗しました。時間を空けて再度実行してください。", res)
        self.assertInResData("タスク登録", res)

    @patch("validate_views.validate_input_todo")
    @patch("business_logic.insert_todo")
    def test_create_todo_success(self, mock_insert_todo, mock_validate_input_todo):
        """ タスクの新規登録失敗確認(DB登録エラー) """
        # モック定義
        mock_validate_input_todo.return_value = True
        mock_insert_todo.return_value = True
        with self.app.session_transaction() as session:
            session["u_id"] = 5

        # POSTデータ
        post_data = ""
        # リクエスト
        res = self.app.post("/todo_apps/create_todo", data=post_data)
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.FOUND)
 
    @patch("business_logic.get_todo")
    def test_show_detail(self, mock_get_todo):
        """ タスク詳細画面の表示確認 """
        # モック定義
        test_data = dict(
            id = "1",
            title = "テストタイトル",
            category_id = "1",
            category = "カテゴリ",
            content = "タスク内容",
            memo = "メモ",
            due_date = "2023-07-01",
            status = "未完了"
        )
        mock_get_todo.return_value = test_data
        # リクエスト
        t_id = 1
        res = self.app.get(f"/todo_apps/detail/{t_id}")
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.OK)
        for k, v in test_data.items():
            self.assertInResData(v, res)

    @patch("business_logic.get_category_all")
    @patch("business_logic.get_todo")
    def test_show_edit(self, mock_get_todo, mock_get_category_all):
        """ タスク編集画面の表示確認 """
        # モック定義
        categories = [
            TodoCategory(id=1, category_name="娯楽"),
            TodoCategory(id=2, category_name="余暇")
        ]
        mock_get_category_all.return_value = categories
        test_data = dict(
            id = "1",
            title = "テストタイトル",
            category_id = "1",
            category = "カテゴリ",
            content = "タスク内容",
            memo = "メモ",
            due_date = "2023-07-01",
        )
        mock_get_todo.return_value = test_data
        # リクエスト
        t_id = 1
        res = self.app.get(f"/todo_apps/edit/{t_id}")
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.OK)
        self.assertInResData("娯楽", res)
        self.assertInResData("余暇", res)
        for k, v in test_data.items():
            self.assertInResData(v, res)

    @patch("business_logic.get_category_all")
    def test_edit_todo_input_error(self, mock_get_category_all):
        """ タスクを編集する確認(入力エラー) """
        # モック定義
        categories = [
            TodoCategory(id=1, category_name="娯楽"),
            TodoCategory(id=2, category_name="余暇")
        ]
        mock_get_category_all.return_value = categories
        # POSTデータ
        post_data = dict(id="1", category="", title="", content="", due_date="")
        # リクエスト
        res = self.app.post("/todo_apps/edit/done", data=post_data)
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.OK)
        self.assertInResData("娯楽", res)
        self.assertInResData("余暇", res)
        self.assertInResData("カテゴリを選択してください。", res)
        self.assertInResData("タイトルを入力してください。", res)
        self.assertInResData("タスク内容を入力してください。", res)
        self.assertInResData("タスク期日を入力してください。", res)

    @patch("business_logic.update_todo")
    @patch("business_logic.get_category_all")
    def test_edit_todo_db_error(self, mock_get_category_all, mock_update_todo):
        """ タスクを編集する確認(DBエラー) """
        # モック定義
        categories = [
            TodoCategory(id=1, category_name="娯楽"),
            TodoCategory(id=2, category_name="余暇")
        ]
        mock_get_category_all.return_value = categories
        mock_update_todo.return_value = False
        # POSTデータ
        post_data = dict(
            id = "1",
            title = "テストタイトル",
            category = "1",
            content = "タスク内容",
            memo = "メモ",
            due_date = "2023-07-01"
        )
        # リクエスト
        res = self.app.post("/todo_apps/edit/done", data=post_data)
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.OK)
        self.assertInResData("娯楽", res)
        self.assertInResData("余暇", res)
        self.assertInResData("タスクの更新に失敗しました。時間を空けて再度実行してください。", res)

    @patch("business_logic.update_todo")
    @patch("business_logic.get_category_all")
    def test_edit_todo_success(self, mock_get_category_all, mock_update_todo):
        """ タスクを編集する確認(DBエラー) """
        # モック定義
        categories = [
            TodoCategory(id=1, category_name="娯楽"),
            TodoCategory(id=2, category_name="余暇")
        ]
        mock_get_category_all.return_value = categories
        mock_update_todo.return_value = True
        # POSTデータ
        post_data = dict(
            id = "1",
            title = "テストタイトル",
            category = "1",
            content = "タスク内容",
            memo = "メモ",
            due_date = "2023-07-01"
        )
        # リクエスト
        res = self.app.post("/todo_apps/edit/done", data=post_data)
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.FOUND)

    @patch("business_logic.delete_task")
    def test_delete_todo_error(self, mock_delete_task):
        """ タスクの削除失敗 """
        # モック定義
        mock_delete_task.return_value = False
        # リクエスト
        t_id = 1
        res = self.app.get(f"/todo_apps/delete/{t_id}", follow_redirects=True)
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.OK)
        self.assertInResData("DBを更新中にエラーが発生しました。時間を空けて、再度ログインからやりなおしてください。", res)

    @patch("business_logic.delete_task")
    def test_delete_todo_error(self, mock_delete_task):
        """ タスクの削除失敗 """
        # モック定義
        mock_delete_task.return_value = True
        # リクエスト
        t_id = 1
        res = self.app.get(f"/todo_apps/delete/{t_id}")
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.FOUND) 

    def test_show_error_db_error(self):
        """ エラー画面表示確認(DBエラー) """
        # リクエスト
        res = self.app.get("/todo_apps/error?error_id=1")
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.OK)
        self.assertInResData("DBを更新中にエラーが発生しました。時間を空けて、再度ログインからやりなおしてください。", res)
        self.assertInResData("処理続行エラー", res)

    def test_show_error_other_error(self):
        """ エラー画面表示確認(DBエラー以外) """
        # リクエスト
        res = self.app.get("/todo_apps/error")
        # 結果確認
        self.assertEqual(res.status_code, HTTPStatus.OK)
        self.assertInResData("エラーが発生しました。時間を空けて、再度ログインからやりなおしてください。", res)
        self.assertInResData("処理続行エラー", res)

    def test_get_todo_1(self):
        """ タスク情報を返却確認(全て値あり) """
        form_item = dict(
            id = "1",
            title = "テストタイトル",
            category = "1",
            content = "タスク内容",
            memo = "メモ",
            due_date = "2023-07-01"
        )
        actual = get_todo(form_item)
        expected = dict(
            id = "1",
            title = "テストタイトル",
            category_id = "1",
            content = "タスク内容",
            memo = "メモ",
            due_date = "2023-07-01" 
        )
        self.assertEqual(actual, expected)

    def test_get_todo_2(self):
        """ タスク情報を返却確認(一部値なし) """
        form_item = dict(
            id = "",
            title = "テストタイトル",
            category = "1",
            content = "タスク内容",
            memo = "メモ",
            due_date = "2023-07-01"
        )
        actual = get_todo(form_item)
        expected = dict(
            id = "",
            title = "テストタイトル",
            category_id = "1",
            content = "タスク内容",
            memo = "メモ",
            due_date = "2023-07-01" 
        )
        self.assertEqual(actual, expected)
    
    def test_get_todo_3(self):
        """ タスク情報を返却確認(一部値なし) """
        form_item = dict(
            id = "1",
            title = "",
            category = "1",
            content = "タスク内容",
            memo = "メモ",
            due_date = "2023-07-01"
        )
        actual = get_todo(form_item)
        expected = dict(
            id = "1",
            title = "",
            category_id = "1",
            content = "タスク内容",
            memo = "メモ",
            due_date = "2023-07-01" 
        )
        self.assertEqual(actual, expected)
    
    def test_get_todo_4(self):
        """ タスク情報を返却確認(一部値なし) """
        form_item = dict(
            id = "1",
            title = "テストタイトル",
            category = "",
            content = "タスク内容",
            memo = "メモ",
            due_date = "2023-07-01"
        )
        actual = get_todo(form_item)
        expected = dict(
            id = "1",
            title = "テストタイトル",
            category_id = "",
            content = "タスク内容",
            memo = "メモ",
            due_date = "2023-07-01" 
        )
        self.assertEqual(actual, expected)
    
    def test_get_todo_5(self):
        """ タスク情報を返却確認(一部値なし) """
        form_item = dict(
            id = "1",
            title = "テストタイトル",
            category = "1",
            content = "",
            memo = "メモ",
            due_date = "2023-07-01"
        )
        actual = get_todo(form_item)
        expected = dict(
            id = "1",
            title = "テストタイトル",
            category_id = "1",
            content = "",
            memo = "メモ",
            due_date = "2023-07-01" 
        )
        self.assertEqual(actual, expected)

    def test_get_todo_6(self):
        """ タスク情報を返却確認(全て値あり) """
        form_item = dict(
            id = "1",
            title = "テストタイトル",
            category = "1",
            content = "タスク内容",
            memo = "",
            due_date = "2023-07-01"
        )
        actual = get_todo(form_item)
        expected = dict(
            id = "1",
            title = "テストタイトル",
            category_id = "1",
            content = "タスク内容",
            memo = "",
            due_date = "2023-07-01" 
        )
        self.assertEqual(actual, expected)

    def test_get_todo_7(self):
        """ タスク情報を返却確認(全て値あり) """
        form_item = dict(
            id = "1",
            title = "テストタイトル",
            category = "1",
            content = "タスク内容",
            memo = "メモ",
            due_date = ""
        )
        actual = get_todo(form_item)
        expected = dict(
            id = "1",
            title = "テストタイトル",
            category_id = "1",
            content = "タスク内容",
            memo = "メモ",
            due_date = "" 
        )
        self.assertEqual(actual, expected)

    def assertInResData(self, text, res):
        """ レスポンス内のHTMLに、指定したtext文字列は含まれるか確認 """
        self.assertIn(text.encode("utf-8"), res.data)


if __name__ == '__main__':
    unittest.main()

 

 

 

スポンサーリンク