Pytest cheat sheet

Snippets and notes on my adventures with Pytest. Work in progress!

Pytest cheatsheet

Testing file writes with Pytest

Consider a simple class with one or more methods for file write/access:

from pathlib import Path


class UnderTest:
    def __init__(self):
        self.BASE_DIR = Path(__file__).parent

    def save_log(self):
        with open(self.BASE_DIR / "log-file.log", "a") as logfile:
            logfile.write("a dummy line")

To test this code as it is you would probably mock open(), or patch BASE_DIR in your tests, an approach I don't like too much.

NOTE: since it's a pathlib Path object, you can also do self.BASE_DIR.write_text() for simpler use cases.

If we make instead BASE_DIR an argument for initialization, we can pass it from the outside, and use Pytest tmp_path in our test. Here's the tiny refactoring:

from pathlib import Path


class UnderTest:
    def __init__(self, basedir=None):
        self.BASE_DIR = basedir or Path(__file__).parent

    def save_log(self):
        with open(self.BASE_DIR / "log-file.log", "a") as logfile:
            logfile.write("a dummy line")

Here's the test for it:

def test_it_writes_to_log_file(tmp_path):
    under_test = UnderTest(basedir=tmp_path)
    under_test.save_log()
    file = tmp_path / "log-file.log"
    assert file.read_text() == "a dummy line"

With the tmp_path fixture we have access to a temporary path in Pytest on which we can write, read, and assert over.

On a Mac for example, unless configured otherwise, this temporary file appears in some folder like /private/var/folders/yf/zmn49pds2ngb2t1jhr2g_fk40000gn/T/pytest-of-valentino/pytest-171/test_it_writes_to_log_file0.

Providing a temporary path, with the corresponding file as an external dependency for our code makes sure that test runs don't leave artifacts behind.

More info: The tmp_path fixture.

Mocking command line arguments with monkeypatch

Suppose we add argument parsing to our class, which now becomes also a CLI tool:

import argparse
from pathlib import Path


class UnderTest:
    def __init__(self, basedir=None):
        self.BASE_DIR = basedir or Path(__file__).parent
        self._parse()

    def _parse(self):
        ap = argparse.ArgumentParser()
        ap.add_argument("-n", "--name", required=True, help="Name of the log file")
        self.args = vars(ap.parse_args())

    def save_log(self):
        with open(self.BASE_DIR / self.args["name"], "a") as logfile:
            logfile.write("a dummy line")


if __name__ == "__main__":
    x = UnderTest()
    x.save_log()

To call this tiny program (assuming it's in a folder named package, inside a file named core.py ) we can do:

python package/core.py --name logfile.log

The problem now is that if we test again, our program complains because it's expecting an argument from the command line.

To make our tests pass, we need to mock sys.args. For this we can use monkeypatch, another Pytest fixture, which is particularly useful for mocking and patching objects.

monkeypatch is available as a parameter in each test function, and once inside the function we can use monkeypatch.setattr() to patch our command line arguments:

def test_it_writes_to_log_file_from_command_line_arg_(monkeypatch, tmp_path):
    monkeypatch.setattr("sys.argv", ["pytest", "--name", "logfilename.log"])
    ## Test as usual here

To put everything in context, here's the complete test:

def test_it_writes_to_log_file_from_command_line_arg_(monkeypatch, tmp_path):
    monkeypatch.setattr("sys.argv", ["pytest", "--name", "logfilename.log"])
    under_test = UnderTest(basedir=tmp_path)
    under_test.save_log()
    file = tmp_path / "logfilename.log"
    assert file.read_text() == "a dummy line"

Note: what to use for mocking in Pytest? There are many different approaches.

Applying fixture to every test function

When we need to patch something in our code, and this "something" must be patched inside every test function, we declare a custom fixture with @pytest.fixture() and autouse:

import pytest
from package.core import UnderTest # Package under test


@pytest.fixture(autouse=True)
def mock_args(monkeypatch):
    monkeypatch.setattr("sys.argv", ["pytest", "--name", "logfilename.log"])

From now on, every test function gets the patch:

import pytest
from package.core import UnderTest


@pytest.fixture(autouse=True)
def mock_args(monkeypatch):
    monkeypatch.setattr("sys.argv", ["pytest", "--name", "logfilename.log"])


def test_it_writes_to_log_file(tmp_path):
    under_test = UnderTest(basedir=tmp_path)
    under_test.save_log()
    file = tmp_path / "logfilename.log"
    assert file.read_text() == "a dummy line"

Mocking httpx with Pytest

COMING SOON

Printing print() outputs in testing

Sometimes you want to take a quick look at a variable in your code, and you put a print() somewhere. By default Pytest suppresses this kind output. To see it during a test, run Pytest with the -s flag:

pytest -s
Valentino Gagliardi

Hi! I'm Valentino! I'm a freelance consultant with a wealth of experience in the IT industry. I spent the last years as a frontend consultant, providing advice and help, coaching and training on JavaScript, testing, and software development. Let's get in touch!