/images/author.jpg

第 2 章:Hello, Flask!

第 2 章:Hello, Flask! 追溯到最初,Flask 诞生于 Armin Ronacher 在 2010 年愚人节开的一个玩笑。后来,它逐渐发展成为一个成熟的 Python Web 框架,越来越受到开发者的喜爱。目前它在 GitHub 上是 Star 数量最多的 Python Web 框架,没有之一。 Flask 是典型的微框架,作为 Web 框架来说,它仅保留了核心功能:请求响应处理和模板渲染。这两类功能分别由 Werkzeug(WSGI 工具库)完成和 Jinja(模板渲染库)完成,因为 Flask 包装了这两个依赖,我们暂时不用深入了解它们。 主页 这一章的主要任务就是为我们的程序编写一个简单的主页。主页的 URL 一般就是根地址,即 /。当用户访问根地址的时候,我们需要返回一行欢迎文字。这个任务只需要下面几行代码就可以完成: app.py:程序主页 1 2 3 4 5 6 from flask import Flask app = Flask(__name__) @app.route('/') def hello(): return 'Welcome to My Watchlist!' 按照惯例,我们把程序保存为 app.py,确保当前目录是项目的根目录,并且激活了虚拟环境,然后在命令行窗口执行 flask run 命令启动程序(按下 Control + C 可以退出): 1 2 3 4 5 6 7 (env) $ flask run * Serving Flask app "app.py" * Environment: production WARNING: Do not use the development server in a production environment. Use a production WSGI server instead. * Debug mode: off * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 现在打开浏览器,访问 http://localhost:5000 即可访问我们的程序主页,并看到我们在程序里返回的问候语,如下图所示: 执行 flask run 命令时,Flask 会使用内置的开发服务器来运行程序。这个服务器默认监听本地机的 5000 端口,也就是说,我们可以通过在地址栏输入 http://127.0.0.1:5000 或是 http://localhost:5000 访问程序。 注意 内置的开发服务器只能用于开发时使用,部署上线的时候要换用性能更好的服务器,我们会在最后一章学习。 解剖时间 下面我们来分解这个 Flask 程序,了解它的基本构成。 首先我们从 flask 包导入 Flask 类,通过实例化这个类,创建一个程序对象 app: 1 2 from flask import Flask app = Flask(__name__) 接下来,我们要注册一个处理函数,这个函数是处理某个请求的处理函数,Flask 官方把它叫做视图函数(view funciton),你可以理解为“请求处理函数”。 所谓的“注册”,就是给这个函数戴上一个装饰器帽子。我们使用 app.route() 装饰器来为这个函数绑定对应的 URL,当用户在浏览器访问这个 URL 的时候,就会触发这个函数,获取返回值,并把返回值显示到浏览器窗口: 1 2 3 @app.route('/') def hello(): return 'Welcome to My Watchlist!' 提示 为了便于理解,你可以把 Web 程序看作是一堆这样的视图函数的集合:编写不同的函数处理对应 URL 的请求。 填入 app.route() 装饰器的第一个参数是 URL 规则字符串,这里的 /指的是根地址。 我们只需要写出相对地址,主机地址、端口号等都不需要写出。所以说,这里的 / 对应的是主机名后面的路径部分,完整 URL 就是 http

第 3 章:模板

