Day 15 实作测试 (1)

前言

今天要开始写测试,这个部份我们不会特别认真写,重点是要把比较常用的函式秀出来。我们会用最原始的 unittest 套件来写,当然也可以自己改成 pytest 等等其他测试框架。

前置作业

虽然说是前置作业,但这个步骤放到最後也没有关系。我们要来在 manage.py 再加入一个新指令。当然要记得 import unittest

@app.cli.command()
def test():
    tests = unittest.TestLoader().discover("tests")
    unittest.TextTestRunner().run(tests)

这是一段 unittest 很常见的程序码,基本上他会找到之前已经建好的 tests/ 然後跑里面全部的测试。

这样一来,等等测试写完之後就可以使用 flask test 来跑测试了。

basic_test

我们今天会写出 basic test 跟蓝图 main_bp 的测试,先从前者开始。以下程序码要放在 tests/test_basic.py 里面。

import unittest
from flask import url_for
from app import create_app


class BasicTest(unittest.TestCase):
    def setUp(self) -> None:
        self.app = create_app("testing")
        self.client = self.app.test_client()
        self.app_context = self.app.app_context()
        self.app_context.push()

    def tearDown(self) -> None:
        if self.app_context:
            self.app_context.pop()

    def test_app_is_alive(self):
        response = self.client.get(url_for("main.index_page"))
        self.assertEqual(response.status_code, 200)

    def test_blueprint(self):
        self.assertNotEqual(self.app.blueprints.get("main", None), None)
        self.assertNotEqual(self.app.blueprints.get("user", None), None)
        self.assertNotEqual(self.app.blueprints.get("admin", None), None)

我们在 setUp 里面先用 create_app 建好一个 app,接着使用了 test_client,他是一个让我们可以模拟客户端的工具,可以使用他来对 app 发 request。接下来的两行跟 app_context 有关,基本上它就是把一个环境套用到里面,这样之後的程序码都会在这个环境之下。然後因为在 setUp 我们套用了 app_context,那在结束 tearDown 的时候就要把他 pop 掉,这样才不会影响之後的测试。

如果觉得有点难懂的话,可以打开 python,然後直接呼叫 current_app.config,他会告诉你 Working outside of application context.,这时候如果你给他一个 app.app_context().push() (当然 app 要自己宣告) 再呼叫一次,就可以看到他没有错误然後给你一个好好的设定档,就像我们之前看到的那样,而 pop 掉之後他又会变成跑出错误。在这里做的事就接近上述的行为,只是把在同一个 python interpreter 的环境变成在同一次测试的环境。

接下来我们继续看到後面,这里有两个测试,第一个是透过抓首页来确定 app 有没有好好活着;第二个是确定蓝图有没有被好好的载入。我们分开来说明。

  • test_app_is_alive 中,我们使用了刚刚宣告的 self.client,并使用了他的 get 函式,这个跟 HTTP method 的那个 get 是同一个,所以可想而知,後面一定会有 post、put 等等类似的函式。而他会回传一个 response,我们可以来检查他的回应有没有符合预期。此处我们去看他的 status_code 是不是 200。
  • test_blueprint 里面,我们去检查 self.app.blueprints 里面有没有该有的蓝图,他是 dict 型别,所以可以这样去确认。

helper

因为测试很多函式的重复性很高,所以在开始写 main_bp 的测试前,我们先来写一下 helper.py,一样要放在 tests/ 里面。

import unittest
from flask import url_for
from app import create_app
from app.database import db, add_user


class TestModel(unittest.TestCase):
    def setUp(self) -> None:
        self.app = create_app("testing")
        self.client = self.app.test_client()
        self.app_context = self.app.app_context()
        self.app_context.push()
        self.user_data = {"username": "user", "password": "user"}
        self.admin_data = {"username": "admin", "password": "admin"}
        generate_test_data()

    def tearDown(self) -> None:
        db.session.remove()
        db.drop_all()
        if self.app_context is not None:
            self.app_context.pop()

    def user_login(self):
        return self.client.post(url_for("user.login_page"), data=self.user_data)

    def admin_login(self):
        return self.client.post(url_for("user.login_page"), data=self.admin_data)

    def login(self, login):
        if login == "user":
            self.user_login()
        if login == "admin":
            self.admin_login()

    def get(self, login=False):
        self.login(login)
        res = self.client.get(self.route, follow_redirects=True)
        return res

    def post(self, login=False, data=None):
        self.login(login)
        res = self.client.post(self.route, data=data, follow_redirects=True)
        return res


def generate_test_data():
    db.create_all()
    add_user("user", "user", "[email protected]")
    add_user("admin", "admin", "[email protected]", is_admin=True)

在这里面我们定义了一个 TestModel,我们之後的测试都会换成继承他,而非刚刚的 unittest.TestCase,也因为如此,接下来定义的函式都不是测试,而是给测试用的工具。在 setUp 还有 tearDown 跟刚刚做的事差不多,有差别的部份是我们新增了 user_dataadmin_data,这在之後登入会用到。同时我们也用之前写好的 add_user 来加入两个测试用的使用者,如果有需要测试贴文、留言的话,也可以自己加入测试的资料。还有我们也在 tearDown 加入了清除资料库的动作。

接着我们定义了 user_loginadmin_login,这边就用到刚刚提到的 post 这个函式,然後他使用 data 参数把登入资料丢进去。再用 login 把上述两者包装起来给之後的函式用。

接下来我们自己定义了 getpost 两个函式,让它包含登入的功能,登入有很多种写法,但有时候并没有那麽直观,可能会遇到明明登入了但後面发请求的时候又变成没登入,这常常都是因为 context 不对。post 的部分跟刚刚一样都使用 data 参数来把资料传送给後端。我们还用到了一个叫做 follow_redirects 的参数,如果不让他 follow 的话,那 status_code 就会变成 302,然後我们看到的资料也都是重新导向页面的资料,而这通常不是我们乐见的 (除非我们只想确定他有重新导向),因此在此处我们加上这个参数。


<<:  Day01 前言

>>:  [Day15] 帮我们的网站设定 SSL 凭证

第二十九日-MYSQL预存程序 STORED PROCEDURE:来写一个BMI小程序(2)

昨天已经认识分隔符号 DELIMITER和STORED PROCEDURE建立语法, 建立出BMI小...

Day16:SwiftUI—GeometryReader

前言 前面几天介绍了很多设计 SwiftUI 画面的元件, 那要怎麽知道元件的位置和尺寸大小呢? 这...

Day 5 Compose UI Row Layout + Position

今年的疫情蛮严重的,希望大家都过得安好,希望疫情快点过去,能回到一些线下技术聚会的时光~ 今天目标:...

Day 24 快速启动个 JSON Server

前端开发者在後端 api 尚在开发阶段,需要模拟 api 回传一些种子资料时,自行架设一个开发用的 ...

[ Day 13 ] React 的生命周期 - Unmounting

今天终於要进入到生命周期的最後一个阶段: Unmounting 了!在元件要被卸载的这个阶段会发生...