先行声明:
1、下面展示的内容多源于 TesterHome 各位前辈的经验总结,我只是按照我的想法进行了简单拼接(目前只适用于 Win 平台下的 Android 自动化测试)
2、主要参考了@tongshanshanshan老师的https://testerhome.com/topics/6810
3、所用语言为 Python,测试报告模板使用了 HTMLTestRunner.py,并稍事修改以方便截图、日志打印。所下载的 HTMLTestRunner.py(已在原作者之上做了修改)来源于何处不明
4、DEMO 工程已上传 GitHub“https://github.com/Hualiner/UI-auto-test-base-macaca”
5、若直接使用需自行安装 “金惠家” App,且需要在 Account.py 中添加正确的用户名和密码
当前工作内容有自动化测试的需要,先期准备开展 UI 自动化测试。通过对比,最终选择了@xdf达峰老师团队的 Macaca。受之前单位的测试总工的影响,准备这样使用 Macaca 做 UI 自动化测试,还请各位前辈看看这种形式是否合适。
工程目录如下:
1、其中 CarInsurance 为某一特定的业务线
2、Public 为所有业务线通用的东西
UI 自动化测试用例:
1、采用 PageObject 模式,将测试用例、页面定位进行分离(TestCase、PageObject)
2、采用测试数据参数化,将测试用例、测试数据分离(TestCase、TestData)
3、自动化测试用例放在 TestSuite_xxxx 下的 TestCase 里面,通过 run_all_cases.bat 执行特定业务线下所有的 TestSuite_xxxx 中的用例,通过 run_cases.bat 执行对应的 TestSuite_xxxx 中的用例
Public:
1、为了使自动化用例编写者编写自动化用例时只需关注用例逻辑、元素定位、测试数据,所以写了一个 Public
2、其中,PageObject 中包含了 BasePage(后面介绍它的作用)以及公用的页面
3、Public 里面的 py 文件则包含了 macaca server 启动、多设备、测试报告、日志打印、错误截图等功能
上面介绍了我准备开展的 UI 自动化测试的一个总的结构,下面简单介绍一下流程:
1、通过类似 run_cases.bat 的批处理作为入口进行 UI 自动化测试
if __name__ == '__main__':
cs = CaseStrategy()
cases = cs.collect_cases(suite=True)
# in future, cases_list may be used for testing strategy in multi devices
Drivers().run(cases)
2、还没考虑好多设备时用例分配策略,所以目前的实现是,同一台 PC 连接的所有设备跑一样的用例(CaseStrategy())
3、macaca server 的开启、以及 driver 的初始化是通过 Drivers() 来完成的,如下代码:
class Drivers:
@staticmethod
def _run_cases(server_url, run, cases):
log = Log()
log.set_logger(run.get_device()['udid'], run.get_path() + '\\' + 'client.log')
log.i('platformName: %s', run.get_device()['platformName'])
log.i('udid: %s', run.get_device()['udid'])
log.i('package: %s\n', run.get_device()['package'])
log.i('macaca server port: %d\n', run.get_port())
# init driver
driver = WebDriver(run.get_device(), server_url)
driver.init()
# set cls.path, it must be call before operate on any page
path = ReportPath()
path.set_path(run.get_path())
# set cls.driver, it must be call before operate on any page
base_page = BasePage()
base_page.set_driver(driver)
# skip wizard
if skip_wizard_to_home():
# run cases
run.run(cases)
# quit driver
driver.quit()
def run(self, cases):
# read all devices on this PC
devices = Devices().get_devices()
# read free ports on this PC
ports = Ports().get_ports(len(devices))
if not len(devices):
print('there is no device connected this PC')
return
runs = []
for i in range(len(devices)):
runs.append(RunCases(devices[i], ports[i]))
# start macaca server
macaca_server = MacacaServer(runs)
macaca_server.start_server()
for port in ports:
while not macaca_server.is_running(port):
print('wait macaca server all ready...')
time.sleep(1)
print('macaca server all ready')
# run on every device
pool = Pool(processes=len(runs))
for run in runs:
pool.apply_async(self._run_cases,
args=(macaca_server.server_url(run.get_port()), run, cases,))
# fix bug of macaca, android driver can not init in the same time
time.sleep(2)
pool.close()
pool.join()
4、上面的多设备是采用的@tongshanshanshan老师的https://testerhome.com/topics/6810中的代码
5、Drivers().run() 中有一个 RunCases(),它主要是设置了每个设备 UI 自动化时设备信息、macaca server 端口、存放测试报告/日志/截图的路径,以及最终通过 HTMLTestRunner 来执行用例,如下代码:
class RunCases:
def __init__(self, device, port):
self.test_report_root = '.\\TestReport'
self.device = device
self.port = port
if not os.path.exists(self.test_report_root):
os.mkdir(self.test_report_root)
date_time = time.strftime('%Y-%m-%d_%H_%M_%S', time.localtime(time.time()))
self.test_report_path = self.test_report_root + '\\' + date_time + '-%s' % self.device['udid']
if not os.path.exists(self.test_report_path):
os.mkdir(self.test_report_path)
self.file_name = self.test_report_path + '\\' + 'TestReport.html'
def get_path(self):
return self.test_report_path
def get_device(self):
return self.device
def get_port(self):
return self.port
def run(self, cases):
with open(self.file_name, 'wb') as file:
runner = HTMLTestRunner(stream=file, title='自动化测试报告', description='用例执行情况:')
runner.run(cases)
6、每个设备(进程)跑的用例是由 Driver()._run_cases() 完成的,在这里面,主要做了每个设备 UI 自动化测试的准备工作,包括每个 macaca client 的 log 配置、测试报告路径的配置、BasePage 中的 driver 配置,以及真正开始自动化测试
7、第 6 点中提到的配置之所以要在这里进行,是因为我采用了@classmethod下的 cls,这样可以解决一个进程中该类只需配置一次,后面的该类的对象可以不用再做配置,如下代码:
class Log:
@classmethod
def set_logger(cls, udid, file):
logger = logging.getLogger('MACACA')
logger.setLevel(logging.INFO)
fh = logging.FileHandler(file)
fh.setLevel(logging.INFO)
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s'
+ ' - %s' % udid
+ ' - %(levelname)s'
+ ' - %(message)s')
fh.setFormatter(formatter)
ch.setFormatter(formatter)
logger.addHandler(fh)
logger.addHandler(ch)
cls.logger = logger
class ReportPath:
@classmethod
def set_path(cls, ps):
cls.path = ps
def get_path(self):
return self.path
class BasePage(object):
@classmethod
def set_driver(cls, dri):
cls.driver = dri
def get_driver(self):
return self.driver
8、通过第 7 点中的方法,就解决了一个进程中(但设备)每个 Page 继承 BasePage 后都有一个特定的 driver,如下代码:
class MyCarInsurancePage(BasePage):
@teststep
def wait_page(self):
"""以“我的车辆”的XPATH为依据"""
try:
self.driver\
.wait_for_element_by_xpath('//android.widget.FrameLayout[1]/android.widget.LinearLayout[1]'
'/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]'
'/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]'
'/android.view.ViewGroup[1]/android.webkit.WebView[1]'
'/android.webkit.WebView[1]/android.view.View[12]')
return True
except WebDriverException:
return False
9、每条用例头带有@testcase这个装饰器,以便在 console 以及 client.log 中显示与记录以及错误后的截图等操作,如下代码:
@testcase
def test_Car_MyCarInsurEntry_Func_010(self):
"""我的车险入口验证"""
self.home_page.click_my()
login = LoginPage()
if login.wait_page():
login.input_account(VALID_ACCOUNT.account())
login.input_password(VALID_ACCOUNT.password())
login.login()
gesture = GesturePasswordPage()
if gesture.wait_page():
gesture.skip()
if self.home_page.wait_page():
self.home_page.click_my()
my_page = PlatformAppMyPage()
my_page.wait_page()
my_page.click_my_car_insurance()
my_car_insurance = MyCarInsurancePage()
self.assertTrue(my_car_insurance.wait_page())
10、每个测试步骤都带有这样的@teststep装饰器,以便在 console 以及 client.log 中显示与记录以及错误后的截图等操作,如下代码:
@teststep
def input_account(self, account):
"""以“请输入手机号码”的TEXT为依据"""
self.driver\
.element_by_name('请输入手机号码')\
.clear()\
.send_keys(account)
11、在 Decorator() 中有@testcase、@teststep这样的装饰器用例执行日志打印、错误后的处理(当前只有截图),代码如下:
def _screenshot(name):
date_time = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
screenshot = name + '-' + date_time + '.PNG'
path = ReportPath().get_path() + '\\' + screenshot
driver = BasePage().get_driver()
driver.save_screenshot(path)
return screenshot
def teststep(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
log.i('\t--> %s', func.__qualname__)
ret = func(*args, **kwargs)
return ret
except WebDriverException:
log.e('\t<-- %s, %s', func.__qualname__, 'Error')
raise WebDriverException(message=flag + _screenshot(func.__qualname__))
return wrapper
def testcase(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
log.i('--> %s', func.__qualname__)
ret = func(*args, **kwargs)
log.i('<-- %s, %s\n', func.__qualname__, 'Success')
return ret
except WebDriverException:
log.e('<-- %s, %s\n', func.__qualname__, 'Error')
raise WebDriverException
except AssertionError:
log.e('<-- %s, %s\n', func.__qualname__, 'Fail')
raise AssertionError(flag + _screenshot(func.__qualname__))
return wrapper
执行过程的日志如下图:
某个特定设备的测试报告路径下的内容如下图:
HTMLTestRunner 生成的测试报告如下图: