Detailed explanation of page object design mode

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!

Added by namasteaz on Fri, 03 Dec 2021 03:10:08 +0200