小码奔腾

记录一些和自动化测试、CI有关的想法


  • Home

  • Archives

重构一份应用pytest框架的测试代码

Posted on 2018-05-13

项目中有份API测试的代码的结构大致如下

/api_test
– test_device_api01_via_lan.py
– test_device_api02_via_lan.py
– test_device_api03_via_lan.py
– test_device_api01_via_wan.py
– test_device_api02_via_wan.py
– test_device_api03_via_wan.py

很容易猜到其实这里是重复的2份代码,只是因为执行测试的时候,有一份是通过lan测试,另一份是通过wan测试。每次修改代码,还需要把修改同步到相应的lan或者wan的代码上去……

但是又不能简单的做个循环,把lan/wan的地址丢进去当参数,因为项目目前运行是需要收集JUnit格式的测试报告的,优化代码后,还需要拿到和之前一样或者差不多的报告,好显示在Jenkins上。

今天实在不能忍了,花点时间研究了下,有如下解决办法。

项目根目录上新建一个conftest.py,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
def pytest_generate_tests(metafunc):
idlist = []
argvalues = []
for scenario in metafunc.cls.scenarios:
idlist.append(scenario[0])
items = scenario[1].items()
argnames = [x[0] for x in items]
argvalues.append(([x[1] for x in items]))
metafunc.parametrize(argnames, argvalues, ids=idlist, scope="class")

scenario1 = ('LAN', {'URL': 'www.baidu.com'})
scenario2 = ('WAN', {'URL': 'www.sohu.com'})

tests目录下任一个test模块,大致做如下修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pytest
from conftest import scenario1, scenario2

class TestLogin(object):
scenarios = [scenario1, scenario2]

def test_login_01(self, URL):
assert "www" in URL

def test_login_02(self, URL):
assert "ok" == "ok"

def test_login_03(self, URL):
assert "sohu" in URL

然后执行以上测试的时候,虽然代码里只写了3个测试,实际上pytest会生成以下6个测试,生成的JUnit测试报告也会有这6个测试的测试结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
rx:pytest_proj reed$ pytest --collect-only tests/test_api01.py
============= test session starts ======
platform darwin -- Python 3.6.5, pytest-3.5.1, py-1.5.3, pluggy-0.6.0
rootdir: /Users/reed/Documents/dev/pytest_proj, inifile:
collected 6 items
<Module 'tests/test_api01.py'>
<Class 'TestLogin'>
<Instance '()'>
<Function 'test_login_01[LAN]'>
<Function 'test_login_02[LAN]'>
<Function 'test_login_03[LAN]'>
<Function 'test_login_01[WAN]'>
<Function 'test_login_02[WAN]'>
<Function 'test_login_03[WAN]'>

pytest测试框架中的setup和tearDown

Posted on 2018-05-08

Part One

最近对pytest比较感兴趣,看了pytest的文档classic xunit-style setup,这里做个小结,直接看代码。

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
# content of test_websites.py

'''
Setup/teardown in pytest, see https://docs.pytest.org/en/3.5.1/xunit_setup.html

Remarks:
1. setup/teardown的结对函数在测试进程中可以被调用多次的。
2. 如果setup函数在执行时失败或被skipped了,相应的tearDown函数不会被调用。
'''

import pytest

def setup_module(module):
"""
这是一个module级别的setup,它会在本module(test_website.py)里
所有test执行之前,被调用一次。
注意,它是直接定义为一个module里的函数"""
print()
print("-------------- setup before module --------------")


def teardown_module(module):
"""
这是一个module级别的teardown,它会在本module(test_website.py)里
所有test执行完成之后,被调用一次。
注意,它是直接定义为一个module里的函数"""
print("-------------- teardown after module --------------")


class TestBaidu(object):

def test_login(self):
print("test baidu login function")
assert True == True


class TestSohu(object):

@classmethod
def setup_class(cls):
""" 这是一个class级别的setup函数,它会在这个测试类TestSohu里
所有test执行之前,被调用一次.
注意它是一个@classmethod
"""
print("------ setup before class TestSohu ------")

@classmethod
def teardown_class(cls):
""" 这是一个class级别的teardown函数,它会在这个测试
类TestSohu里所有test执行完之后,被调用一次.
注意它是一个@classmethod
"""
print("------ teardown after class TestSohu ------")

def setup_method(self, method):
""" 这是一个method级别的setup函数,它会在这个测试
类TestSohu里,每一个test执行之前,被调用一次.
"""
print("--- setup before each method ---")