第 3 章:模板 在一般的 Web 程序里,访问一个地址通常会返回一个包含各类信息的 HTML 页面。因为我们的程序是动态的,页面中的某些信息需要根据不同的情况来进行调整,比如对登录和未登录用户显示不同的信息,所以页面需要在用户访问时根据程序逻辑动态生成。 我们把包含变量和运算逻辑的 HTML 或其他格式的文本叫做模板,执行这些变量替换和逻辑计算工作的过程被称为渲染,这个工作由我们这一章要学习使用的模板渲染引擎——Jinja2 来完成。 按照默认的设置,Flask 会从程序实例所在模块同级目录的 templates 文件夹中寻找模板,我们的程序目前存储在项目根目录的 app.py 文件里,所以我们要在项目根目录创建这个文件夹: 1 $ mkdir templates 模板基本语法 在社交网站上,每个人都有一个主页,借助 Jinja2 就可以写出一个通用的模板: 1 2 3 4 5 6 <h1>{{ username }}的个人主页</h1> {% if bio %} <p>{{ bio }}</p> {# 这里的缩进只是为了可读性,不是必须的 #} {% else %} <p>自我介绍为空。</p> {% endif %} {# 大部分 Jinja 语句都需要声明关闭 #} Jinja2 的语法和 Python 大致相同,你在后面会陆续接触到一些常见的用法。在模板里,你需要添加特定的定界符将 Jinja2 语句和变量标记出来,下面是三种常用的定界符: {{ ... }} 用来标记变量。 {% ... %} 用来标记语句,比如 if 语句,for 语句等。 {# ... #} 用来写注释。 模板中使用的变量需要在渲染的时候传递进去,具体我们后面会了解。 编写主页模板 我们先在 templates 目录下创建一个 index.html 文件,作为主页模板。主页需要显示电影条目列表和个人信息,代码如下所示: templates/index.html:主页模板 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>{{ name }}'s Watchlist</title> </head> <body> <h2>{{ name }}'s Watchlist</h2> {# 使用 length 过滤器获取 movies 变量的长度 #} <p>{{ movies|length }} Titles</p> <ul> {% for movie in movies %} {# 迭代 movies 变量 #} <li>{{ movie.title }} - {{ movie.year }}</li> {# 等同于 movie['title'] #} {% endfor %} {# 使用 endfor 标签结束 for 语句 #} </ul> <footer> <small>&copy; 2018 <a href="http://helloflask.com/tutorial">HelloFlask</a></small> </footer> </body> </html> 为了方便对变量进行处理,Jinja2 提供了一些过滤器,语法形式如下: 1 {{ 变量|过滤器 }} 左侧是变量,右侧是过滤器名。比如,上面的模板里使用 length 过滤器来获取 movies 的长度,类似 Python 里的 len() 函数。 提示 访问 http://jinja.pocoo.org/docs/2.10/templates/#list-of-builtin-filters 查看所有可用的过滤器。 准备虚拟数据 为了模拟页面渲染,我们需要先创建一些虚拟数据,用来填充页面内容: app.py:定义虚拟数据 1 2 3 4 5 6 7 8 9 10 11 12 13 name = 'Grey Li' movies = [ {'title': 'My Neighbor Totoro', 'year': '1988'}, {'title': 'Dead Poets Society', 'year': '1989'}, {'title': 'A Perfect World', 'year': '1993'}, {'title': 'Leon', 'year': '1994'}, {'title': 'Mahjong', 'year': '1996'}, {'title': 'Swallowtail Butterfly', 'year': '1996'}, {'title': 'King of Comedy', 'year': '1999'}, {'title': 'Devils on the Doorstep', 'year': '1999'}, {'title': 'WALL-E', 'year': '2008'}, {'title': 'The Pork of Music', 'year': '2012'}, ] 渲染主页模板 使用 render_template() 函数可以把模板渲

第 4 章:静态文件

第 4 章:静态文件 静态文件(static files)和我们的模板概念相反,指的是内容不需要动态生成的文件。比如图片、CSS 文件和 JavaScript 脚本等。 在 Flask 中,我们需要创建一个 static 文件夹来保存静态文件,它应该和程序模块、templates 文件夹在同一目录层级,所以我们在项目根目录创建它: 1 $ mkdir static 生成静态文件 URL 在 HTML 文件里,引入这些静态文件需要给出资源所在的 URL。为了更加灵活,这些文件的 URL 可以通过 Flask 提供的 url_for() 函数来生成。 在第 2 章的最后,我们学习过 url_for() 函数的用法,传入端点值(视图函数的名称)和参数,它会返回对应的 URL。对于静态文件,需要传入的端点值是 static,同时使用 filename 参数来传入相对于 static 文件夹的文件路径。 假如我们在 static 文件夹的根目录下面放了一个 foo.jpg 文件,下面的调用可以获取它的 URL: 1 <img src="{{ url_for('static', filename='foo.jpg') }}"> 花括号部分的调用会返回 /static/foo.jpg。 提示 在 Python 脚本里,url_for() 函数需要从 flask 包中导入,而在模板中则可以直接使用,因为 Flask 把一些常用的函数和对象添加到了模板上下文(环境)里。 添加 Favicon Favicon(favourite icon) 是显示在标签页和书签栏的网站头像。你需要准备一个 ICO、PNG 或 GIF 格式的图片,大小一般为 16×16、32×32、48×48 或 64×64 像素。把这个图片放到 static 目录下,然后像下面这样在 HTML 模板里引入它: templates/index.html:引入 Favicon 1 2 3 4 <head> ... <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}"> </head> 保存后刷新页面,即可在浏览器标签页上看到这个图片。 添加图片 为了让页面不那么单调,我们来添加两个图片:一个是显示在页面标题旁边的头像,另一个是显示在页面底部的龙猫动图。我们在 static 目录下面创建一个子文件夹 images,把这两个图片都放到这个文件夹里: 1 2 $ cd static $ mkdir images 创建子文件夹并不是必须的,这里只是为了更好的组织同类文件。同样的,如果你有多个 CSS 文件,也可以创建一个 css 文件夹来组织他们。下面我们在页面模板中添加这两个图片,注意填写正确的文件路径: templates/index.html:添加图片 1 2 3 4 5 6 <h2> <img alt="Avatar" src="{{ url_for('static', filename='images/avatar.png') }}"> {{ name }}'s Watchlist </h2> ... <img alt="Walking Totoro" src="{{ url_for('static', filename='images/totoro.gif') }}"> 提示 这两张图片你可以自己替换为任意的图片(注意更新文件名),也可以在示例程序的 GitHub 仓库下载。 添加 CSS 虽然添加了图片,但页面还是非常简陋,因为我们还没有添加 CSS 定义。下面在 static 目录下创建一个 CSS 文件 style.css,内容如下: static/style.css:定义页面样式 1

