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 . Feel free to use it! here Project structure If you take a look at the , Pytest has two main test layouts. documentation 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 directory. You can apply any logic to separate the tests, such as: test 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 “ .py” files. init 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 file and the list. README dependencies The 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 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. README README The common practice is to Markdown and create a file named 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 . use README.md 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 in your project will dramatically simplify project deployment process and integration and implementation. file CI/CD Python and its utility can create and install dependency lists with cmd or terminal commands. As the first step, the command will save (overwrite) all external dependencies to file named “requirements.txt ”: pip pip freeze pip freeze > requirements.txt Then, the command will install all external dependencies from the file named “requirements.txt ”: pip install pip install -r requirements.txt This approach works even better if you work with Python virtual environments ( ). will help you keep dependency lists clean and transparent and provide only what is really required for your project. I strongly recommend using in your work, as it can help you operate several projects smoothly as well, if needed. venv Venvs venv 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 the argparse module to your file and handle the input data. add main But first, we need to keep in mind the following things: You can use as the file or of your project. conftest.py main entry point Data should be passed to test cases with . fixtures Pytest has its own (hook) to parse input arguments. It has pretty much the same logic and as argparse. class rules For implementation, we need to call function in and parse the data with . In the example, is a built-in , not a library for HTTP requests. That being said, we need to add to : pytest_addoption conftest.py request.config.getoption request fixture 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 fixture and its data from any other fixture in : console_options 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 or . configuration file config 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: Configs 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 method read_field 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 and add three classes to meet our goals. root_folder/helpers/config.py 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 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: conftest.py 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 we choose what values are more important (console options). conftest.py @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 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. used 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 () method which lists the tuple contents in a name=value format. repr 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 we will use one fixture named that contains connection info and several classes with API methods - all in one. test server 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!