def teardown_method(self, method):
""" 这是一个method级别的teardown函数,它会在这个测试
类TestSohu里,每一个test执行之后,被调用一次.
"""
print("--- teardown after each method ---")

def test_login(self):
print("sohu login")
assert True == True

def test_logout(self):
print("sohu logout")
pytest.skip()

pytest中的setup/teardown还有一个更推荐的实现方法是去使用pytest.fixture特性,上面这种经典的setup/teardown,pytest表示也会继续支持。下面准备总结下用pytest.fixture实现setup/teardown的方法。

Part Two

下面内容是阅读文档pytest fixtures: explicit, modular, scalable的一些总结,pytest fixture功能很丰富,功能远不止用来构建测试中传统的setup/teardown。

但是还是先看下用pytest.fixture特性写的setup/teardown,据stakoverflow上一哥们说,这还是目前的最佳实践。

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
import time
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from utils.log import logger
from utils.config import get_url

@pytest.fixture()
def chrome_driver(scope="function"):
"""
scope="function"是scope的默认值,表示这是一个function级别的fixture
"""
print("setup() begin")
driver = webdriver.Chrome()
driver.get(get_url())
print("setup() end")
yield driver
#这里会返回driver,给使用这个fixture为参数的test_函数使用,
#test_函数结束后,会回到这里,继续执行后面语句
print("teardown() begin")
driver.close()
print("teardown() end")


class TestBaiDu(object):

locator_kw = (By.ID, 'kw')
locator_su = (By.ID, 'su')
locator_result = (By.XPATH, '//div[contains(@class, "result")]/h3/a')

def test_search_0(self, chrome_driver):
"""
这里的chrome_driver是在本模块中定义的fixture,这里输入
的参数是上面yield driver中返回的driver
"""
chrome_driver.find_element(*self.locator_kw).send_keys(u'selenium 测试')
chrome_driver.find_element(*self.locator_su).click()
time.sleep(2)
links = chrome_driver.find_elements(*self.locator_result)
for link in links:
logger.info(link.text)

这样写看起来有点pythonic的味道,我理解写这样fixture形式的setup/teardown函数,主要还是给那些需要打开然后关闭的资源,比如上面例子中的浏览器driver,确实需要收尾(driver.quit())。

可能还有其他应用,比如写一个数据库查询的函数,就可以把连接数据库,获得数据查询句柄,yield 句柄,关闭数据库句柄,关闭数据连接写成一个fixture,这样代码应该清爽多了。

1
2
3
4
5
6
7
@pytest.fixture(scope="module") #一个module里的所有函数共用一个句柄实例
def sql_query():
#连接数据库
#获得数据库查询句柄
yield "查询句柄"
#关闭句柄
#关闭数据库连接

fixture如果不用到yield,则只是把fixture函数里返回的值,作为参数给到使用fixture的函数,代码如下

1
2
3
4
5
6
@pytest.fixture()
def fruit_name():
return "apple"

def test_fruit(fruit_name):
assert "apple" == fruit_name

不使用IDE的情况下,导入自定义Python module的最佳实践

Posted on 2018-05-04

Part One

微软出的Visual Studio Code这个代码编辑器很好用,和Sublime有点相似,但是用起来更方便一些。和PyCharm不一样,用VS code写自定义module的时候,会出现找不到module的报错,原因是VS code不会像IDE那样,帮用户把项目目录临时性加入到系统PATH中去。

今天做了些探索,目前可行的方法大致如下,但是仍然不能算是最佳实践,已经在CPyUG上提了这个问题,看看有没有什么更好的办法。

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env python3

import os
import sys

base_path = os.path.dirname(os.path.abspath(__file__)) + '\\..\\'

sys.path.append(base_path) #这里临时性的把项目目录加入到系统路径中

from utils.config_reader import ConfigReader #这样才可以导入位置在 ../utils/config_reader.py 里的 ConfigReader 类
print(ConfigReader)

Part Two

CPyUG确实是个挺好的组织,Python方面的问题一般都会有热心又专业的小伙伴帮忙回答,针对上次导入自定义Python module的问题,目前有两个可行的办法。

方法1是针对应用了pytest框架的测试项目的,例如有如下项目结构。


TEST_PROJECT
/testlogin
test_login.py
/util
init.py
global_values.py
conftest.py

