Pytest real test framework API

https://www.jianshu.com/p/40a0b396465c?utm_campaign=hugo&utm_medium=reader_share&utm_content=note&utm_source=weixin-timeline&from=timeline&isappinstalled=0

Functional Planning

  1. Database assertion pymysql -> Package
  2. Environmental clean-up database operations -> Fixtures
  3. Concurrent execution of multiple processes in parallel pytest-xdist
  4. Composite assertion pytest-check
  5. Examples heavy 跑 pytest-rerunfailures
  6. Context switching pytest-base-url
  7. Data separation pyyaml
  8. Configuration separation pytest.ini
  9. Report generation pytest-html, allure-pytest
  10. Example level with pytest-level
  11. Restricting the timeout period pytest-timeout
  12. Send mail through custom reports and Hooks achieve Fixture

Mounting a respective package

When installed by pip -i https://pypi.doubanio.com/simple/, watercress used to specify the source, slightly faster downloads

pip install requests pymysql pyyaml pytest pyetst-xdist pytest-check pytest-rerunfailures pytest-base-url pytest-html pytest-level pytest-timeout -i https://pypi.doubanio.com/simple/

Export-dependent in the requirements.txt

pip freeze > requirments.txt

Structure Plan

Hierarchy

Hierarchical design mode: Each layer provides services for the upper

用例层(测试用例)
  |
Fixtures辅助层(全局的数据、数据库操作对象和业务流等)
  |
utils实用方法层(数据库操作, 数据文件操作,发送邮件方法等等)

Static Directory

  • data: data storage
  • reports: storage report

Directory Structure

longteng17/
  - data/
    - data.yaml: 数据文件
  - reports/: 报告目录
  - test_cases/: 用例目录
    - pytest.ini:  pytest配置
    - api_test/:  接口用例目录
      - conftest.py:  集中管理Fixtures方法
    - web_test/:  web用例目录
    - app_test/:  app用例目录
  - utils/: 辅助方法
    - data.py: 封装读取数据文件方法
    - db.py: 封装数据库操作方法
    - api.py: 封装api请求方法
    - notify.py: 封装发送邮件等通知方法
  - conftest.py: 用来放置通用的Fixtures和Hooks方法
  - pytest.ini: Pytest运行配置

Conftest.py location planning, to ensure that the directory is imported into the project with the environment variable path (sys.path) to go.
Example conftest.py and import mechanism is used:

  1. If the packet (the same level have the init .py) inside, is introduced into the uppermost package (outermost containing the init .py) of the parent directory.
  2. If the directory is not the init .py, directly into conftest.py parent directory.

Select the data file

  • Unstructured
    • txt: Branch, unstructured text data
  • Tabular
    • csv: tabular, for a lot of the same types of data
    • excel: tabular, easy data structure, larger files, parsing slower
  • Tree
    • json: can store multiple levels of data, strict format does not support Notes
    • yaml: compatible json, flexible, multi-layer data storage
    • xml: can store multiple layers, complicated file format to teach
  • Configurable
    • .ini / .properties / .conf: 1-2 can store the data for the profile

Since the embodiment of the data often requires use of multi-level data structures, where selection yaml file as a data file of the project, the following example format:

test_case1: 
    a: 1
    b: 2 

A first layer identification data pieces used in Example data used in Example names, and where agreed to use exactly the same name cases, easy to use method for automatically back Into Fixture Example allocation data.

Marking plan

Tag: mark, also referred to as labels for convenient and flexible cross cataloged selection execution cases.

  • By Type: api, web, app
  • Marked with a bug: bug
  • Flag exceptions process: negative

May according to their needs, by module, according to whether labeled with destructive like cases.

Practical Method layer utils

Data file operations: data.py

First need to install pyyaml, mounting method: pip install pyyaml
method reads file data is yaml:

  1. Open the file with open (..) as f:
  2. Loading data data = yaml.safe_load (f)

yaml.safe_load () and yaml.load () the difference:

Since the files are supported yaml arbitrary Python object
loaded from the file directly injected Python is very unsafe, safe_load () will block Python object types, only parse the dictionary load / list / string / numeric data types other levels

Examples are as follows:

import yaml

def load_yaml_data(file_path): with open(file_path, encoding='utf-8') as f: data = yaml.safe_load(f) print("加载yaml文件: {file_path} 数据为: {data}") return data 

For a simple example, there is no file does not exist, an abnormality such yaml File Format for processing. Exception handling unified into Fixture layer.

