paint-brush
Pytest Tips and Tricks for Beginnersby@pietester
12,428 reads
12,428 reads

Pytest Tips and Tricks for Beginners

by Artem TregubSeptember 13th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Pytest tips and tricks for beginners. Improve your test framework with simple tips!
featured image - Pytest Tips and Tricks for Beginners
Artem Tregub HackerNoon profile picture

Pytest is a framework that helps you to write tests for various types of software. It is commonly used for testing APIs, front-end and back-end services.


It is a versatile instrument that you can use to write simple unit tests or complex functional testing. My personal experience showed that it also works great with hardware (HIL) testing.


What makes Pytest good?

  • It’s free and open source
  • It has a relatively simple Python syntax
  • It collects tests automatically: Pytest has multiple instruments to manage the test execution queue and prepare the environment for it. It allows skipping tests as well.
  • There is a Python and Pytest library list


In this article I want to share useful patterns that can make your work with Pytest easier and more efficient. For convenience, I gathered all the examples from this article in the Pytest project here. Feel free to use it!

Project structure

If you take a look at the documentation, Pytest has two main test layouts.


First is tests outside application code.

pyproject.toml

src/

    mypkg/

        __init__.py

        app.py

        view.py

tests/

    test_app.py

    test_view.py

    ...


Another one - tests as part of application code.

pyproject.toml

[src/]mypkg/

    __init__.py

    app.py

    view.py

    tests/

        __init__.py

        test_app.py

        test_view.py

        ...


I prefer and recommend you to use the “Tests outside application code” way. On top of that, I would suggest adding an extra layer (or even several layers) of test separation in the test directory. You can apply any logic to separate the tests, such as:


  • Functionality: registration, cart, payment etc.
  • Services: frontend, gateway, billing, user data, order tracking etc.
  • Devices or hardware parts: GSM modem, screen, battery, camera etc.
  • Test type: unit, smoke, regression, stability, load etc.


Choose the logic that serves best to your project, you can even mix these approaches. This will help you to not get overwhelmed by tens, hundreds or even thousands of tests.


However, you should keep in mind that Pytest has its own test collector and you need to follow the rules in order to keep your experience smooth.


Pytest uses the “test_” pattern in file names to collect tests. That is why files with tests should be named according to masks “test_*.py” or “*_test.py”, where * stands for any name of your choice.


Tests directory or folders should not be Python packages, meaning they should not contain “init.py” files.

The README file

It is considered a good manners rule among the software engineers, QA automation testers and others who may use the project to include a README file and the dependencies list.


The README can be a regular quick start guide that may contain some helpful information, namely: how to install required dependencies, where to find it, how to run tests. Do not hesitate to mention anything that could help others use your project or understand its idea and logic. The README file could be very useful even for you as the project author, if you need to pick up work on it after a long break.


The common practice is to use Markdown and create a file named README.md in the root folder of your project, but even a plain text file is better than nothing. You can find an example of such a file here.

Dependency list

Dependencies are external, non built-in libraries or code that your project uses to run. In our project, Pytest itself will serve as a dependency, because it’s not a part of the built-in Python functionality. Such file in your project will dramatically simplify project deployment process and CI/CD integration and implementation.


Python and its pip utility can create and install dependency lists with cmd or terminal commands. As the first step, the command pip freeze will save (overwrite) all external dependencies to file named “requirements.txt ”:


pip freeze > requirements.txt 

Then, the command pip install will install all external dependencies from the file named “requirements.txt ”:

pip install -r requirements.txt


This approach works even better if you work with Python virtual environments (venv). Venvs will help you keep dependency lists clean and transparent and provide only what is really required for your project. I strongly recommend using venv in your work, as it can help you operate several projects smoothly as well, if needed.

Parameters parsing from console input

Most Pytest projects have an object of testing or require test environment preparation. For this, you may need some input information, arguments or parameters. In the case of classic Python software or script implementation, it is easy to do: you need to add the argparse module to your main file and handle the input data.


But first, we need to keep in mind the following things:


  • You can use conftest.py as the main file or entry point of your project.
  • Data should be passed to test cases with fixtures.
  • Pytest has its own class (hook) to parse input arguments. It has pretty much the same logic and rules as argparse.


For implementation, we need to call pytest_addoption function in conftest.py and parse the data with request.config.getoption. In the example, request is a built-in fixture, not a library for HTTP requests. That being said, we need to add to conftest.py:

def pytest_addoption(parser):

   parser.addoption('--server_address', action="store", default='', type=str,

                    help="base_http hostname or IP address")

   parser.addoption('--server_port', action="store", default='', type=str, help="base_http port")

@pytest.fixture(scope='session')

def console_options(request):

   options = namedtuple('options', ['host', 'port'])

   host = request.config.getoption('--server_address')

   port = request.config.getoption('--server_port')

   return options(host=host, port=port)


Now we can request the console_options  fixture and its data from any other fixture in conftest.py:

@pytest.fixture(scope='session', autouse=True)

def server(console_options):

       host = console_options.host


You can notice that this code has flows and generally could be better. Don’t worry, we will improve it further in the article.

Parameters parsing from a configuration file

Quality engineers are not the lonely wolves, they work in teams. It can be very useful to share your tests with other QA engineers or developers in order to briefly check how the local code runs. However, filling in the console arguments every time can be frustrating for users. This can be solved with a configuration file or config.


