Skip to content

Modus-Logo-Long-BlackCreated with Sketch.

  • Services
  • Work
  • Blog
  • Resources

    OUR RESOURCES

    Innovation Podcast

    Explore transformative innovation with industry leaders.

    Guides & Playbooks

    Implement leading digital innovation with our strategic guides.

    Practical guide to building an effective AI strategy
  • Who we are

    Our story

    Learn about our values, vision, and commitment to client success.

    Open Source

    Discover how we contribute to and benefit from the global open source ecosystem.

    Careers

    Join our dynamic team and shape the future of digital transformation.

    How we built our unique culture
  • Let's talk
  • EN
  • FR

This is part of the Python Automation Testing blog series. You can review the code from this article on the Python Automation Git repo.

A while back I started working on a new project where I had to put together an automated testing solution for Hybrid Applications developed with Ionic/Angular. I first considered ProtractorJS, as I’ve used it before and it works well with AngularJS. But the client wanted to use Appium/Python due to restrictions of the test environment, Amazon Device Farm (ADF).


NEW RESEARCH: LEARN HOW DECISION-MAKERS ARE PRIORITIZING DIGITAL INITIATIVES IN 2024.

Get Report


Using Python for Automation Testing was new to me. I had only used it in the past to write backend scripts and DevOps automation. My journey began when I took to the web, trying to find something to help me get started. I was surprised to find there were either no complete setups for this Technology Stack or I could not find them. Placed in a situation where I had to start from scratch, I began to research again. This time I started reading documentation on the tools … a lot of documentation.

I will describe everything that you need for a successful setup which can be used for local runs below. Follow these instructions and you will benefit from skipping all the bumps in the road I faced. You’re welcome 😉

For setup, you will use two tools: pytest, which is a test framework for Python (this allows us to write scalable and complex functional tests) and Pytest_bdd, which implements a subset of the Gherkin language for the automation of the project requirements testing (and easier behavioral driven development). Please note, pytest_bdd does not require a separate test runner; it works out of the box with the py.test runner.

A full tutorial on how to install all dependencies can be found here.

First, let’s talk about the folder structure. Ideally, you should place all your files into a separate folder. In my example it’s called tests_root:

Tests Root Folder for Python Automation Testing

The WebDriver configuration is kept within conf_driver.py and needs to contain the following methods:

def set_up():# This sets up the browser object
  ...
def tear_down():# This will destroy the browser object and clear all related data
  ...

Next let’s address the conftest.py file. This is the root configuration file for pytest. Here you can set Fixtures, External plugin loading, Hooks or Test root path. Your setup can also have test specific configurations. Refer to this GitHub project for examples. — more –.

I use pytest.fixtures for actions like set_up and tear_down. The purpose of test fixtures is to provide a fixed baseline upon which tests can reliably and repeatedly execute. Fixtures have explicit names and are activated by declaring their use from test functions, modules, classes or whole projects.
I also use tags which skip the test scenarios that are not automated, for example. So, by applying the tag @automated to a scenario, pytest will try to run it, otherwise it will skip it.

The next file to understand is the constants.json file. This is where project specific data and test configuration data is stored:

{
"driver": {
  "implicit_wait_time": 10,
  "timeout": 30
},
"project": {
  "suites": {
     "calculator": "Calculator"
},
  "tags":"",
  "language": "en",
  "market": "us"
}
}

Since BDD is used, don’t miss the Gherkin Feature files:

@JIRA-1
Feature: Calculator
  As a user
  I want to be able to sum numbers

  @JIRA-2 @automated
  Scenario: Add two numbers with examples
    Given I have powered calculator on
    When I enter  into the calculator
    When I enter  into the calculator
    When I press 
    Then the result should be  on the screen
    Examples:
      | number_1 | number_2 | result |
      | 10       | 20       | 30     |
      | 50       | 60       | 110    |

First we describe the feature, which in most cases is the acceptance criteria, then we have different scenarios, which are test cases. The Gherkin language uses the given, when, then steps to describe an action, which is easy to read and understand not only for the QA team, but for all stakeholders.

Next we have the i18n.json file where we define our internationalization keys:

{
  "en": {
     "ADD": "Add"
},
  "es": {
     "ADD": "Anadir"
}
}

This allows you to choose which language you want to use. And example is: en/es.

Next up is the pages folder. This is where we have the page objects, which are structured like this:

  • Base_page.py – contains the generic methods that are inherited by all pages:
def find_element(self, *locator):
  if locator.__len__() == 2:
     return self.driver.find_element(*locator)
  # This was added to make the parametrization of a locator possible.
  # Usage: self.find_element(‘Next’, *LoginPageLocators.login_button
  return self.driver.find_element(*(locator[1], locator[2] % locator[0]))