If the project is to support multiple data files, you can use classes to handle.

Database operations: db.py

As used herein, pymysql, installation method pip install pymysql

Sensitive data processing

Database Configuration separation

Database passwords and other sensitive data, directly in code or configuration file, there will be exposed to the risk of sensitive user data we can put the environment variable, and then read from the environment variable.

Note: When you deploy the project, you should remember to configure the environment variables on the server to run.

Add Variable MYSQL_PWD Windows environment variable, the value of the user's database password, it can also address database, users, and information is also configured to the environment variable.
Linux / Mac users can in /.bashrc or add /.bash_profile or / etc / profile in

export MYSQL_PWD=数据库密码

Then the corresponding source file to make it take effect, such as source ~/.bashrc.

Python is used os.getenv('MYSQL_PWD')will be able to get the value of the corresponding environment variable.

Note: If you use PyCharm, after you set the environment variable, restart PyCharm to be able to read to the new environment variable.

We use a dictionary configured to store the entire database, and passed to the dictionary database connection method unpacking.

import os
import pymysql

DB_CONF = {
    'host': '数据库地址',
    'port': 3306, 'user': 'test', 'password': os.getenv('MYSQL_PWD'), 'db': 'longtengserver', 'charset': 'utf8' } conn = pymysql.connect(**DB_CONF) 

The method of operation of the package database

Data common methods of operation have queries, execute modification statements and close the connection. A method corresponding to the plurality of kind of object, we use the class to encapsulate.
At the same time in order to avoid crosstalk and query execution statements, we use the autocommit = True when the connection is established to ensure that the implementation of each statement submitted immediately, complete code is as follows.

import os
import pymysql

DB_CONF = {
    'host': '数据库地址',
    'port': 3306, 'user': 'test', 'password': os.getenv('MYSQL_PWD'), 'db': 'longtengserver', 'charset': 'utf8' } class DB(object): def __init__(self, db_conf=DB_CONF) self.conn = pymysql.connect(**db_conf, autocommit=True) self.cur = self.conn.cursor(pymysql.cursors.DictCursor) def query(self, sql): self.cur.execute(sql) data = self.cur.fetchall() print(f'查询sql: {sql} 查询结果: {data}') return data def change_db(self, sql): result = self.cur.execute(sql) print(f'执行sql: {sql} 影响行数: {result}') def close(self): print('关闭数据库连接') self.cur.close() self.conn.close() 

Wherein if the query contains Chinese, according to the database in response to specify charset, charset utf8 value can not be written here utf8.
self.conn.cursor (pymysql.cursors.DictCursor) is used here cursors dictionary format, the returned results will contain the name of the table field response, and the results more clearly.

Since all single sql statements are automatically submitted, it does not support transactions, so when change_db, no need for transaction rollback of abnormal operation, abnormal for database operations, uniform layers Fixture simple process.

Encapsulate common database operations

# db.py
...
class FuelCardDB(DB): def del_card(self, card_number): print(f'删除加油卡: {card_number}') sql = f'DELETE FROM cardinfo WHERE cardNumber="{card_number}"' self.change_db(sql) def check_card(self, card_number): print(f'查询加油卡: {card_number}') sql = f'SELECT id FROM cardinfo WHERE cardNumber="{card_number}"' res = self.query(sql) return True if res else False def add_card(self, card_number): print(f'添加加油卡: {card_number}') sql = f'INSERT INTO cardinfo (cardNumber) VALUES ({card_number})' self.change_db(sql) 

Email notification: notify.py

Using Python to send mail

Send Mail via SMTP To send a general agreement. First to open the SMTP service on your mailbox settings, clear SMTP server address, port number already have to use secure SSL encrypted transmission and so on.
Using Python to send mail in three steps:

  1. Assembled message content MIMEText
  2. Assembly headers: From, To, and Subject
  3. Log SMTP server to send mail
  • Assembled message content MIMEText
from email.mime.text import MIMEText
import smtplib
body = 'Hi, all\n附件中是测试报告, 如有问题请指出'
body2 = '<h2>测试报告</h2><p>以下为测试报告内容<p>' # msg = MIMEText(content, 'plain', 'utf-8') msg = MIMEText(content2, 'html', 'utf-8') 

Email message assembled using MIMEText data object, plain text and html supports text formats.

  • Assembly headers: From, To, and Subject
...
msg['From'] = '[email protected]'
msg['To'] = '[email protected]'
msg['Subject'] = '接口测试报告' 

