Pytest cheat sheet
Snippets and notes on my adventures with Pytest. Work in progress!
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