[PYTHON] A quick introduction to pytest-mock

Here is a brief introduction to pytest-mock.

Significance of mock

If you want to test a module or flask app, you will often write test cases in pytest. But what if your test includes the process of communicating with HTTP and the outside world? You can create an endpoint during testing, or you can create an if branch during testing. However, when creating an endpoint, there is a problem that the backend must be changed, and if it is, the implementation becomes complicated. That's where ** pytest-mock ** comes in.

What is pytest-mock?

A thin wrapper for unittest.mock. It is a plug-in library that mock existing libraries and functions (models of existing products), and you can avoid the actual behavior of objects mocked during pytest. For example, if your test was using his HTTP POST library, you can avoid her actual POST operation by replacing it with a mock. If you chew more, it says, "It's as if he POSTs, but he doesn't."

How to install pytest-mock

pip install pytest-mock

All you have to do now is call the argument mocker in pytest and you'll be able to use mocker.

patch

Let's start with a test that doesn't have pytest-mock.

mock_project
├── __init__.py
├── some_file.py
└── tests
    ├── __init__.py
    └── test_some_file.py

some_file.py


from random import random
def generate_random():
    return random()

tests/test_some_file.py


import pytest
from mock_project import some_file
class TestA:
    def test_01(self):
        assert some_file.generate_random() == 1

This will definitely fall. Even if you actually generate numbers randomly, the probability of becoming 1 is extremely small. Then what should we do?

Here we use a mock.

When mocking, you need to specify the object to import to.

Below, mock random in some_file with fake_random.

tests/test_some_file.py


import pytest
import some_file

def fake_random():
    return 1

class TestA:
    def test_01(self, mocker):
        mocker.patch.object(some_file,"random",fake_random)
        assert some_file.generate_random() == 1

The actual operation is as follows. (Processed)

>> pytest test_mock.py
=========== test session starts ===========
platform darwin -- Python 3.7.3                                                                                    

tests/test_some_file.py . [100%]

=========== 1 passed in 0.79s ===========

Another patch

mocker.patch.object Surprisingly, there is a way to mock using mocker.patch. mocker.patch.object inputs the module in the first argument, while mocker.patch inputs string. See the code below.

tests/test_some_file.py


import pytest
from mock_project import some_file 
import random

# SAMPLE ONE
def fake_random():
    return 1

class TestA:
    def test_01(self, mocker):
        mocker.patch.object(some_file,"random",fake_random)
        assert some_file.generate_random() == 1

    def test_02(self, mocker):
        mocker.patch("mock_project.some_file.random",fake_random)
        assert some_file.generate_random() == 1

When executed, it is as follows.

>> pytest test_mock.py
=========== test session starts ===========
platform darwin -- Python 3.7.3                                                                                    

tests/test_some_file.py .. [100%]

=========== 1 passed in 0.79s ===========

That's as expected. Both can do the same thing, so which one to use depends on the situation and it's a matter of taste, but if I choose, I'll choose mocker.patch.object whenever possible.

Function mock

It is also possible to mock a function with arguments.

some_file.py


def amplify_10(x):
    return 10 * x

def process_10(x):
    return amplify_10(x)

tests/test_some_file.py


import some_file
import random

def fake_amplify_10(x):
    return  int(x/10)

class TestB:

    def test_01(self, mocker):
        mocker.patch.object(some_file,"amplify_10",fake_amplify_10)
        assert some_file.process_10(10) == 1
        print("amplify test")

When executed, it is as follows.

>> pytest test_mock.py
=========== test session starts ===========
platform darwin -- Python 3.7.3                                                                                    

tests/test_some_file.py . [100%]

=========== 1 passed in 0.60s ===========

Mock classes and libraries

It is also possible to mock classes and modules in other files as shown below.

some_file.py


import MeCab
def parse_sent(x):
    mecab = MeCab.Tagger("-d /tmp/xxxxx")
    return mecab.parse(x).split()

tests/test_some_file.py