第 5 章:数据库

第 5 章:数据库 大部分程序都需要保存数据,所以不可避免要使用数据库。用来操作数据库的数据库管理系统(DBMS)有很多选择,对于不同类型的程序,不同的使用场景,都会有不同的选择。在这个教程中,我们选择了属于关系型数据库管理系统(RDBMS)的 SQLite,它基于文件,不需要单独启动数据库服务器,适合在开发时使用,或是在数据库操作简单、访问量低的程序中使用。 使用 SQLAlchemy 操作数据库 为了简化数据库操作,我们将使用 SQLAlchemy——一个 Python 数据库工具(ORM,即对象关系映射)。借助 SQLAlchemy,你可以通过定义 Python 类来表示数据库里的一张表(类属性表示表中的字段 / 列),通过对这个类进行各种操作来代替写 SQL 语句。这个类我们称之为模型类,类中的属性我们将称之为字段。 Flask 有大量的第三方扩展,这些扩展可以简化和第三方库的集成工作。我们下面将使用一个叫做 Flask-SQLAlchemy 的官方扩展来集成 SQLAlchemy。 首先安装它: 1 (env) $ pip install flask-sqlalchemy 大部分扩展都需要执行一个“初始化”操作。你需要导入扩展类,实例化并传入 Flask 程序实例: 1 2 3 4 5 from flask_sqlalchemy import SQLAlchemy # 导入扩展类 app = Flask(__name__) db = SQLAlchemy(app) # 初始化扩展,传入程序实例 app 设置数据库 URI 为了设置 Flask、扩展或是我们程序本身的一些行为,我们需要设置和定义一些配置变量。Flask 提供了一个统一的接口来写入和获取这些配置变量:Flask.config 字典。配置变量的名称必须使用大写,写入配置的语句一般会放到扩展类实例化语句之前。 下面写入了一个 SQLALCHEMY_DATABASE_URI 变量来告诉 SQLAlchemy 数据库连接地址: 1 2 3 4 5 import os # ... app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////' + os.path.join(app.root_path, 'data.db') 注意 这个配置变量的最后一个单词是 URI,而不是 URL。 对于这个变量值,不同的 DBMS 有不同的格式,对于 SQLite 来说,这个值的格式如下: 1 sqlite:////数据库文件的绝对地址 数据库文件一般放到项目根目录即可,app.root_path 返回程序实例所在模块的路径(目前来说,即项目根目录),我们使用它来构建文件路径。数据库文件的名称和后缀你可以自由定义,一般会使用 .db、.sqlite 和 .sqlite3 作为后缀。 另外,如果你使用 Windows 系统,上面的 URI 前缀部分只需要写入三个斜线(即 sqlite:///)。在本书的示例程序代码里,做了一些兼容性处理,另外还新设置了一个配置变量,实际的代码如下: app.py:数据库配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import os import sys from flask import Flask from flask_sqlalchemy import SQLAlchemy WIN = sys.platform.startswith('win') if WIN: # 如果是 Windows 系统,使用三个斜线 prefix = 'sqlite:///' else: # 否则使用四个斜线 prefix = 'sqlite:////'