def find_elements(self, *locator):
  if locator.__len__() == 2:
     return self.driver.find_elements(*locator)
  return self.driver.find_elements(*(locator[1], locator[2] % locator[0]))

-- more --

As you can see this creates a new object class, then creates methods for different helper functions that can be used in functional tests.

Next we have the locators.py file, where the locators for all pages are defined:

from selenium.webdriver.common.by import by
class GlobalLocators(object):
  button = (By.XPATH, '//button/*[contains(., "%s")]')

button_list = {
  'disagree': 'Disagree',
  'continue': 'Continue',
  'done': 'Done'
}

class WelcomeLocators(object):
  …

You might notice the %s placeholder in the button locator. This is there because I created a single locator for all the buttons in the app. Above in the base_page.py file you can see, for example, the find_element method, which has two returns:
return self.driver.find_element(*locator) – used when no extra parameter is passed, like in the button_list example, therefore the locator will have 2 arguments
return self.driver.find_element(*(locator[1], locator[2] % locator[0])) – this return is used when locator length has 3 arguments

Below, in the welcome_page.py, are some examples on how the find_element is used using 2 and 3 arguments:

from tests.pages.base_page import BasePage
from tests.pages.locators import GlobalLocators, WelcomeLocators

class WelcomePage(BasePage):

  def get_started(self):
     self.find_element(*WelcomeLocators.get_started_button).click()
     self.find_element(GlobalLocators.button_list['continue'], 
*GlobalLocators.button).click()

In test_common.py there are common steps reusable in different places:

import pytest
from pytest_bdd import when, parsers

@when(parsers.parse('I enter {number:d} into the calculator'))
def input_number(number):
  return pytest.globalDict['number'].append(number)

@when('I enter  into the calculator')
def input_number_first(number_1):
  input_number(number_1)

And finally, we have the test_calculator.py file where the main test lives:

import pytest
from pytest_bdd import given
from pytest_bdd import scenarios

scenarios('features/calculator.feature',
     example_converters=dict(number_1=int, number_2=int, result=int))

@given('I have switched calculator on')
def power_on_calculator(driver):
  print('PLATFORM NAME')
  if pytest.globalDict['driver']:
     print(pytest.globalDict['driver'].desired_capabilities['platformName'])
  else:
     print('NONE')
  print('PLATFORM NAME')
  if driver:
     print(driver.desired_capabilities['platformName'])
  else:
     print('NONE')
  pytest.globalDict['number'] = []

Congratulations! Now you can run the automated tests by simply running Appium in a different terminal window, and run:
py.test -vv --gherkin-terminal-reporter
This will run the test suite that is specified in the constants.json file, and give you a FAILED/PASSED status for each scenario.

The full code can be accessed in the github repository here.

Next article we will talk about Pytest and Amazon Device Farm integration.

Ensure your software is secure, reliable, and ready to scale—explore our expert testing and QA services today!

Posted in Quality Assurance
Share this

Sergiu Popescu

Sergiu Popescu is a QA Engineer at Modus Create. He specializes in automating test processes for web and hybrid apps using Java, JS, and a wide range of tools and libraries like TestNG, jUnit, Webdriver, WebdriverJS, Protractor and Siesta. When he is not hunting bugs in apps, he enjoys spending time with his lovely wife and son.

Related Posts

  • Automation Testing with Pytest-BDD and Python3
    Automation Testing with Pytest-BDD and Python3

    Python3 comes upgraded with new QA features and testing tools. Learn how to setup Pytest-bdd…

  • Test Artifacts Test Rail and Pytest-bdd and Python
    Pytest-bdd, TestRail, and Test Artifacts

    This is part of the Python Automation Testing blog series. You can review the code…

Want more insights to fuel your innovation efforts?

Sign up to receive our monthly newsletter and exclusive content about digital transformation and product development.

What we do

Our services
AI and data
Product development
Design and UX
IT modernization
Platform and MLOps
Developer experience
Security

Our partners
Atlassian
AWS
GitHub
Other partners

Who we are

Our story
Careers
Open source

Our work

Our case studies

Our resources

Blog
Innovation podcast
Guides & playbooks

Connect with us

Get monthly insights on AI adoption

© 2025 Modus Create, LLC

Privacy PolicySitemap
Scroll To Top
  • Services
  • Work
  • Blog
  • Resources
    • Innovation Podcast
    • Guides & Playbooks
  • Who we are
    • Careers
  • Let’s talk
  • EN
  • FR