On databricks, I have a notebook of code and a notebook of unit tests.
The code is "imported" into the unit test notebook using the "%run" command.
How can I make a mock object of one of the functions in the code notebook from the unit test notebook? I'd typically use the patch context manager for this.
Here is the code notebook with the function to be patched (get_name):
# Databricks notebook source
def get_name_func():
return 'name1'
Here is the unit test code:
# Databricks notebook source:
from unittest.mock import patch
import inspect
# COMMAND ----------
# MAGIC %run ./get_name
# COMMAND ----------
def local_get_name():
return 'name_local'
# COMMAND ----------
get_name_func()
# COMMAND ----------
print(inspect.getmodule(get_name_func))
print(inspect.getsourcefile(get_name_func))
# COMMAND ----------
inspect.unwrap(get_name_func)
# COMMAND ----------
with patch('get_name_func') as mock_func:
print(mock_func)
# COMMAND ----------
with patch('local_get_name') as mock_func:
print(mock_func)
Both patch attempts, for the local function and the function in the code notebook, give the same error:
TypeError: Need a valid target to patch. You supplied: 'get_name_func'
The inspect commands return:
<module '__main__' from '/local_disk0/tmp/1625490167313-0/PythonShell.py'>
<command-6807918>
and
Out[38]: <function __main__.get_name_func()>
I've tried various combinations for the module path with no luck.
Strangely, __name__ returns '__main__'. But using the path '__main__.get_name_func' in the patch call does not work.
My belief is that if the object exists in the notebook (which it definitely does), then it must be patchable.
Any suggestions?
I had to make my own patching function:
class FunctionPatch():
'''
This class is a context manager that allows patching of functions "imported" from another notebook using %run.
The patch function must be at global scope (i.e. top level)
'''
def __init__(self, real_func_name: str, patch_func: Callable):
self._real_func_name = real_func_name
self._patch_func = patch_func
self._backup_real_func = None
def __enter__(self):
self._backup_real_func = globals()[self._real_func_name]
globals()[self._real_func_name] = self._patch_func
def __exit__(self, exc_type, exc_value, tb):
if exc_type is not None:
traceback.print_exception(exc_type, exc_value, tb)
globals()[self._real_func_name] = self._backup_real_func
Usage:
def test_function_patch_real_func():
return 'real1'
def test_function_patch():
assert test_function_patch_real_func() == 'real1'
def mock_func():
return 'mock1'
with FunctionPatch('test_function_patch_real_func', mock_func):
assert test_function_patch_real_func() == 'mock1'
assert test_function_patch_real_func() == 'real1'
Related
Hey I got a simple test where the fixure is not found. I am writting in vsc and using windows cmd to run pytest.
def test_graph_add_node(test_graph):
E fixture 'test_graph' not found
> available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
> use 'pytest --fixtures [testpath]' for help on them.
This is the error I get, here is the test code:
import pytest
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'giddeon1.settings')
import django
django.setup()
from graphs.models import Graph, Node, Tag
#pytest.fixture
def test_graph():
graph = Graph.objects.get(pk='74921f18-ed5f-4759-9f0c-699a51af4307')
return graph
def test_graph():
new_graph = Graph()
assert new_graph
def test_graph_add_node(test_graph):
assert test_graph.name == 'Test1'
im using python 3.9.2, pytest 6.2.5.
I have see some similar questions but they all handle wider or bigger problems.
You appear to be defining test_graph twice, which means that the second definition will overwrite the first. And you added #pytest.fixture to a test_ method when you used it, but #pytest.fixture should be added to non test methods so that tests can use that fixture. Here's how the code should probably look:
import pytest
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'giddeon1.settings')
import django
django.setup()
from graphs.models import Graph, Node, Tag
#pytest.fixture
def graph():
graph = Graph.objects.get(pk='74921f18-ed5f-4759-9f0c-699a51af4307')
return graph
def test_graph():
new_graph = Graph()
assert new_graph
def test_graph_add_node(graph):
assert graph.name == 'Test1'
Above, the first method has been renamed to graph so that the next method doesn't override it (and now #pytest.fixture is applied to a non-test method). Then, the 3rd method uses the graph fixture. Make any other changes as needed.
I am building a test suite for a web app. I am using fixtures as below:
import pytest
from selenium import webdriver
from common import Common
#pytest.fixture(scope='module')
def driver(module, headless=True):
opts = webdriver.FirefoxOptions()
opts.add_argument('--headless') if headless else None
driver = webdriver.Firefox(options=opts)
driver.get('http://localhost:8080/request')
yield driver
driver.quit()
def test_title(driver):
assert driver.title == 'abc'
if __name__ == '__main__':
test_title() #what I need to execute to see if everything is fine
Suppose I need to see if my test_title function is doing what it needs to by running this module directly inside a if __name__ == '__main__':. How can I call test_title() with driver passed in as an argument?
calling test_title like below:
if __name__ == '__main__':
test_title(driver(None, False))
python produces an error mentioned below:
(virtual) sflash#debian:~/Documents/php/ufj/ufj-test$ ./test_r*
Traceback (most recent call last):
File "./test_request.py", line 30, in <module>
test_empty_all(driver(None, headless=True))
File "/home/sflash/Documents/php/ufj/ufj-test/virtual/lib/python3.7/site-packages/_pytest/fixtures.py", line 1176, in result
fail(message, pytrace=False)
File "/home/sflash/Documents/php/ufj/ufj-test/virtual/lib/python3.7/site-packages/_pytest/outcomes.py", line 153, in fail
raise Failed(msg=msg, pytrace=pytrace)
Failed: Fixture "driver" called directly. Fixtures are not meant to be called directly,
but are created automatically when test functions request them as parameters.
See https://docs.pytest.org/en/stable/fixture.html for more information about fixtures, and
https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly about how to update your code.
As has been discussed in the comments, fixtures cannot be called directly - they only work together with pytest.
To invoke the test directly from your code, you can call pytest.main() in your __main__ section, which has the same effect as calling pytest on the command line. Any command line options can be added as arguments to the call (as a list), for example:
if __name__ == '__main__':
pytest.main(['-vv', 'test.py::test_title'])
To use the driver without involving pytest (which was your intention) you have to extract the driver logic and call it separately both from the fixture and from main:
import pytest
from selenium import webdriver
from common import Common
def get_driver(headless=True):
opts = webdriver.FirefoxOptions()
opts.add_argument('--headless') if headless else None
driver = webdriver.Firefox(options=opts)
driver.get('http://localhost:8080/request')
return driver
):
#pytest.fixture(scope='module')
def driver(module):
yield get_driver()
driver.quit()
def test_title(driver):
assert driver.title == 'abc'
if __name__ == '__main__':
driver = get_driver()
test_title()
driver.quit()
Note that this only works if the test function does not rely on any pytest-specific stuff (for example auto-applied fixtures).
Note also that you cannot use a parameter in your fixture as you did in your example, as you have no way to provide the parameter. Instead you can use parametrized fixtures.
I've written some python code that needs to read a config file at /etc/myapp/config.conf . I want to write a unit test for what happens if that file isn't there, or contains bad values, the usual stuff. Lets say it looks like this...
""" myapp.py
"""
def readconf()
""" Returns string of values read from file
"""
s = ''
with open('/etc/myapp/config.conf', 'r') as f:
s = f.read()
return s
And then I have other code that parses s for its values.
Can I, through some magic Python functionality, make any calls that readconf makes to open redirect to custom locations that I set as part of my test environment?
Example would be:
main.py
def _open_file(path):
with open(path, 'r') as f:
return f.read()
def foo():
return _open_file("/sys/conf")
test.py
from unittest.mock import patch
from main import foo
def test_when_file_not_found():
with patch('main._open_file') as mopen_file:
# Setup mock to raise the error u want
mopen_file.side_effect = FileNotFoundError()
# Run actual function
result = foo()
# Assert if result is expected
assert result == "Sorry, missing file"
Instead of hard-coding the config file, you can externalize it or parameterize it. There are 2 ways to do it:
Environment variables: Use a $CONFIG environment variable that contains the location of the config file. You can run the test with an environment variable that can be set using os.environ['CONFIG'].
CLI params: Initialize the module with commandline params. For tests, you can set sys.argv and let the config property be set by that.
In order to mock just calls to open in your function, while not replacing the call with a helper function, as in Nf4r's answer, you can use a custom patch context manager:
from contextlib import contextmanager
from types import CodeType
#contextmanager
def patch_call(func, call, replacement):
fn_code = func.__code__
try:
func.__code__ = CodeType(
fn_code.co_argcount,
fn_code.co_kwonlyargcount,
fn_code.co_nlocals,
fn_code.co_stacksize,
fn_code.co_flags,
fn_code.co_code,
fn_code.co_consts,
tuple(
replacement if call == name else name
for name in fn_code.co_names
),
fn_code.co_varnames,
fn_code.co_filename,
fn_code.co_name,
fn_code.co_firstlineno,
fn_code.co_lnotab,
fn_code.co_freevars,
fn_code.co_cellvars,
)
yield
finally:
func.__code__ = fn_code
Now you can patch your function:
def patched_open(*args):
raise FileNotFoundError
with patch_call(readconf, "open", "patched_open"):
...
You can use mock to patch a module's instance of the 'open' built-in to redirect to a custom function.
""" myapp.py
"""
def readconf():
s = ''
with open('./config.conf', 'r') as f:
s = f.read()
return s
""" test_myapp.py
"""
import unittest
from unittest import mock
import myapp
def my_open(path, mode):
return open('asdf', mode)
class TestSystem(unittest.TestCase):
#mock.patch('myapp.open', my_open)
def test_config_not_found(self):
try:
result = myapp.readconf()
assert(False)
except FileNotFoundError as e:
assert(True)
if __name__ == '__main__':
unittest.main()
You could also do it with a lambda like this, if you wanted to avoid declaring another function.
#mock.patch('myapp.open', lambda path, mode: open('asdf', mode))
def test_config_not_found(self):
...
I have the following class file and a corresponding test file
dir.py:
import os
class Dir:
def __init__(self, path=''):
self.path = path
#property
def path(self):
return self._path
#path.setter
def path(self, path):
abspath = os.path.abspath(path)
if abspath.exists():
self._path = path
else:
raise IOError(f'{path} does not exist')
and dir_test.py:
import unittest
from ..dir import Dir
class TestDir(unittest.TestCase):
def IOErrorIfPathNotExists(self):
with self.assertRaises(IOError):
Dir.path = "~/invalidpath/"
with self.assertRaises(IOError):
Dir('~/invalidpath/')
if __name__ == "__main__":
unittest.main()
but when I run
pytest -x dir_test.py
it just prints no tests ran in 0.01 seconds
and I have no idea why. It is my first time using pytest except with exercises from exercism.io, and I can't spot any difference to their test files.
I am running it in a virtual environment (Python 3.6.5), with pytest and pytest-cache installed via pip.
That's because your test method is not named properly.
By default, pytest will consider any class prefixed with Test as a test collection.
Yours is TestDir, this matches.
By default, pytest will consider any function prefixed with test as a test.
Yours is IOErrorIfPathNotExists, which does not start with test and is not executed.
Source.
AIM - I am trying to pass a config variable 'db_str' to my pytest script (test_script.py)
The db_str variable is defined in development.ini
I have tried using command
pytest -c development.ini regression_tests/test_script.py
But it didn't work
Error
> conn_string = config['db_string']
KeyError: 'db_string'
I tried using conftest.py, but didn't work
#contest.py code
import pytest
def pytest_addoption(parser):
parser.addoption("--set-db_st",
action="store",help="host='localhost' dbname='xyz' user='portaladmin'")
#pytest.fixture
def db_str(request):
return request.config.getoption("--set-db_str")
Pytest code
from S4M_pyramid.modelimport MyModel
from S4M_pyramid.lib.deprecated_pylons_globals import config
import subprocess
config['db_str'] = db_str
def test_get_dataset_mapping_id():
result = MyModel.get_dataset_mapping_id()
assert len(result) >1
How can I pass variable 'db_str' from development.ini or any other ini file to pytest script
The logic is the fillowing:
Define CLI argument that will be used to pass information about environment/config file
Get CLI argument value in pytest fixture
Parse config file
Use parsed config in get_database_string fixture to get database connection string
Use get_database_string fixture in your tests to get connection string
conftest.py
import os
from configparser import ConfigParser
# in root of the project there is file project_paths.py
# with the following code ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
import project_paths
def pytest_addoption(parser):
"""Pytest hook that defines list of CLI arguments with descriptions and default values
:param parser: pytest specific argument
:return: void
"""
parser.addoption('--env', action='store', default='development',
help='setup environment: development')
#pytest.fixture(scope="function")
def get_database_string(get_config):
"""Fixture that returns db_string
:param get_config: fixture that returns ConfigParser object that access
to config file
:type: ConfigParser
:return: Returns database connection string
:rtype: str
"""
return get_config['<section name>']['db_string']
#pytest.fixture(scope="function")
def get_config(request):
"""Functions that reads and return ConfigParser object that access
to config file
:rtype: ConfigParser
"""
environment = request.config.getoption("--env")
config_parser = ConfigParser()
file_path = os.path.join(project_paths.ROOT_DIR, '{}.ini'.format(environment))
config_parser.read(file_path)
return config_parser
test_file.py
import pytest
def test_function(get_database_string)
print(get_database_string)
>>> <data base string from development.ini>
As described on pytest_addoption:
add options:
To add command line options, call parser.addoption(...).
To add ini-file values call parser.addini(...).
get options:
Options can later be accessed through the config object, respectively:
config.getoption(name) to retrieve the value of a command line option.
config.getini(name) to retrieve a value read from an ini-style file.
conftest.py:
def pytest_addoption(parser):
parser.addini('foo', '')
test.py:
def test_func(request):
request.config.getini('foo')