Configs are common for various types of projects and in our tests we want to keep both methods of arguments/parameters passing. To address this task, I strongly recommend do do the following:


  • Create an abstraction for config field names/config paths
  • Create an abstraction for console option keys
  • Create an abstraction class for your config that will provide a kind of a read_field method


Moreover, you can use any external library to operate your configuration file. In this sample project, I will use vyper-config.


Let’s create our first helper in root_folder/helpers/config.py and add three classes to meet our goals.

from os import path

from vyper import v as config

class CfgPath:

   SERVER_ADDRESS = 'server.address'

   SERVER_PORT = 'server.port'

class Options:

   SERVER_ADDRESS = '--server_address'

   SERVER_PORT = '--server_port'

class Config:

   def __init__(self, consts=CfgPath):

       self.ROOT_DIR = path.abspath(path.join(__file__, '../../'))

       self.consts = consts

       self.init_config()

   def init_config(self):

       config_name = 'config'

       config.set_config_name(config_name)

       config.set_config_type('yaml')

       config.add_config_path(self.ROOT_DIR)

       config.read_in_config()

   @staticmethod

   def get_raw_value(key_path):

       return config.get(key_path)

This will change conftest.py as well.

@pytest.fixture(scope='session', autouse=True)

def init():

   Config()

def pytest_addoption(parser):

   parser.addoption(Options.SERVER_ADDRESS, action="store", default='', type=str,

                    help="base_http hostname or IP address")

   parser.addoption(Options.SERVER_PORT, action="store", default='', type=str, help="base_http port")

@pytest.fixture(scope='session')

def console_options(request):

   options = namedtuple('options', ['host', 'port'])

   host = request.config.getoption(Options.SERVER_ADDRESS)

   port = request.config.getoption(Options.SERVER_PORT)

   return options(host=host, port=port)


Now conftest.py does not pay any attention to the key names for terminal arguments or config fields - instead, it works with an abstraction. This allow us to:


  • Modify key/field names in a single class/file
  • Find a usage of these names using any IDE
  • Easy to add new ones
  • Use IDE autocomplete (console_options.host)
  • Build simple code to work with both console arguments and config values


In this fixture of conftest.py we choose what values are more important (console options).

@pytest.fixture(scope='session', autouse=True)

def server(console_options):

   server = namedtuple('options', ['host', 'port', 'xml', 'json', 'common'])

   if not console_options.host == '':

       host = console_options.host

   else:

       host = Config.get_raw_value(CfgPath.SERVER_ADDRESS)

   if not console_options.port == '':

       port = console_options.port

   else:

       port = Config.get_raw_value(CfgPath.SERVER_PORT)

   xml = XML(host, port)

   json = JSON(host, port)

   common = CommonMethods(host, port)

   return server(host=host, port=port, xml=xml, json=json, common=common)

Named tuples for fixtures

You might have noticed that namedtuples are used in the code examples. It is not just a coincidence - they are actually very useful for code improvement. Named Tuples can help you create transparent and compact code that is easy to maintain.


According to the official documentation,


collections.namedtuple(typename, field_names, *, rename=False, defaults=None, module=None)¶


Returns a new tuple subclass named typename. The new subclass is used to create tuple-like objects that have fields accessible by attribute lookup as well as being indexable and iterable. Instances of the subclass also have a helpful docstring (with typename and field_names) and a helpful repr() method which lists the tuple contents in a name=value format.


This means that we can pass a single fixture (instead of 3 or even 5) with multiple sub-objects to any test and have access to any of them. In this test we will use one fixture named server that contains connection info and several classes with API methods - all in one.

This pattern is ideal to cover the cases when testing objects have several ports with various APIs, or when you have a device with multiple subunits.

@pytest.mark.json

@pytest.mark.order('last')

@allure.testcase('Write data id', name='Write data id')

@allure.description('Write json data and assert id in it')

def test_write_data_id(server):

   with allure.step('reset json data to default'):

       server.json.reset_data()

   with allure.step('assert default data id'):

       assert_that(server.json.get_data_id(), 'FEFE')

   with allure.step('send test data'):

       test_data = {'status': True, 'data': {'id': 'FFFF', 'some_field': 'some_field_data'}}

       server.json.send_data(test_data)

   with allure.step('assert test data id'):

       assert_that(server.json.get_data_id(), 'FFFF')

Conclusion

In this article, we touched upon:

  • Project structure
  • Readme for your project
  • Dependencies and pip freeze functionality
  • Parameters parsing from console
  • Parameters parsing from config, config abstraction
  • Named tuples and their benefits


I hope that these simple tips help you improve your project, work process and open new automation opportunities. Treat your Pytest project as a software product (because that’s what it is!) and you will be surprised how much it can do for you and your team.


P.S. This is just the tip of the iceberg of all the Pytest possibilities for testing. In my next articles, I am going to to dive deeper into:


  • Allure and allure serve
  • Fixture dependency, execution order
  • Fixture usage, data flow between fixture and tests
  • Test scenario description and tips. Separation of test scenario and implementation
  • Hamcrest and your own assert function
  • Pytest.ini and marks, access to closest mark from conftest.py
  • Tests execution order
  • Pytest internal variables and its usage
  • Stability tests data pass
  • How to create setup.py and use external git code base in a project


Stay tuned!