How to write a pytest to test for opening two separate files in a single function
How do I use pytest to check that two different files in the same function are opened correctly?
open_two_files.py
def open_two_files(input_filepath, output_filepath):
open_output = open(output_filepath, 'w')
with open(input_filepath, 'r') as open_input:
for line in open_input:
open_output.write('foo')
I used this answer to build a test
test_open_two_files.py
import pytest
from unittest.mock import patch, mock_open
from open_two_files import *
def test_open_two_files():
open_mock = mock_open()
with patch('open_two_files.open', open_mock, create=True):
open_two_files("input_filepath", "output_filepath")
open_mock.assert_called_with("input_filepath", 'r')
# pytest passes with the following line commented out
open_mock.assert_called_with("output_filepath", 'w')
The error I get is
E AssertionError: expected call not found.
E Expected: open('output_filepath', 'w')
E Actual: open('input_filepath', 'r')
If I comment out the last line, the tests pass. The testing seems to only look at the last time that a file is opened. How do I test all occurrences of a file being opened please?
Answer
The issue you're encountering is due to how the patch
context manager is replacing the built-in open
function. The mock_open
object only tracks the last open
call made within the patched context. Therefore, after the open
function is called for the input file, the mock will track it, and when you call open
for the output file, it overwrites the mock, so only the last call is checked.
To solve this and track all open
calls, you can use mock_open
and set it up to track the calls to both files separately. To do this, you can use patch
to mock the open
function and capture both calls separately.
Here’s an updated version of your test:
Updated Test: test_open_two_files.py
import pytest
from unittest.mock import patch, mock_open
from open_two_files import open_two_files
def test_open_two_files():
# Mock for both input and output files
mock_input = mock_open()
mock_output = mock_open()
with patch('builtins.open', side_effect=[mock_input, mock_output]):
open_two_files("input_filepath", "output_filepath")
# Assert the input file is opened in 'r' mode
mock_input.assert_called_once_with("input_filepath", 'r')
# Assert the output file is opened in 'w' mode
mock_output.assert_called_once_with("output_filepath", 'w')
Explanation:
mock_open()
: We create two mock objects: one for the input file (mock_input
) and one for the output file (mock_output
).side_effect
: Theside_effect
argument ofpatch
allows us to specify different mock objects for consecutive calls toopen
. In this case, the first call toopen
will usemock_input
, and the second will usemock_output
. This simulates the two different file opens in youropen_two_files
function.- Assertions:
- We assert that
mock_input
was called with the correct input filepath and'r'
mode. - We assert that
mock_output
was called with the correct output filepath and'w'
mode.
- We assert that
Why This Works:
- The
side_effect
allows us to provide multiple mock objects in sequence, so every timeopen
is called, it uses the next mock in the list. - Each mock tracks the calls to
open
, so we can check that both the input and output files were opened correctly without overwriting each other.
Alternative Approach:
If you prefer to track all calls to open
without using side_effect
, you could mock the open
function with a single mock and manually check its call arguments:
import pytest
from unittest.mock import patch, mock_open
from open_two_files import open_two_files
def test_open_two_files():
# Create a single mock
open_mock = mock_open()
with patch('builtins.open', open_mock):
open_two_files("input_filepath", "output_filepath")
# Assert that open was called with the correct arguments for both files
calls = open_mock.mock_calls
assert calls[0] == ('input_filepath', 'r')
assert calls[1] == ('output_filepath', 'w')
Here, mock_calls
contains a list of all calls made to the mocked open
function, and you can check each one individually. This approach is simpler if you're mocking a single open
function but still want to check both file paths.