In the previous technical article, we have systematically introduced the knowledge system of the technology stack of UI automatic testing, but in terms of maintenance cost, we still need to consider further optimization. Then we can use the page object design pattern, and its advantages can be summarized as follows:
- Create code that can be shared across multiple test cases
- Reduce the number of duplicate codes
- If the user interface is maintained, we only need to maintain one place, so the cost of modification and maintenance is relatively high
Directory structure design
Below, we design the directory for this part. The specific directory structure is as follows:

Let me explain in detail what each directory does, which is summarized as follows:
- base package mainly writes basic code, which can be understood as the basic layer
- The page package mainly stores the code of the object layer, which can be understood as the object layer
- The test report mainly stores the code of the written test module, which can be understood as the test layer
- utils mainly stores the code of tool classes, such as the processing of JSON files and YAML files
- common mainly stores the code of public classes, such as the processing of files and directories
- Data mainly stores the data used in the test process
- Report mainly stores the test report
Page object design pattern
The advantages of the page object design pattern and the design of the directory structure have been explained in detail above. The code of each part is implemented in turn below.
Foundation layer
The following mainly implements the code of the basic layer. Create a file with the module basePage.py under the base package. The source code information in it is:
#! /usr/bin/env python # -*- coding:utf-8 -*- #author: boundless from selenium import webdriver from selenium.webdriver.support.expected_conditions import NoSuchElementException from selenium.webdriver.common.by import By import time as t class WebDriver(object): def __init__(self,driver): self.driver=driver def findElement(self,*loc): '''Positioning of individual elements''' try: return self.driver.find_element(*loc) except NoSuchElementException as e: return e.args[0] def findElements(self,*loc): '''Positioning of multiple elements''' try: return self.driver.find_elements(*loc) except NoSuchElementException as e: return e.args[0]
Object layer
Take sina's mailbox as an example to write the specific code, and create the login.py file under the page package. The source code in it is as follows:
#! /usr/bin/env python # -*- coding:utf-8 -*- #author: boundless from base.basePage import WebDriver from selenium.webdriver.common.by import By class Login(WebDriver): username=(By.ID,'freename') password=(By.ID,'freepassword') loginButton=(By.LINK_TEXT,'Sign in') divText=(By.XPATH,'/html/body/div[3]/div/div[2]/div/div/div[4]/div[1]/div[1]/div[1]/span[1]') def setUsername(self,username): '''User name input box''' self.findElement(*self.username).send_keys(username) def setPassword(self,password): '''Password input box''' self.findElement(*self.password).send_keys(password) @property def clickLogin(self): '''Click the login button''' self.findElement(*self.loginButton).click() @property def getDivText(self): '''Get error information''' return self.findElement(*self.divText).text
Test layer
Next, create test in the test layer, that is, under the test package_ sina_ The original code of the login.py module is as follows:
#! /usr/bin/env python # -*- coding:utf-8 -*- #author: boundless from page.login import Login from selenium import webdriver import unittest import time as t class TestSinaLogin(unittest.TestCase,Login): def setUp(self) -> None: self.driver=webdriver.Chrome() self.driver.maximize_window() self.driver.implicitly_wait(30) self.driver.get('https://mail.sina.com.cn/#') def tearDown(self) -> None: self.driver.quit() def test_login_null(self): '''validate logon:User name and password are null error message verification''' self.setUsername('') self.setPassword('') self.clickLogin self.assertEqual(self.getDivText,'Please enter a mailbox name') def test_login_email_format(self): '''validate logon:Malformed mailbox name validation''' self.setUsername('aertydrty') self.setPassword('erstytry') self.clickLogin self.assertEqual(self.getDivText,'The email name you entered is not in the correct format') def test_login_username_password_error(self): '''validate logon:Error message authentication for user name and password mismatch''' self.setUsername('srtyua@sina.com') self.setPassword('sertysrty') self.clickLogin self.assertEqual(self.getDivText,'Login or password error') if __name__ == '__main__': unittest.main(verbosity=2)
Keep in mind that you need to validate the specific test cases we write.
Public method
Next, create a public.py module under the common package, which mainly writes the processing of file paths. The specific source code is as follows:
#! /usr/bin/env python # -*- coding:utf-8 -*- #author: boundless import os def base_dir(): return os.path.dirname(os.path.dirname(__file__)) def filePath(directory='data',fileName=None): return os.path.join(base_dir(),directory,fileName)
Data driven
Next, create the sina.json file under the data folder and separate the data used for login into the sina.json file. The specific contents of the file are as follows:
{ "login": { "null": "Please enter a mailbox name", "format": "The email name you entered is not in the correct format", "loginError": "Login or password error" } }
Tool class
Next, write the JSON file processing in the specific tool class. The created module name is operationJson.py, and the specific source code is:
#! /usr/bin/env python # -*- coding:utf-8 -*- #author: boundless from common.public import filePath import json def readJson(): return json.load(open(filePath(fileName='sina.json'))) print(readJson()['login']['null'])
Test firmware separation
We have achieved data-driven separation. Next, we will separate the test firmware and create an init.py file under the page package to separate our test firmware. The source code information is:
#! /usr/bin/env python # -*- coding:utf-8 -*- #author: boundless from selenium import webdriver import unittest class Init(unittest.TestCase): def setUp(self) -> None: self.driver=webdriver.Chrome() self.driver.maximize_window() self.driver.implicitly_wait(30) self.driver.get('https://mail.sina.com.cn/#') def tearDown(self) -> None: self.driver.quit()
Improve the test layer
The test firmware and data have been separated to improve the code in the test module. The improved code is as follows:
#! /usr/bin/env python # -*- coding:utf-8 -*- #author: boundless from page.login import Login from selenium import webdriver from page.init import Init import unittest import time as t from utils.operationJson import readJson class TestSinaLogin(Init,Login): def test_login_null(self): '''validate logon:User name and password are null error message verification''' self.setUsername('') self.setPassword('') self.clickLogin self.assertEqual(self.getDivText,readJson()['login']['null']) def test_login_email_format(self): '''validate logon:Malformed mailbox name validation''' self.setUsername('aertydrty') self.setPassword('erstytry') self.clickLogin self.assertEqual(self.getDivText,readJson()['login']['format']) def test_login_username_password_error(self): '''validate logon:Error message authentication for user name and password mismatch''' self.setUsername('srtyua@sina.com') self.setPassword('sertysrty') self.clickLogin self.assertEqual(self.getDivText,readJson()['login']['loginError']) if __name__ == '__main__': unittest.main(verbosity=2)
Introducing waiting mechanism
Next, we introduce the wait mechanism in the basic layer code, that is, explicit wait. Remember, the improved basic layer code is as follows:
#! /usr/bin/env python # -*- coding:utf-8 -*- #author: boundless from selenium import webdriver from selenium.webdriver.support.expected_conditions import NoSuchElementException from selenium.webdriver.common.by import By from selenium.webdriver.support.wait import WebDriverWait import time as t class WebDriver(object): def __init__(self,driver): self.driver=driver def findElement(self,*loc): '''Positioning of individual elements''' try: return WebDriverWait(self.driver,20).until(lambda x:x.find_element(*loc)) except NoSuchElementException as e: return e.args[0] def findElements(self,*loc): '''Positioning of multiple elements''' try: return WebDriverWait(self.driver,20).until(lambda x:x.find_elements(*loc)) except NoSuchElementException as e: return e.args[0]
Introduction of plant design pattern
In the source code of Appium, the mobile testing framework, we can see that its element positioning class inherits the By class in Selenium. The specific source code is:
#!/usr/bin/env python # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from selenium.webdriver.common.by import By class MobileBy(By): IOS_PREDICATE = '-ios predicate string' IOS_UIAUTOMATION = '-ios uiautomation' IOS_CLASS_CHAIN = '-ios class chain' ANDROID_UIAUTOMATOR = '-android uiautomator' ANDROID_VIEWTAG = '-android viewtag' ANDROID_DATA_MATCHER = '-android datamatcher' ANDROID_VIEW_MATCHER = '-android viewmatcher' WINDOWS_UI_AUTOMATION = '-windows uiautomation' ACCESSIBILITY_ID = 'accessibility id' IMAGE = '-image' CUSTOM = '-custom'
According to such an inheritance idea, we can integrate Appium test framework and selenium 3 test framework. In this way, we can use a set of element positioning methods for both mobile and WEB platforms. In this process, we can introduce the factory design pattern in the design pattern. After introducing the factory design pattern, This improves the code of the basic layer. The improved code is:
#! /usr/bin/env python # -*- coding:utf-8 -*- #author: boundless from selenium import webdriver from selenium.webdriver.support.expected_conditions import NoSuchElementException from selenium.webdriver.common.by import By from selenium.webdriver.support.wait import WebDriverWait from appium.webdriver.common.mobileby import MobileBy import time as t class Factory(object): def __init__(self,driver): self.driver=driver def createDriver(self,driver): if driver=='web': return WEB(self.driver) elif driver=='app': return APP(self.driver) class WebDriver(object): def __init__(self,driver): self.driver=driver def findElement(self,*loc): '''Positioning of individual elements''' try: return WebDriverWait(self.driver,20).until(lambda x:x.find_element(*loc)) except NoSuchElementException as e: return e.args[0] def findElements(self,*loc): '''Positioning of multiple elements''' try: return WebDriverWait(self.driver,20).until(lambda x:x.find_elements(*loc)) except NoSuchElementException as e: return e.args[0] class WEB(WebDriver): def __str__(self): return 'web' class APP(WebDriver): def __str__(self): return 'app'
Next, we need to modify and maintain the code of the object layer, that is, inherit the WEB class instead of WebDriver. The specific modified source code is:
#! /usr/bin/env python # -*- coding:utf-8 -*- #author: boundless from base.basePage import WebDriver,WEB from selenium.webdriver.common.by import By class Login(WEB): username=(By.ID,'freename') password=(By.ID,'freepassword') loginButton=(By.LINK_TEXT,'Sign in') divText=(By.XPATH,'/html/body/div[3]/div/div[2]/div/div/div[4]/div[1]/div[1]/div[1]/span[1]') def setUsername(self,username): '''User name input box''' self.findElement(*self.username).send_keys(username) def setPassword(self,password): '''Password input box''' self.findElement(*self.password).send_keys(password) @property def clickLogin(self): '''Click the login button''' self.findElement(*self.loginButton).click() @property def getDivText(self): '''Get error information''' return self.findElement(*self.divText).text
Integrated continuous integration platform
Finally, we integrate the prepared test framework into the continuous integration platform of Ci, and generate the test report by combining the Pytest test framework and the third-party test tool Allure. The specific contents entered in Execute Sehll are as follows:
cd /Applications/code/Yun/uiSevenFrame/test python3 -m pytest -s -v test_sina_login.py --alluredir=${WORKSPACE}/report
Select Allure Report as the operation step after construction, as shown below:


After clicking build, the execution result information is as follows:
Started by user Boundless Running as SYSTEM Building in workspace /Users/liwangping/.jenkins/workspace/uiSeven [uiSeven] $ /bin/sh -xe /Applications/devOps/CICD/apache-tomcat/temp/jenkins7666607542083974346.sh + cd /Applications/code/Yun/uiSevenFrame/test + python3 -m pytest -s -v test_sina_login.py --alluredir=/Users/liwangping/.jenkins/workspace/uiSeven/report ============================= test session starts ============================== platform darwin -- Python 3.7.4, pytest-6.2.5, py-1.9.0, pluggy-0.13.1 -- /Library/Frameworks/Python.framework/Versions/3.7/bin/python3 cachedir: .pytest_cache sensitiveurl: .* metadata: {'Python': '3.7.4', 'Platform': 'Darwin-20.6.0-x86_64-i386-64bit', 'Packages': {'pytest': '6.2.5', 'py': '1.9.0', 'pluggy': '0.13.1'}, 'Plugins': {'instafail': '0.4.1.post0', 'forked': '1.0.2', 'asyncio': '0.15.1', 'variables': '1.9.0', 'emoji': '0.2.0', 'tavern': '1.12.2', 'sugar': '0.9.4', 'timeout': '1.3.3', 'xdist': '2.3.0', 'dependency': '0.5.1', 'mock': '3.6.1', 'base-url': '1.4.1', 'html': '2.1.1', 'django': '3.7.0', 'cov': '2.7.1', 'nameko': '2.13.0', 'repeat': '0.9.1', 'selenium': '2.0.1', 'trio': '0.7.0', 'Faker': '4.14.0', 'allure-pytest': '2.8.11', 'metadata': '1.8.0', 'rerunfailures': '10.0'}, 'BUILD_NUMBER': '3', 'BUILD_ID': '3', 'BUILD_URL': 'http://localhost:8080/jenkins/job/uiSeven/3/', 'NODE_NAME': 'master', 'JOB_NAME': 'uiSeven', 'BUILD_TAG': 'jenkins-uiSeven-3', 'EXECUTOR_NUMBER': '1', 'JENKINS_URL': 'http://localhost:8080/jenkins/', 'JAVA_HOME': '/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home', 'WORKSPACE': '/Users/liwangping/.jenkins/workspace/uiSeven', 'Base URL': '', 'Driver': None, 'Capabilities': {}} rootdir: /Applications/code/Yun/uiSevenFrame/test plugins: instafail-0.4.1.post0, forked-1.0.2, asyncio-0.15.1, variables-1.9.0, emoji-0.2.0, tavern-1.12.2, sugar-0.9.4, timeout-1.3.3, xdist-2.3.0, dependency-0.5.1, mock-3.6.1, base-url-1.4.1, html-2.1.1, django-3.7.0, cov-2.7.1, nameko-2.13.0, repeat-0.9.1, selenium-2.0.1, trio-0.7.0, Faker-4.14.0, allure-pytest-2.8.11, metadata-1.8.0, rerunfailures-10.0 collecting ... Please enter a mailbox name collected 3 items test_sina_login.py::TestSinaLogin::test_login_email_format PASSED test_sina_login.py::TestSinaLogin::test_login_null PASSED test_sina_login.py::TestSinaLogin::test_login_username_password_error PASSED =============================== warnings summary =============================== ../../../../../Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/eventlet/patcher.py:1 /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/eventlet/patcher.py:1: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses import imp ../../../../../Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/dns/hash.py:25 /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/dns/hash.py:25: DeprecationWarning: dns.hash module will be removed in future versions. Please use hashlib instead. DeprecationWarning) ../../../../../Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/dns/namedict.py:35 /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/dns/namedict.py:35: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working class NameDict(collections.MutableMapping): -- Docs: https://docs.pytest.org/en/stable/warnings.html ======================== 3 passed, 3 warnings in 16.89s ======================== [uiSeven] $ /Users/liwangping/.jenkins/tools/ru.yandex.qatools.allure.jenkins.tools.AllureCommandlineInstallation/Allure/bin/allure generate /Users/liwangping/.jenkins/workspace/uiSeven/report -c -o /Users/liwangping/.jenkins/workspace/uiSeven/allure-report Report successfully generated to /Users/liwangping/.jenkins/workspace/uiSeven/allure-report Allure report was successfully generated. Creating artifact for the build. Artifact was added to the build. Finished: SUCCESS
Click the icon of Allure Report to display the test report information, as shown below:



So far, a complete testing framework has been completed and can be completely applied to the actual case of the enterprise. Thank you for reading, the follow-up will continue to update!