Pytest - How to test that a function is called with specific parameters?

ยท

2 min read

Unit tests are written to test single possible units of your system. When you interact with an external system, such as reading a file from AWS S3, your tests must run without any dependency on this external system. The standard testing practice for this use case is to mock the function interacting with this external system and ensure everything else works as expected.

To illustrate this example, Let's start with a simple Python function that reads data from a CSV and writes it to another CSV.

def read_data(input_path=None, output_path=None):
    input_path = input_path or "input_data/data.csv"
    output_path = output_path or "output_data/output.csv"

    with open(input_path, "r") as fin:
        with open(output_path, "w") as fout:
            fout.write(fin.read())

The test case we are interested in is to ensure that the open function is called with the passed param and not the default. Since open is a Python built-in, we don't have to test its logic explicitly.

Let's start by mocking the builtins.open built-in with the mock_open function using the patch decorator.

import pytest
import script

from unittest.mock import patch, mock_open, call

@patch("builtins.open", new_callable=mock_open, read_data="data")
def test_file_logic(mock_open_obj):
    read_data(input_path, output_path)

Next, we have to ensure the read_data function calls the open method with the expected input and output path. assert_has_calls is a function of the mock object, which will check the trace of calls and ensure that the call with specific params is present.

import pytest
import script

from unittest.mock import patch, mock_open, call

@patch("builtins.open", new_callable=mock_open, read_data="data")
def test_file_logic(mock_open_obj):
    read_data(input_path, output_path)
    mock_open_obj.assert_has_calls(
        [call(expected_output_path, "w"), 
        call(expected_input_path, "r")], 
        any_order=True)

The any_order is important because open go through various stack trace like __enter__ and doesn't appear in the order.

ย