One of the most common uses for the various setup and teardown functions is to ensure certain class or module variables are available with a known value before each test method is run.
pytest offers a completely different way of doing this, using what are known as fixtures. Fixtures are basically named variables that are predefined in a test configuration file. This allows us to separate configuration from the execution of tests, and allows fixtures to be used across multiple classes and modules.
To use them, we add parameters to our test function. The names of the parameters are used to look up specific arguments in specially named functions. For example, if we wanted to test the StatsList class we used while demonstrating unittest, we would again want to repeatedly test a list of valid integers. But we can write our tests as follows instead of using a setup method:
import pytest
from stats import StatsList
@pytest.fixture
def valid_stats():
return StatsList([1, 2, 2, 3, 3, 4])
def test_mean(valid_stats):
assert valid_stats.mean() == 2.5
def test_median(valid_stats):
assert valid_stats.median() == 2.5
valid_stats.append(4)
assert valid_stats.median() == 3
def test_mode(valid_stats):
assert valid_stats.mode() == [2, 3]
valid_stats.remove(2)
assert valid_stats.mode() == [3]
Each of the three test methods accepts a parameter named valid_stats; this parameter is created by calling the valid_stats function, which was decorated with @pytest.fixture.
Fixtures can do a lot more than return basic variables. A request object can be passed into the fixture factory to provide extremely useful methods and attributes to modify the funcarg's behavior. The module, cls, and function attributes allow us to see exactly which test is requesting the fixture. The config attribute allows us to check command-line arguments and a great deal of other configuration data.
If we implement the fixture as a generator, we can run cleanup code after each test is run. This provides the equivalent of a teardown method, except on a per-fixture basis. We can use it to clean up files, close connections, empty lists, or reset queues. For example, the following code tests the os.mkdir functionality by creating a temporary directory fixture:
import pytest
import tempfile
import shutil
import os.path
@pytest.fixture
def temp_dir(request):
dir = tempfile.mkdtemp()
print(dir)
yield dir
shutil.rmtree(dir)
def test_osfiles(temp_dir):
os.mkdir(os.path.join(temp_dir, "a"))
os.mkdir(os.path.join(temp_dir, "b"))
dir_contents = os.listdir(temp_dir)
assert len(dir_contents) == 2
assert "a" in dir_contents
assert "b" in dir_contents
The fixture creates a new empty temporary directory for files to be created in. It yields this for use in the test, but removes that directory (using shutil.rmtree, which recursively removes a directory and anything inside it) after the test has completed. The filesystem is then left in the same state in which it started.
We can pass a scope parameter to create a fixture that lasts longer than one test. This is useful when setting up an expensive operation that can be reused by multiple tests, as long as the resource reuse doesn't break the atomic or unit nature of the tests (so that one test does not rely on, and is not impacted by, a previous one). For example, if we were to test the following echo server, we may want to run only one instance of the server in a separate process, and then have multiple tests connect to that instance:
import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(('localhost',1028)) s.listen(1) while True: client, address = s.accept() data = client.recv(1024) client.send(data) client.close()
All this code does is listen on a specific port and wait for input from a client socket. When it receives input, it sends the same value back. To test this, we can start the server in a separate process and cache the result for use in multiple tests. Here's how the test code might look:
import subprocess
import socket
import time
import pytest
@pytest.fixture(scope="session")
def echoserver():
print("loading server")
p = subprocess.Popen(["python3", "echo_server.py"])
time.sleep(1)
yield p
p.terminate()
@pytest.fixture
def clientsocket(request):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("localhost", 1028))
yield s
s.close()
def test_echo(echoserver, clientsocket):
clientsocket.send(b"abc")
assert clientsocket.recv(3) == b"abc"
def test_echo2(echoserver, clientsocket):
clientsocket.send(b"def")
assert clientsocket.recv(3) == b"def"
We've created two fixtures here. The first runs the echo server in a separate process, and yields the process object, cleaning it up when it's finished. The second instantiates a new socket object for each test, and closes the socket when the test has completed.
The first fixture is the one we're currently interested in. From the scope="session" keyword argument passed into the decorator's constructor, pytest knows that we only want this fixture to be initialized and terminated once for the duration of the unit test session.
The scope can be one of the strings class, module, package, or session. It determines just how long the argument will be cached. We set it to session in this example, so it is cached for the duration of the entire pytest run. The process will not be terminated or restarted until all tests have run. The module scope, of course, caches it only for tests in that module, and the class scope treats the object more like a normal class setup and teardown.