第 6 章:模板优化

第 6 章:模板优化 这一章我们会继续完善模板,学习几个非常实用的模板编写技巧,为下一章实现创建、编辑电影条目打下基础。 自定义错误页面 为了引出相关知识点,我们首先要为 Watchlist 编写一个错误页面。目前的程序中,如果你访问一个不存在的 URL,比如 /hello,Flask 会自动返回一个 404 错误响应。默认的错误页面非常简陋,如下图所示: 在 Flask 程序中自定义错误页面非常简单,我们先编写一个 404 错误页面模板,如下所示: templates/404.html:404 错误页面模板 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>{{ user.name }}'s Watchlist</title> <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css"> </head> <body> <h2> <img alt="Avatar" class="avatar" src="{{ url_for('static', filename='images/avatar.png') }}"> {{ user.name }}'s Watchlist </h2> <ul class="movie-list"> <li> Page Not Found - 404 <span class="float-right"> <a href="{{ url_for('index') }}">Go Back</a> </span> </li> </ul> <footer> <small>&copy; 2018 <a href="http://helloflask.com/tutorial">HelloFlask</a></small> </footer> </body> </html> 接着使用 app.errorhandler() 装饰器注册一个错误处理函数,它的作用和视图函数类似,当 404 错误发生时,这个函数会被触发,返回值会作为响应主体返回给客户端: app.py:404 错误处理函数 1 2 3 4 @app.errorhandler(404) # 传入要处理的错误代码 def page_not_found(e): # 接受异常对象作为参数 user = User.query.first() return render_template('404.html', user=user), 404 # 返回模板和状态码 提示 和我们前面编写的视图函数相比,这个函数返回了状态码作为第二个参数,普通的视图函数之所以不用写出状态码,是因为默认会使用 200 状态码,表示成功。 这个视图返回渲染好的错误模板,因为模板中使用了 user 变量,这里也要一并传入。现在访问一个不存在的 URL,会显示我们自定义的错误页面: 编写完这部分代码后,你会发现两个问题: 错误页面和主页都需要使用 user 变量,所以在对应的处理函数里都要查询数据库并传入 user 变量。因为每一个页面都需要获取用户名显示在页面顶部,如果有更多的页面,那么每一个对应的视图函数都要重复传入这个变量。 错误页面模板和主页模板有大量重复的代码,比如 <head> 标签的内容,页首的标题,页脚信息等。这种重复不仅带来不必要的工作量,而且会让修改变得更加麻烦。举例来说,如果页脚信息需要更新,那么每个页面都要一一进行修改。 显而易见,这两个问题有更优雅的处理方法,下面我们来一一了解。 模板上下文处理函数 对于多个模板内都需要使用的变量,我们可以使用 app.context_processor 装饰器注册一个模板上下文处理函数,如下所示: app.py:模板上下文处理函数 1 2 3 4 @app.context_processor def inject_user(): # 函数名可以随意修改 user = User.query.first() return dict(user=user) # 需要返回字典,等同于 return {'user': user} 这个函数返回的变量(以字典键值对的形式)将会统一注入到每一个模板的上下文环境中,因此可以直接在模板中使用。 现在我们可以删除 404 错误处理函数和主页视图函

第 7 章:表单

