Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I mock pathlib.Path.open and pathlib.path.unlink with the same syntax?

I use pathlib.Path.open() and pathlib.Path.unlink() in my productive code. The unittest for that work. But I use two different ways to patch(). One with @patch decorator and one with a context manager with mock.patch().

I would like to have @patch only like this.

class MyTest(unittest.TestCase):
    @mock.patch('pathlib.Path.unlink')
    @mock.patch('pathlib.Path.open')
    def test_foobar(self, mock_open, mock_unlink):

But the real code currenty looks like this

import unittest
from unittest import mock
import pathlib

class MyTest(unittest.TestCase):
    @mock.patch('pathlib.Path.unlink')
    def test_foobar(self, mock_unlink):
        # simulated CSV file
        opener = mock.mock_open(read_data='A;B\n1;2')

        with mock.patch('pathlib.Path.open', opener):
            result = validate_csv(file_path=pathlib.Path('foo.csv'))
            self.assertTrue(result)

Technical my problem here is that I do not know how to add my CSV content to mock_open when using the @patch decorator.

It could look like this:

class MyTest(unittest.TestCase):
    @mock.patch('pathlip.Path.open')
    @mock.patch('pathlib.Path.unlink')
    def test_foobar(self, mymock_unlink, mymock_open):
        # simulated CSV file
        opener = mock.mock_open(read_data='A;B\n1;2')

        # QUESTION: How do I bring 'opener' and 'mymock_open'
        # together now?

        result = validate_csv(file_path=pathlib.Path('foo.csv'))
        self.assertTrue(result)

But the goal of my question is to improve readability and maintainability of the code. Using two decorators would reduce the indention. Choosing one way (decorators or context managers) would IMHO be easier to read.

like image 671
buhtz Avatar asked Feb 15 '26 04:02

buhtz


1 Answers

For learning purposes

Q: How do I bring 'opener' and 'mymock_open' together now?

A: Assign side_effect and return_value of mymock_open to those of opener.

@mock.patch('pathlib.Path.open')
@mock.patch('pathlib.Path.unlink')
def test_foobar(self, mymock_unlink, mymock_open):
    # simulated CSV file
    opener = mock.mock_open(read_data='A;B\n1;2')

    # QUESTION: How do I bring 'opener' and 'mymock_open'
    # together now?
    mymock_open.side_effect = opener.side_effect    # +
    mymock_open.return_value = opener.return_value  # +

    result = validate_csv(file_path=pathlib.Path('foo.csv'))
    opener.assert_not_called()          # +
    mymock_open.assert_called_once()    # +
    mymock_unlink.assert_called_once()  # +
    self.assertTrue(result)

But this is hardly a readability improvement.

Both using decorators

@mock.patch('pathlib.Path.open', new_callable=lambda: mock.mock_open(read_data='A;B\n1;2'))  # +
@mock.patch('pathlib.Path.unlink')
def test_foobar(self, mock_unlink, mock_open):
    result = validate_csv(file_path=pathlib.Path('foo.csv'))
    mock_open.assert_called_once()    # +
    mock_unlink.assert_called_once()  # +
    self.assertTrue(result)

Passing just mock.mock_open(read_data='A;B\n1;2') (as positional argument new) instead of new_callable=lambda: ... works too, but then @mock.patch won't pass mock_open to test_foobar.

Both using context managers

def test_foobar(self):
    # simulated CSV file
    opener = mock.mock_open(read_data='A;B\n1;2')

    with mock.patch('pathlib.Path.unlink') as mock_unlink,\
            mock.patch('pathlib.Path.open', opener) as mock_open:  # +
        self.assertIs(mock_open, opener)  # +
        result = validate_csv(file_path=pathlib.Path('foo.csv'))
        mock_open.assert_called_once()    # +
        mock_unlink.assert_called_once()  # +
        self.assertTrue(result)

Notice that mock_open is the same instance as opener.


Verifying the solutions

Sample implementation of validate_csv for a minimal, reproducible example:

def validate_csv(file_path):
    """
    :param pathlib.Path file_path:
    :rtype: bool
    """
    with file_path.open() as f:
        data = f.read()
    file_path.unlink()
    return data == 'A;B\n1;2'
like image 130
aaron Avatar answered Feb 16 '26 17:02

aaron



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!