项目中定义了一个包util,其中有模块global_values.py,在另一个目录testlogin中有test_login.py希望导入模块global_values.py,这里就需要在项目根目录下创建一个conftest.py文件(内容为空也是允许的),这样,当在项目根目录下执行pytest来启动测试的时候,pytest会帮你识别整个项目目录中的各种自定义模块,不会出现找不到模块的问题。

1
2
3
4
5
6
import pytest
from util import global_values #导入自定义模块

def test_login():
print(global_values.USER_NAME)
assert 5 == 5

通过这个解答,我发现pytest里对conftest.py的应用还挺有意思的,stackoverflow里这篇解答写的很棒,值得一看!

另一个方法是通过配置一个系统变量PYTHONPATH,python在查找module的时候会这个变量定义的目录里去查找,所以在不同平台下临时性定义这个系统变量,也是个解决导入自定义模块的办法。我猜想PyCharm就是用了这个方法来让用户方便导入模块的吧。

python程序中将常量放在同一个文件里并防止修改

Posted on 2018-04-19

在看《编写高质量代码-改善Python程序的91个建议》一书,是国内Python圈和CPyUG里的名人赖总和人合著的,里面建议了很多比较好,比较Pythonic的代码写法,打算边看边实践,挑选一些记录下,这算是第一篇吧。

常量也就是那些一般不会变的数据,建议的做法如下:

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
'''
Best practice 01

Put all constants in one file, and protect them from changing value.

'''
# File name: constants.py

class Const(object):
class ConstError(TypeError):
pass

class ConstCaseError(ConstError):
pass

def __setattr__(self, name, value):
if name in self.__dict__: # 判断是否已经被赋值,如果是则报错
raise self.ConstError("Can't change const.%s" % name)
if not name.isupper(): # 判断所赋值是否是全部大写,用来做第一次赋值的格式判断,也可以根据需要改成其他判断条件
raise self.ConstCaseError('const name "%s" is not all supercase' % name)

self.__dict__[name] = value

const = Const()
const.MY_CONSTANT = "CHINA"
const.MY_SECOND_CONSTANT = "RUSSIA"
1
2
3
4
#File name: test.py
from constants import const
print(const.MY_CONSTANT)
const.MY_CONSTANT = 2 #此处尝试再赋值会触发ConstError

转一篇关于Python类属性与实例属性的博文

Posted on 2018-04-17

链接见 https://segmentfault.com/a/1190000002671941

小结下我的理解,如下一个Person类

1
2
3
4
5
6
7
8
9
10
class Person(object):
school_name = "ABC school"

def __init__(self, name):
self.name = name

def print_name(self):
print('My name is ' + self.name)

jo = Person('Jo')

其中的school_name就是一个类属性,它可以通过Person.school_name和jo.school_name(在jo已经通过Person('Jo')实例化为一个Person对象后)来访问到。

实际上,在jo刚刚实例化为一个Person对象后,jo是没有school_name属性的,此时如果有print(jo.school_name)这样的操作,实际上也是去访问的Person这个类对象的school_name属性。除非主动给jo.school_name赋值,不然jo一直不会有自己的school_name属性。

在jo拥有自己的school_name属性之后,对jo.school_name的修改操作,不会去改变Person.school_name的值,因为如上所说,Person类本身也是一个拥有school_name属性的对象,它的属性school_name只能通过Person.school_name来修改。

感谢原作者!_zhao

virtualenv的使用小记

Posted on 2018-04-17

virtualenv可以用来创建一套虚拟的、独立的、干净python环境,如果系统中安装有多个python版本,则还可以指定版本。

pip install virtualenv pip安装virtualenv。
virtualenv venv --python=python3.6 创建一个名字叫venv的python环境,python版本指定为3.6,然后virtualenv会在当前目录下创建名为venv的目录。
virtualenv --no-site-packages venv 创建一个名字叫venv的,无第三方包的干净的python环境,python版本与系统中的版本一致。
source venv/bin/activate 启用venv环境。
pip install -r requirements.txt 安装文件requirements.txt里列举的第三方python模块,也即是当前的venv环境中安装。

在PyCharm中使用virtualenv作为项目解释器(Project Interpreter)时,选择Add Local -> Exisiting environment,Interpreter再定位到venv/bin/python并选中即可。

deactivate 关闭venv环境。

1234…6
Reed Xia

Reed Xia

记录一些和自动化测试、CI有关的想法

33 posts
32 tags
GitHub E-Mail
© 2018 Reed Xia
Hosted by GitHub Pages