class TestC:

    def test_01(self, mocker):
        mock_MeCab = mocker.Mock()
        mock_MeCab_Tagger = mocker.Mock()

        def fake_parse(x):
            return "a b c"
        mock_MeCab_Tagger.parse = fake_parse
        
        mock_MeCab.Tagger = mocker.Mock(return_value=mock_MeCab_Tagger)

        mocker.patch.object(some_file,"MeCab",mock_MeCab)
        
        res = some_file.parse_sent("Hello, today")

        assert res == ["a","b","c"]

If you also execute this, it will be as follows.

>> pytest test_mock.py
=========== test session starts ===========
platform darwin -- Python 3.7.3                                                                                    

tests/test_some_file.py . [100%]

=========== 1 passed in 0.58s ===========

To explain a little about this, the return_value of mocker.Mock that appears here means what to return on call.

x=mocker.Mock(return_value="hi")
print(x("zzzzzzz")) # hi

In other words, if you look at the above classes one by one, it will be as follows.

some_file.py


# MeCab -> mock_MeCab
mecab = mock_MeCab.Tagger("-d /tmp/xxxxx")
# mock_MeCab.Tagger -> mocker.Mock(return_value=mock_MeCab_Tagger) -> mock_instance
mecab = mocker.Mock(return_value=mock_MeCab_Tagger)("-d /tmp/xxxxx") #The second argument is call
# mock_instance("xxx") -> mock_MeCab_Tagger
mecab.parse = mock_MeCab_Tagger.parse
mecab.parse = fake_parse

So some_file.py does the following:

some_file.py


def parse_sent(x):
    return fake_parse(x).split()

Summary

I introduced pytest-mock as above. This time we introduced overwriting functions and overwriting modules, but you can also record how many times it was called and what arguments it was called. If you have a chance to use it, please try it.

Recommended Posts

A quick introduction to pytest-mock
A super introduction to Linux
A quick introduction to the neural machine translation library
A light introduction to object detection
Introduction to MQTT (Introduction)
Introduction to Scrapy (1)
Introduction to Scrapy (3)
Introduction to Supervisor
Introduction to Tkinter 1: Introduction
A super introduction to Python bit operations
Introduction to PyQt
Introduction to Scrapy (2)
[Linux] Introduction to Linux
Introduction to Scrapy (4)
Introduction to discord.py (2)
Introduction to discord.py
An Introduction to Object-Oriented-Give an object a child.
[Introduction to Python3 Day 23] Chapter 12 Become a Paisonista (12.1 to 12.6)
Introduction to Lightning pytorch
Introduction to Web Scraping
Introduction to Nonparametric Bayes
Introduction to EV3 / MicroPython
Introduction to Python language
Introduction to TensorFlow-Image Recognition
Introduction to OpenCV (python)-(2)
Introduction to PyQt4 Part 1
Introduction to Dependency Injection
Introduction to Private Chainer
Introduction to machine learning
[Introduction to python] A high-speed introduction to Python for busy C ++ programmers
Introduction to Linear Algebra in Python: A = LU Decomposition
An introduction to machine learning from a simple perceptron
AOJ Introduction to Programming Topic # 1, Topic # 2, Topic # 3, Topic # 4
Introduction to electronic paper modules
Introduction to dictionary lookup algorithm
Introduction to Monte Carlo Method
[Learning memorandum] Introduction to vim
Introduction to PyTorch (1) Automatic differentiation
opencv-python Introduction to image processing
Introduction to Python Django (2) Win
Introduction to Cython Writing [Notes]
An introduction to private TensorFlow
Kubernetes Scheduler Introduction to Homebrew
A road to intermediate Python
An introduction to machine learning
[Introduction to cx_Oracle] Overview of cx_Oracle
Day 68 [Introduction to Kaggle] Random Forest was a simple one.
AOJ Introduction to Programming Topic # 7, Topic # 8
[Introduction to pytorch-lightning] First Lit ♬
Add a dictionary to MeCab
How to call a function
Upload a file to Dropbox
Send a signal to subprocess
Introduction to Anomaly Detection 1 Basics
Introduction to RDB with sqlalchemy Ⅰ
[Introduction to Systre] Fibonacci Retracement ♬
Introduction to Nonlinear Optimization (I)
Introduction to serial communication [Python]
How to hack a terminal
AOJ Introduction to Programming Topic # 5, Topic # 6
[Introduction to AWS] A memorandum of building a web server on AWS