msg [ 'From'] can also declare the recipient's name in the format:

msg['From'] = '<韩志超> [email protected]'

msg [ 'To'] may be written in a plurality of recipients, using a string is written separated by commas:

msg['To'] = '[email protected],[email protected]'

Note that the header From, To is only a statement, not necessarily the actual sender and recipient, such as From A write mailbox actually transmitted, using the SMTP mail transmission B, substituting mail will form ( B represents A transmission).

  • Log SMTP server to send mail
...
smtp = smtplib.SMTP('邮箱SMTP地址')
# smtp = smtplib.SMTP_SSL('邮箱SMTP地址')
smtp.login('发件人邮箱', '密码')
smtp.sendmail('发件人邮箱', '收件人邮箱', msg.as_string()) 

Log in here and SMTP_SSL SMTP mail service provider to see what kind of support, you can also specify the port number when connecting, such as:

smtp = smtplib.SMTP_SSL('邮箱SMTP地址', 465)

Password login support according to the mailbox can be password or authorization code (such as QQ-mail using the general authorization code, does not support the use of SMTP login password).
When sendmail to send mail using the sender's mailbox and the recipient's mailbox is the practice of the sender and recipient can be inconsistent, and in the message header. But the sender's mailbox must be consistent and log in SMTP mail.
sendmail can only send messages to a recipient, when there are multiple recipients, the method can be used multiple times sendmail, for example:

receivers = ['[email protected]', '[email protected]']
for person in receivers:
   smtp.sendmail('发件人邮箱', person, msg.as_string()) 

msg.as_string () message msg is sent after the target sequence into a character string.

Send e-mail with attachments

As the body of the message will filter out most of the styles and JavaScript, so html reports directly read out into the message body often without any formatting. At this time, we can send a test report by the annex.

Mail attachments generally binary stream format (application / octet-stream), then the body text format. To mix the two formats we need to use MIMEMultipart this mixed MIME format, the general steps:

  1. Establish a MIMEMultipart message object
  2. Add MIMEText format text
  3. Add attachments MIMEText format (open the attachment, according to Base64 encoded into MIMEText format)
  4. Add a mail header information
  5. send email

Sample code is as follows:

from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import smtplib # 1. 建立一个MIMEMultipart消息对象 msg = MIMEMultipart() # 2. 添加邮件正文 body = MIMEText('hi, all\n附件中是测试报告,请查收', 'plain', 'utf-8') msg.attach(body) # 3. 添加附件 att = MIMEText(open('report.html', 'rb').read(), 'base64', 'utf-8') att['Content-Type'] = 'application/octet-stream' att["Content-Disposition"] = 'attachment; filename=report.html' msg.attach(att1) # 4. 添加邮件头信息 ... # 5. 发送邮件 ... 

To add message body and attachment MIMEText format message object using the attach method of msg.
When constructing attachment MIMEText object to use rb mode opens the file using base64-encoded, and to declare the content type of the attachment Content-Type and display arrangement Content-Dispositon, here attachment; filename=report.html, attachment on behalf of attachment icon and file name represents the filename displayed, here the icon representing the left and the right in the file name, displayed as report.html.

Adding e-mail header information and send regular mail to send mail with the same.

The method of sending mail package

Similarly, we can be sensitive information mailbox password configuration environment variable to go, where the variable name to SMTP_PWD.

import os
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart import smtplib SMTP_HOST = '邮箱SMTP地址' SMTP_USER = '发件人邮箱' SMTP_PWD = os.getenv('SMTP_PWD') def send_email(self, body, subject, receivers, file_path): msg = MIMEMultipart() msg.attach(MIMEText(body, 'html', 'utf-8')) att1 = MIMEText(open(file_path, 'rb').read(), 'base64', 'utf-8') att1['Content-Type'] = 'application/octet-stream' att1["Content-Disposition"] = f'attachment; filename={file_name}' msg.attach(att1) msg['From'] = SMTP_USER msg['To'] = ','.join(receivers) msg['Subject'] = subject smtp = smtplib.SMTP_SSL(SMTP_HOST) smtp.login(SMTP_USER, SMTP_PWD) for person in receivers: print(f'发送邮件给: {person}') smtp.sendmail(SMTP_USER, person, msg.as_string()) print('邮件发送成功') 

Also, for example simple here and no SMTP connection, log in, send a message to do exception handling, the reader can make the appropriate supplement.

The method of packaging the request: api.py