第 7 章:表单 在 HTML 页面里,我们需要编写表单来获取用户输入。一个典型的表单如下所示: 1 2 3 4 5 6 7 <form method="post"> <!-- 指定提交方法为 POST --> <label for="name">名字</label> <input type="text" name="name" id="name"><br> <!-- 文本输入框 --> <label for="occupation">职业</label> <input type="text" name="occupation" id="occupation"><br> <!-- 文本输入框 --> <input type="submit" name="submit" value="登录"> <!-- 提交按钮 --> </form> 编写表单的 HTML 代码有下面几点需要注意: 在 <form> 标签里使用 method 属性将提交表单数据的 HTTP 请求方法指定为 POST。如果不指定,则会默认使用 GET 方法,这会将表单数据通过 URL 提交,容易导致数据泄露,而且不适用于包含大量数据的情况。 <input> 元素必须要指定 name 属性,否则无法提交数据,在服务器端,我们也需要通过这个 name 属性值来获取对应字段的数据。 提示 填写输入框标签文字的 <label> 元素不是必须的,只是为了辅助鼠标用户。当使用鼠标点击标签文字时,会自动激活对应的输入框,这对复选框来说比较有用。for 属性填入要绑定的 <input> 元素的 id 属性值。 创建新条目 创建新条目可以放到一个新的页面来实现,也可以直接在主页实现。这里我们采用后者,首先在主页模板里添加一个表单: templates/index.html:添加创建新条目表单 1 2 3 4 5 6 <p>{{ movies|length }} Titles</p> <form method="post"> Name <input type="text" name="title" autocomplete="off" required> Year <input type="text" name="year" autocomplete="off" required> <input class="btn" type="submit" name="submit" value="Add"> </form> 在这两个输入字段中,autocomplete 属性设为 off 来关闭自动完成(按下输入框不显示历史输入记录);另外还添加了 required 标志属性,如果用户没有输入内容就按下了提交按钮,浏览器会显示错误提示。 两个输入框和提交按钮相关的 CSS 定义如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 /* 覆盖某些浏览器对 input 元素定义的字体 */ input[type=submit] { font-family: inherit; } input[type=text] { border: 1px solid #ddd; } input[name=year] { width: 50px; } .btn { font-size: 12px; padding: 3px 5px; text-decoration: none; cursor: pointer; background-color: white; color: black; border: 1px solid #555555; border-radius: 5px; } .btn:hover { text-decoration: none; background-color: black; color: white; border: 1px solid black; } 接下来,我们需要考虑如何获取提交的表单数据。 处理表单数据 默认情况下,当表单中的提交按钮被按下,浏览器会创建一个新的请求,默认发往当前 URL(在 <form> 元素使用 action 属性可以自定义目标 URL)。 因为我们在模板里为表单定义了 POST 方法,当你输入数据,按下提交按钮,一个携带输入信息的 POST 请求会发往根地址。接着,你会看到一个 405 Method Not Allowed 错误提示。这是因为处理根地址请求的 index 视图默认只接受 GET 请求。 提示 在 HTTP 中,GET 和 POST 是两种最常见的请求方法,其中 GET 请求用来获取资源,而 POST 则用来创建 / 更新

第 8 章:用户认证

第 8 章:用户认证 目前为止,虽然程序的功能大部分已经实现,但还缺少一个非常重要的部分——用户认证保护。页面上的编辑和删除按钮是公开的,所有人都可以看到。假如我们现在把程序部署到网络上,那么任何人都可以执行编辑和删除条目的操作,这显然是不合理的。 这一章我们会为程序添加用户认证功能,这会把用户分成两类,一类是管理员,通过用户名和密码登入程序,可以执行数据相关的操作;另一个是访客,只能浏览页面。在此之前,我们先来看看密码应该如何安全的存储到数据库中。 安全存储密码 把密码明文存储在数据库中是极其危险的,假如攻击者窃取了你的数据库,那么用户的账号和密码就会被直接泄露。更保险的方式是对每个密码进行计算生成独一无二的密码散列值,这样即使攻击者拿到了散列值,也几乎无法逆向获取到密码。 Flask 的依赖 Werkzeug 内置了用于生成和验证密码散列值的函数,werkzeug.security.generate_password_hash() 用来为给定的密码生成密码散列值,而 werkzeug.security.check_password_hash() 则用来检查给定的散列值和密码是否对应。使用示例如下所示: 1 2 3 4 5 6 7 8 >>> from werkzeug.security import generate_password_hash, check_password_hash >>> pw_hash = generate_password_hash('dog') # 为密码 dog 生成密码散列值 >>> pw_hash # 查看密码散列值 'pbkdf2:sha256:50000$mm9UPTRI$ee68ebc71434a4405a28d34ae3f170757fb424663dc0ca15198cb881edc0978f' >>> check_password_hash(pw_hash, 'dog') # 检查散列值是否对应密码 dog True >>> check_password_hash(pw_hash, 'cat') # 检查散列值是否对应密码 cat False 我们在存储用户信息的 User 模型类添加 username 字段和 password_hash 字段,分别用来存储登录所需的用户名和密码散列值,同时添加两个方法来实现设置密码和验证密码的功能: 1 2 3 4 5 6 7 8 9 10 11 12 13 from werkzeug.security import generate_password_hash, check_password_hash class User(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(20)) username = db.Column(db.String(20)) # 用户名 password_hash = db.Column(db.String(128)) # 密码散列值 def set_password(self, password): # 用来设置密码的方法,接受密码作为参数 self.password_hash = generate_password_hash(password) # 将生成的密码保持到对应字段 def validate_password(self, password): # 用于验证密码的方法,接受密码作为参数 return check_password_hash(self.password_hash, password) # 返回布尔值 因为模型(表结构)发生变化,我们需要重新生成数据库(这会清空数据): 1 (env) $ flask initdb --drop 生成管理员账户 因为程序只允许一个人使用,没有必要编写一个注册页面。我们可以编写一个命令来创建管理员账户,下面是实现这个功能的 admin() 函数: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import click @app.cli.command() @click.option('--username', prompt=True, help='The username used to login.') @click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True, help='The password used to login.') def admin(username, password): """Create user.""" db.create_all() user = User.query.first() if user is not None: click.echo('Updating user...') user.username = username user.set_password(password) # 设置密码 else: click.echo('Creating user...') user = User(username=username, name='Admin') user.set_password(password) # 设置密码 db.session.add(user) db.session.commit() # 提交数据库会话 click.echo('Done.') 使用 click.option() 装饰器设置的两个选项分别用来接受输入用户名和密码。执行 flask admin 命令,输入用户名和密码后,即可创建管理员账户。如果执行这个命令时账户已存在,则更新相关信息: 1 2 3 4 5 6 (env) $ flask admin Username: greyli Password: 123 # hide_input=True 会让密码输

第 9 章:测试

第 9 章:测试 在此之前,每次为程序添加了新功能,我们都要手动在浏览器里访问程序进行测试。除了测试新添加的功能,你还要确保旧的功能依然正常工作。在功能复杂的大型程序里,如果每次修改代码或添加新功能后手动测试所有功能,那会产生很大的工作量。另一方面,手动测试并不可靠,重复进行测试操作也很枯燥。 基于这些原因,为程序编写自动化测试就变得非常重要。 注意 为了便于介绍,本书统一在这里介绍关于测试的内容。在实际的项目开发中,你应该在开发每一个功能后立刻编写相应的测试,确保测试通过后再开发下一个功能。 单元测试 单元测试指对程序中的函数等独立单元编写的测试,它是自动化测试最主要的形式。这一章我们将会使用 Python 标准库中的测试框架 unittest 来编写单元测试,首先通过一个简单的例子来了解一些基本概念。假设我们编写了下面这个函数: 1 2 3 4 def sayhello(to=None): if to: return 'Hello, %s!' % to return 'Hello!' 下面是我们为这个函数编写的单元测试: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import unittest from module_foo import sayhello class SayHelloTestCase(unittest.TestCase): # 测试用例 def setUp(self): # 测试固件 pass def tearDown(self): # 测试固件 pass def test_sayhello(self): # 第 1 个测试 rv = sayhello() self.assertEqual(rv, 'Hello!') def test_sayhello_to_somebody(self): # 第 2 个测试 rv = sayhello(to='Grey') self.assertEqual(rv, 'Hello, Grey!') if __name__ == '__main__': unittest.main() 测试用例继承 unittest.TestCase 类,在这个类中创建的以 test_ 开头的方法将会被视为测试方法。 内容为空的两个方法很特殊,它们是测试固件,用来执行一些特殊操作。比如 setUp() 方法会在每个测试方法执行前被调用,而 tearDown() 方法则会在每一个测试方法执行后被调用(注意这两个方法名称的大小写)。 如果把执行测试方法比作战斗,那么准备弹药、规划战术的工作就要在 setUp() 方法里完成,而打扫战场则要在 tearDown() 方法里完成。 每一个测试方法(名称以 test_ 开头的方法)对应一个要测试的函数 / 功能 / 使用场景。在上面我们创建了两个测试方法,test_sayhello() 方法测试 sayhello() 函数,test_sayhello_to_somebody() 方法测试传入参数时的 sayhello() 函数。 在测试方法里,我们使用断言方法来判断程序功能是否正常。以第一个测试方法为例,我们先把 sayhello() 函数调用的返回值保存为 rv 变量(return value),然后使用 self.assertEqual(rv, 'Hello!') 来判断返回值内容是否符合预期。如果断言方法出错,就表示该测试方法未通过。 下面是一些常用的断言方法: assertEqual(a, b) assertNotEqual(a, b) assertTrue(x) assertFalse(x) assertIs(a, b) assertIsNot(a, b) assertIsNone(x) assertIsNotNone(x) assertIn(a, b) assertNotIn(a, b) 这些方法的作用从方法名称上基本可以得知。 假设我们把上面的测试代码保存到 test_sayhello.py 文件中,通过执行 python test_sayhello.py 命令即可执行所有测试,并输出测试的结果、通过情况、总耗时等信息。 测试 Flask 程序 回到我们的程序,我们在项目根目录创建一

【Python】使用Beautiful Soup等三种方式定制Jmeter测试脚本

背景介绍 我们在做性能调优时,时常需要根据实际压测的情况,调整线程组的参数,比如循环次数,线程数,所有线程启动的时间等。 如果是在一台Linux机器上,就免不了在本机打开图形页面修改,然后最后传递到压测机上面的过程,所有为了解决这个业务痛点 ,使用Python写了一个能直接修改Jmeter基础压测参数的脚本,能修改jmx脚本的线程组数、循环次数、线程组全部启动需要花的时间。 实现思路 刚开始准备写这个脚本的时候,想了两个思路: 把脚本数据读出,使用正则表达式(re库)匹配关键数据进行修改 优点:可以快速的改写数据 缺点:无法进行区块的修改 把脚本数据读出,使用BeautifulSoup的xml解析功能解析后修改 注:我们的Jmx脚本其实就是一个标准格式的xml 优点: 能快速的查找元素并进行修改 缺点: 需要熟悉BeautifulSoup的用法 通过Beautiful Soup Beautiful Soup Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库.我们使用BeautifulSoup解析xml或者html的时候,能够得到一个 BeautifulSoup 的对象,我们可以通过操作这个对象来完成原始数据的结构化数据。具体的使用可以参照这份文档。 具体实现 主要使用了bs4的soup.find 和self.soup.find_all功能。结化或数据的修改如loops.string = num。 值得注意的是,find_all支持正则匹配,甚至如果没有合适过滤器,那么还可以定义一个方法,方法只接受一个元素参数。 修改后的脚本将以"T{}L{}R{}-{}_{}.jmx".format(thread_num, loop_num, ramp_time, self.src_script, self.get_time()) 的形式保存,具体封装如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 import time import os from bs4 import BeautifulSoup class OpJmx: def __init__(self, file_name): self.src_script = self._split_filename(file_name) with open(file_name, "r") as f: data = f.read() self.soup = BeautifulSoup(data, "xml") @staticmethod def _split_filename(filename): """ 新生成的文件兼容传入相对路径及文件名称 :param filename: :return: """ relative = filename.split("/") return relative[len(relative)-1].split(".jmx")[0] def _theard_num(self): """ :return: 线程数据对象 """ return self.soup.find("stringProp", {"name": {"ThreadGroup.num_threads"}}) def _ramp_time(self): """ :return: 启动所有线程时间配置对象 """ return self.soup.find("stringProp", {"name": {"ThreadGroup.ramp_time"}}) def _bean_shell(self): """ :return: bean_shell对象 """ return self.soup.find("stringProp", {"name": {"BeanShellSampler.query"}})