requests itself provides a good method, particularly general request method requests.request (). Here the package is simply added base_url assembly, the default timeout and printing information.

import requests

TIMEOUT = 30

class Api(object): def __init__(self, base_url=None): self.session = requests.session() self.base_url = base_url def request(self, method, url, **kwargs): url = self.base_url + url if self.base_url else url kwargs['timeout'] = kwargs.get('timeout', TIMEOUT) res = self.session.request(method, url, **kwargs) print(f"发送请求: {method} {url} {kwargs} 响应数据: {res.text}") return res def get(self, url, **kwargs): return self.request('get', url, **kwargs) def post(self, url, **kwargs): return self.request('post', url, **kwargs) 

Here, if the instantiation of Api base_url passed parameters, all of the splice will base_url url.
kwargs['timeout'] = kwargs.get('timeout', TIMEOUT)Set the default timeout is set to 30s.

Method layer Fixtures

import pytest
from utils.data import Data
from utils.db import FuelCardDB from utils.api import Api @pytest.fixture(scope='session') def data(request): basedir = request.config.rootdir try: data_file_path = os.path.join(basedir, 'data', 'api_data.yaml') data = Data().load_yaml(data_file_path) except Exception as ex: pytest.skip(str(ex)) else: return data @pytest.fixture(scope='session') def db(): try: db = FuelCardDB() except Exception as ex: pytest.skip(str(ex)) else: yield db db.close() @pytest.fixture(scope='session') def api(base_url): api = Api(base_url) return api 

Here pair of, practical method utils layer skip abnormality simple process, i.e., when a data connection or a data file in question, with all references to this embodiment Fixture automatically skipped.

In this api Fixtures we introduced base_url, it comes from the plug pytest-base-url, can be specified via command line option --base-url or configuration item base_url pytest.ini at run time.

[pytest]
...
base_url=http://....:8080

Press distribution use case use case name

Fixture cases by replacing the parameter, is injected into the use cases. Fixture modules can get process embodiment where the modules variables, objects, etc. Example data that are encapsulated in the context parameter Request Fixture method.
The original data with the method in Example Fixture return all the data in the data file, but is generally used in Example data only needs to use the current embodiment. We use a first layer and a method with the same name items according to distinguish data for each use case in a data file. Such as:

# api_data.yaml
test_add_fuel_card_normal: 
  data_source_id: bHRz
 cardNumber: hzc_00001 ... 

The following example demonstrates a method by Fixture Example of distribution data name:

# conftest.py
...
@pytest.fixture
def case_data(request, data): case_name = request.function.__name__ return data.get(case_name) 

Fixture request is a request context parameter with the method embodiment, which contains the config objects, various context information Pytest running through introspection Python method of print(request.__dict__)viewing all of the attributes in the request object.

  • request.function function to invoke an object method Fixture, if it is used in direct call Fixture, with the embodiment here is the function object, by function object name acquired function name attribute.
  • Request.module get by with cases where the module, and further configured for the corresponding dynamic module according to certain properties.
  • You can get pytest runtime operating parameters by request.config, configuration parameters and so on.

Thus, with the embodiment case_data parameters just introduced by the embodiment data.

EXAMPLE layer with

A complete use cases shall comprising the steps of:

  1. Environmental inspection or data preparation
  2. Business operations
  3. More than one assertion (including databases assertion)
  4. Environmental clean-up

Further embodiments should generally designated by numerals plus.

import pytest

@pytest.mark.level(1)
@pytest.mark.api
def test_add_fuel_card_normal(api, db, case_data): """正常添加加油卡""" url = '/gasStation/process' data_source_id, card_number = case_data.get('data_source_id'), case_data.get('card_number') # 环境检查 if db.check_card(card_number): pytest.skip(f'卡号: {card_number} 已存在') json_data = {"dataSourceId": data_source_id, "methodId": "00A", "CardInfo": {"cardNumber": card_number}} res_dict = api.post(url, json=json_data).json() # 响应断言 assert 200 == res_dict.get("code")) assert "添加卡成功" == res_dict.get("msg") assert res_dict.get('success') is True # 数据库断言 assert db.check_card(card_number) is True # 环境清理 db.del_card(card_number) 

The use of composite assertion: pytest-check

When the assert asserted, an assertion failure when a, i.e. the article with the embodiment as a failure, the latter will no longer be asserted is determined. Sometimes we all need time to check every checkpoint, the output of all items assertion failure. At this point we can use pytest-check plug-complex assertions.
Installation pip install pytest-check.
The so-called composite claim that, when a strip assertion failure still continue to check the following assertion, the final summary of all entries that fail.
pytest-check method used

import pytest_check as check
...
check.equal(200, es_dict.get("code"))
check.equal("添加卡成功",res_dict.get("msg")) check.is_true(res_dict.get('success')) check.is_true(db.check_card(card_number)) 

In addition In addition commonly used are:

  • check.is_false (): assert is False
  • check.is_none (): asserted value of None
  • check.is_not_none (): assert that the value is not None

Examples skipped and labeled with the expected failure

If the environment does not meet certain use cases is temporarily unable to run may be labeled skip, you may also be used SKIPIF () to skip determination condition. For the known Bug, unfinished feature can also be marked as xfail (expected failure).
Use as follows:

import os
import pytest

@pytest.mark.skip(reason="暂时无法运行该用例")
def test_a(): pass @pytest.mark.skipif(os.getenv("MYSQL_PWD") is None, reason="缺失环境变量MYSQL_PWD配置") def test_b(): pass @pytest.mark.xfail(reason='尚未解决的已知Bug') def test_c(): pass 

First test_b environment variables was examined, if not configured MYSQL_PWD environment variable, this use case is skipped.
test_c desired fail, and if it is considered normal by treatment with xpass abnormal state, on failure xfail as a normal state, in --strict strict mode, illustrated by the use xfail considered, xpass use cases considered failed.
Were used -r / -x flag here runtime / -X shows the cause skip, xfail, xpass description:

pytest -rsxX

-s herein may display some of the information on the command line using the print embodiments.

Further, the method may be Fixture or use cases using pytest.skip ( "skip Cause"), pytest.xfail ( "failure reason desired") according to the condition table with the expected failure and Examples skipped.

After labeling skip and xfail is a temporary isolation strategy, such as bug fixes, should be promptly removed the tag.

Operational control

Switching environment

By passing runtime --base-urlto switch environment:

pytest --base-url=http://服务地址:端口号

Failure to run with patients with severe

-Lf parameter default Pytest support use cases to re-run the last failure. But if we want to automatically After the re-run with cases of failure, you can use pytest-rerunfailures plug.
Installation pip install pytest-rerunfailures.
Using runtime

pytest --reruns 3 --reruns-delay 1

Failure to specify the delay with Example 1s auto-run, run up to 3 times the weight.

Corresponding to the known instability use cases, we can flasky mark, so that the failure to automatically re-run, the following examples.

import pytest
@pytest.mark.flaky(reruns=3, reruns_delay=1)
def test_example(): import random assert random.choice([True, False]) 

Press Run as in Example Level

Pytest-level use cases can use marker level, the installation method: pip install pyest-level
use:

@pytest.mark.level(1)
def test_basic_math(): assert 1 + 1 == 2 @pytest.mark.level(2) def test_intermediate_math(): assert 10 / 2 == 5 @pytest.mark.level(3) def test_complicated_math(): assert 10 ** 3 == 1000 

Run-time to run the selected level by --level.

pytest --level 2

Level1 level2 and only runs above use cases (the larger the number, about low priority)

Use case execution time limit

Use plug pytest-timeout limit the maximum run time of use cases.
Installation: pip install pytest-timeout
use of

pytest --timeout=30

Or configurations of the pytest.ini

...
timeout=30

Example parallel with

Use pytest-xdist can open multiple processes running use case.
Installation: pip install pytest-xdist
use

pytest -n 4

All use cases assigned to the four processes running.

Complete project configuration file

pytest.ini

[pytest]
miniversion = 5.0.0

addopts = --strict --html=report_{}.html --self-contained-html base_url = http://115.28.108.130:8080 testpaths = test_cases/ markers = api: api test case web: web test case app: app test case negative: abnormal test case email_subject = Test Report email_receivers = superhin@126.com,[email protected] email_body = Hi,all\n, Please check the attachment for the Test Report. log_cli = true log_cli_level = info log_cli_format = %(asctime)s %(levelname)s %(message)s log_cli_date_format = %Y-%m-%d %H:%M:%S timeout = 10 timeout_func_only = true 


Author: HAN super
link: https: //www.jianshu.com/p/40a0b396465c
Source: Jane book
Jane book copyright reserved by the authors, are reproduced in any form, please contact the author to obtain authorization and indicate the source.
 

Guess you like

Origin www.cnblogs.com/QaStudy/p/11828982.html