Python / Pytest: how test a CLI command? - python

I have a simple class with Flask_restx:
from flask_restx import Namespace, Resource
from flask import current_app
ping_namespace = Namespace("ping")
class Ping(Resource):
def get(self):
return {
"status": "success",
"message": "system up and running",
"api_version": current_app.config["APP_VERSION"],
}
def pingFromCommand():
print(current_app.config["APP_VERSION"])
ping_namespace.add_resource(Ping, "")
This is the simple command in manage.py
[...]
app = create_app()
cli = FlaskGroup(create_app=create_app)
from project.batch.v2.ping.ping import Ping
#cli.command('ping')
def ping():
Ping.pingFromCommand()
If I run from command line, I get the desiderated result:
(env) $ python manage.py ping
(env) $ 2.0.0
I know how write tests for API resources with pytest, but I don't know how can I write a test for the pingFromCommand method.
I did try:
from project.batch.v2.ping.ping import Ping
def test_ping_command(test_app):
assert Ping.pingFromCommand() == "0.0.0"
But I get
> assert Ping.pingFromCommand() == "0.0.0"
E AssertionError: assert None == '0.0.0'
E + where None = <function Ping.pingFromCommand at 0x7fd41c619160>()
E + where <function Ping.pingFromCommand at 0x7fd41c619160> = Ping.pingFromCommand
Someone can help me or address me? Thank you in advance.

In the pingFromCommand function, print is used to print the current app version. That function returns None which is what you get in the AssertionError.
You can update the function to return the app version
def pingFromCommand():
return current_app.config["APP_VERSION"]
Or you might be able to use capsys fixture provided by pytest which will capture
standard output:
def test_ping_command(capsys, test_app):
Ping.pingFromCommand()
captured = capsys.readouterr() # Capture output
assert captured.out == "0.0.0" # Assert stdout

Related

How to mock a class method that is called from another class with pytest_mock

In the below files I have
InternalDogWebhookResource which calls VisitOrchestrator.fetch_visit. I am attempting to write a test for InternalDogWebhookResource but mock VisitOrchestrator.fetch_visit since it is a network call.
I have tried the mock paths:
api.dog.handlers.internal.VisitOrchestrator.fetch_visit
api.dog.handlers.internal.InternalDogWebhookResource.VisitOrchestrator.fetch_visit
api.dog.handlers.internal.InternalDogWebhookResource.fetch_visit
and many others, but I am always getting AssertionError: assert None
I can confirm that the client.post in the test works because when i remove the mock asserts, i get a response back from the api which means fetch_visit is called.
How can I find the mocker.patch path?
api/dog/handlers/internal.py
from api.dog.helpers.visits import VisitOrchestrator
#api.route("/internal/dog/webhook")
class InternalDogWebhookResource():
def post(self) -> JsonResponse:
if event_type == EventType.CHANGE:
VisitOrchestrator.fetch_visit(event['visitId'])
return JsonResponse(status=204)
api/dog/helpers/visits.py
class VisitOrchestrator:
#classmethod
def fetch_visit(cls, visit_id: str) -> VisitModel:
# do stuff
return visit
tests/v0/dog/handlers/test_webhook.py
import pytest
from pytest_mock import MockerFixture
from api.dog.handlers.internal import InternalDogWebhookResource, EventType
from tests.v0.utils import url_for
def test_webhook_valid(client, config, mocker: MockerFixture):
visit_id = '1231231'
mock_object = mocker.patch(
'api.dog.handlers.internal.VisitOrchestrator.fetch_visit',
return_value=visit_id,
)
res = client.post(
url_for(InternalDogWebhookResource),
json={'blag': 'blargh'}
)
assert mock_object.assert_called_once()
You're doing the right things - your second approach is generally the way to go with mocks (mocking api.dog.handlers.internal.InternalDogWebhookResource.VisitOrchestrator.fetch_visit)
I would try to do the minimal test code function:
def test_webhook_valid(mocker):
mock_fetch_visit = mocker.MagicMock(name='fetch_visit')
mocker.patch('api.dog.handlers.internal.VisitOrchestrator.fetch_visit',
new=mock_fetch_visit)
InternalDogWebhookResource().post()
assert 1 == mock_fetch_visit.call_count
If this works for you - maybe the problem is with the client or other settings in your test method.

Testing argument using Python Click

I have a command-line script with Python-click with an argument and option:
# console.py
import click
#click.command()
#click.version_option()
#click.argument("filepath", type=click.Path(exists=True), default=".")
#click.option(
"-m",
"--max-size",
type=int,
help="Max size in megabytes.",
default=20,
show_default=True,
)
def main(filepath: str, max_size: int) -> None:
max_size_bytes = max_size * 1024 * 1024 # convert to MB
if filepath.endswith(".pdf"):
print("success")
else:
print(max_size_bytes)
Both the argument and option have default values and work on the command-line and using the CLI it behaves as expected. But when I try testing it following Click documentation and debug it, it does not enter the first line:
# test_console.py
from unittest.mock import Mock
import click.testing
import pytest
from pytest_mock import MockFixture
from pdf_split_tool import console
#pytest.fixture
def runner() -> click.testing.CliRunner:
"""Fixture for invoking command-line interfaces."""
return click.testing.CliRunner()
#pytest.fixture
def mock_pdf_splitter_pdfsplitter(mocker: MockFixture) -> Mock:
"""Fixture for mocking pdf_splitter.PdfSplitter."""
return mocker.patch("pdf_split_tool.pdf_splitter.PdfSplitter", autospec=True)
def test_main_uses_specified_filepath(
runner: click.testing.CliRunner,
mock_pdf_splitter_pdfsplitter: Mock,
) -> None:
"""It uses the specified filepath."""
result = runner.invoke(console.main, ["test.pdf"])
assert result.exit_code == 0
I couldn't see why it is giving since the debugger did not enter the first line of function main(). Any ideas of what could be wrong?
The failure is due to following error.
(pdb)print result.output
"Usage: main [OPTIONS] [FILEPATH]\nTry 'main --help' for help.\n\nError: Invalid value for '[FILEPATH]': Path 'test.pdf' does not exist.\n"
This is happening due to following code in console.py which checks if the filepath exists.
#click.argument("filepath", type=click.Path(exists=True), default=".")
One way to test creating a temporary file is using afterburner's code:
# test_console.py
def test_main_uses_specified_filepath() -> None:
runner = click.testing.CliRunner()
with runner.isolated_filesystem():
with open('test.pdf', 'w') as f:
f.write('Hello World!')
result = runner.invoke(main, ["test.pdf"])
assert result.exit_code == 0
I've changed your test method to the following. However, this is more an augmentation to apoorva kamath's answer.
def test_main_uses_specified_filepath() -> None:
runner = click.testing.CliRunner()
with runner.isolated_filesystem():
with open('test.pdf', 'w') as f:
f.write('Hello World!')
result = runner.invoke(main, ["test.pdf"])
assert result.exit_code == 0
Simply put, it creates an isolated file system that gets cleaned up after the text is executed. So any files created there are destroyed with it.
For more information, Click's Isolated Filesystem documentation might come in handy.
Alternatively, you can remove the exists=True parameter to your file path.

Dynamically create test file templates for your entire repo

I've been looking around, but I haven't been able to find anything that does exactly what I want.
I was wondering if there's a utility out there that scans the structure and source code of your entire repo and creates a parallel test structure where one isn't there already, in which every single function and method in your code has an equivalent empty unit test.
It's pretty tedious to have to manually write a bunch of unit test boilerplate.
For example, assuming this project structure:
myproject
|--src
|--__init__.py
|--a.py
|--subpackage
|--__init__.py
|--b.py
|--c.py
It should create:
myproject
|--src
| |--__init__.py
| |--a.py
| |--subpackage
| |--__init__.py
| |--b.py
| |--c.py
|
|--tests
|--test_a.py
|--subpackage
|--test_b.py
|--test_c.py
And if the contents of a.py are:
class Printer:
def print_normal(self, text):
print(text)
def print_upper(self, text):
print(str(text).upper())
def print_lower(self, text):
print(str(text).lower())
def greet():
print("Hi!")
It the contents of test_a.py should be something similar to this:
import pytest
from myproject.src import a
def test_Printer_print_normal():
assert True
def test_Printer_print_upper():
assert True
def test_Printer_print_lower():
assert True
def test_greet():
assert True
Is anyone aware of any python project that does something like this? Even if it isn't exactly the same, anything that would save some work when initially setting up the pytest boilerplate for a giant repo with hundreds of classes and thousands of methods would be a massive time-saver.
Thanks in advance.
Searching for the tests generator tools in Python myself, I could find only those that generate unittest-style classes:
pythoscope
Installation of the latest version from Github:
$ pip2 install git+https://github.com/mkwiatkowski/pythoscope
Looks promising in theory: generates classes based on static code analysis in modules, maps the project structure to tests dir (one test module per library module), each function gets its own test class. The problem with this project is that it's pretty much abandoned: no Python 3 support, fails when encounters features backported to Python 2, thus IMO unusable nowadays. There are pull requests out there that claim to add Python 3 support, but they didn't work for me back then.
Nevertheless, here's what it would generate if your module would have Python 2 syntax:
$ pythoscope --init .
$ pythoscope spam.py
$ cat tests/test_spam.py
import unittest
class TestPrinter(unittest.TestCase):
def test_print_lower(self):
# printer = Printer()
# self.assertEqual(expected, printer.print_lower())
assert False # TODO: implement your test here
def test_print_normal(self):
# printer = Printer()
# self.assertEqual(expected, printer.print_normal())
assert False # TODO: implement your test here
def test_print_upper(self):
# printer = Printer()
# self.assertEqual(expected, printer.print_upper())
assert False # TODO: implement your test here
class TestGreet(unittest.TestCase):
def test_greet(self):
# self.assertEqual(expected, greet())
assert False # TODO: implement your test here
if __name__ == '__main__':
unittest.main()
Auger
Installation from PyPI:
$ pip install auger-python
Generates tests from runtime behavior. While it may be an option for tools with a command line interface, it requires writing an entrypoint for libraries. Even with tools, it will only generate tests for stuff that was explicitly requested; if a function is not executed, no test will be generated for it. This makes it only partially useable for tools (worst case is that you have to run the tool multiple times with all options activated to cover the completed code base) and hardly useable with libraries.
Nevertheless, this is what Auger would generate from an example entrypoint for your module:
# runner.py
import auger
import spam
with auger.magic([spam.Printer], verbose=True):
p = spam.Printer()
p.print_upper()
Executing the runner.py yields:
$ python runner.py
Auger: generated test: tests/test_spam.py
$ cat tests/test_spam.py
import spam
from spam import Printer
import unittest
class SpamTest(unittest.TestCase):
def test_print_upper(self):
self.assertEqual(
Printer.print_upper(self=<spam.Printer object at 0x7f0f1b19f208>,text='fizz'),
None
)
if __name__ == "__main__":
unittest.main()
Custom tool
For a one-time job, it shouldn't be hard to write own AST visitor that generates the test stubs from existing modules. The example script testgen.py below generates simple test stubs using the same idea as pythoscope. Usage example:
$ python -m testgen spam.py
class TestPrinter:
def test_print_normal(self):
assert False, "not implemented"
def test_print_upper(self):
assert False, "not implemented"
def test_print_lower(self):
assert False, "not implemented"
def test_greet():
assert False, "not implemented"
Contents of testgen.py:
#!/usr/bin/env python3
import argparse
import ast
import pathlib
class TestModuleGenerator(ast.NodeVisitor):
linesep = '\n'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.imports = set()
self.lines = []
self.indent = 0
self.current_cls = None
#property
def code(self):
lines = list(self.imports) + [self.linesep] + self.lines
return self.linesep.join(lines).strip()
def visit_FunctionDef(self, node: ast.FunctionDef):
arg_self = 'self' if self.current_cls is not None else ''
self.lines.extend([
' ' * self.indent + f'def test_{node.name}({arg_self}):',
' ' * (self.indent + 1) + 'assert False, "not implemented"',
self.linesep,
])
self.generic_visit(node)
def visit_ClassDef(self, node: ast.ClassDef):
clsdef_line = ' ' * self.indent + f'class Test{node.name}:'
self.lines.append(clsdef_line)
self.indent += 1
self.current_cls = node.name
self.generic_visit(node)
self.current_cls = None
if self.lines[-1] == clsdef_line:
self.lines.extend([
' ' * self.indent + 'pass',
self.linesep
])
self.indent -= 1
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
self.imports.add('import pytest')
self.lines.extend([
' ' * self.indent + '#pytest.mark.asyncio',
' ' * self.indent + f'async def test_{node.name}():',
' ' * (self.indent + 1) + 'assert False, "not implemented"',
self.linesep,
])
self.generic_visit(node)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument(
'module',
nargs='+',
default=(),
help='python modules to generate tests for',
type=lambda s: pathlib.Path(s).absolute(),
)
modules = parser.parse_args().module
for module in modules:
gen = TestModuleGenerator()
gen.visit(ast.parse(module.read_text()))
print(gen.code)

Python mock Patch os.environ and return value

Unit testing conn() using mock:
app.py
import mysql.connector
import os, urlparse
def conn():
if "DATABASE_URL" in os.environ:
url = urlparse(os.environ["DATABASE_URL"])
g.db = mysql.connector.connect(
user=url.username,
password=url.password,
host=url.hostname,
database=url.path[1:],
)
else:
return "Error"
test.py
def test_conn(self):
with patch(app.mysql.connector) as mock_mysql:
with patch(app.os.environ) as mock_environ:
con()
mock_mysql.connect.assert_callled_with("credentials")
Error: Assertion mock_mysql.connect.assert_called_with is not called.
which I believe it is because 'Database_url' is not in my patched os.environ and because of that test call is not made to mysql_mock.connect.
Questions:
What changes do I need to make this test code work?
Do I also have to patch urlparse?
You can try unittest.mock.patch.dict solution. Just call conn with a dummy argument:
import mysql.connector
import os, urlparse
from unittest import mock
#mock.patch.dict(os.environ, {"DATABASE_URL": "mytemp"}, clear=True) # why need clear=True explained here https://stackoverflow.com/a/67477901/248616
def conn(mock_A):
print os.environ["mytemp"]
if "DATABASE_URL" in os.environ:
url = urlparse(os.environ["DATABASE_URL"])
g.db = mysql.connector.connect(
user=url.username,
password=url.password,
host=url.hostname,
database=url.path[1:],
)
else:
return "Error"
Or if you don't want to modify your original function try this solution:
import os
from unittest import mock
def func():
print os.environ["mytemp"]
def test_func():
k = mock.patch.dict(os.environ, {"mytemp": "mytemp"})
k.start()
func()
k.stop()
test_func()
For this, I find that pytest's monkeypatch fixture leads to better code when you need to set environment variables:
def test_conn(monkeypatch):
monkeypatch.setenv('DATABASE_URL', '<URL WITH CREDENTIAL PARAMETERS>')
with patch(app.mysql.connector) as mock_mysql:
conn()
mock_mysql.connect.assert_called_with(<CREDENTIAL PARAMETERS>)
The accepted answer is correct. Here's a decorator #mockenv to do the same.
def mockenv(**envvars):
return mock.patch.dict(os.environ, envvars)
#mockenv(DATABASE_URL="foo", EMAIL="bar#gmail.com")
def test_something():
assert os.getenv("DATABASE_URL") == "foo"
In my use case, I was trying to mock having NO environmental variable set. To do that, make sure you add clear=True to your patch.
with patch.dict(os.environ, {}, clear=True):
func()
At the head of your file mock environ before importing your module:
with patch.dict(os.environ, {'key': 'mock-value'}):
import your.module
You can also use something like the modified_environ context manager describe in this question to set/restore the environment variables.
with modified_environ(DATABASE_URL='mytemp'):
func()
Little improvement to answer here
#mock.patch.dict(os.environ, {"DATABASE_URL": "foo", "EMAIL": "bar#gmail.com"})
def test_something():
assert os.getenv("DATABASE_URL") == "foo"

unittest - Importerror

I'm following this tutorial on web2py where you get to make a testdriven environment. However when I try to run the test with unittest, selenium I get this error:
$ python functional_tests.py
running tests
Traceback (most recent call last):
File "functional_tests.py", line 56, in <module>
run_functional_tests()
File "functional_tests.py", line 46, in run_functional_tests
tests = unittest.defaultTestLoader.discover('fts')
File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/loader.py", line 202, in discover
raise ImportError('Start directory is not importable: %r' % start_dir)
ImportError: Start directory is not importable: 'fts'
This is how the functional_tests.py looks like:
#!/usr/bin/env python
try: import unittest2 as unittest #for Python <= 2.6
except: import unittest
import sys, urllib2
sys.path.append('./fts/lib')
from selenium import webdriver
import subprocess
import sys
import os.path
ROOT = 'http://localhost:8001'
class FunctionalTest(unittest.TestCase):
#classmethod
def setUpClass(self):
self.web2py = start_web2py_server()
self.browser = webdriver.Firefox()
self.browser.implicitly_wait(1)
#classmethod
def tearDownClass(self):
self.browser.close()
self.web2py.kill()
def get_response_code(self, url):
"""Returns the response code of the given url
url the url to check for
return the response code of the given url
"""
handler = urllib2.urlopen(url)
return handler.getcode()
def start_web2py_server():
#noreload ensures single process
print os.path.curdir
return subprocess.Popen([
'python', '../../web2py.py', 'runserver', '-a "passwd"', '-p 8001'
])
def run_functional_tests(pattern=None):
print 'running tests'
if pattern is None:
tests = unittest.defaultTestLoader.discover('fts')
else:
pattern_with_globs = '*%s*' % (pattern,)
tests = unittest.defaultTestLoader.discover('fts', pattern=pattern_with_globs)
runner = unittest.TextTestRunner()
runner.run(tests)
if __name__ == '__main__':
if len(sys.argv) == 1:
run_functional_tests()
else:
run_functional_tests(pattern=sys.argv[1])
I solved this problem by replacing fts with the full path i.e. /home/simon/web2py/applications/testapp/fts
Hope this helps
I had the same problem and based on an excellent article unit testing with web2py,
I got this to work by doing the following:
Create a tests folder in the tukker directory
Copy/save the amended code(below) into tests folder as alt_functional_tests.py
Alter the web2py path in the start_web2py_server function to your own path
To run, enter the command: python web2py.py -S tukker -M -R applications/tukker/tests/alt_functional_tests.py
I am no expert but hopefully this will work for you also.
import unittest
from selenium import webdriver
import subprocess
import urllib2
execfile("applications/tukker/controllers/default.py", globals())
ROOT = 'http://localhost:8001'
def start_web2py_server():
return subprocess.Popen([
'python', '/home/alan/web2py/web2py/web2py.py', 'runserver',
'-a "passwd"', '-p 8001' ])
class FunctionalTest(unittest.TestCase):
#classmethod
def setUpClass(self):
self.web2py = start_web2py_server()
self.browser = webdriver.Firefox()
self.browser.implicitly_wait(1)
#classmethod
def tearDownClass(self):
self.browser.close()
self.web2py.kill()
def get_response_code(self, url):
"""Returns the response code of the given url
url the url to check for
return the response code of the given url
"""
handler = urllib2.urlopen(url)
return handler.getcode()
def test_can_view_home_page(self):
# John opens his browser and goes to the home-page of the tukker app
self.browser.get(ROOT + '/tukker/')
# He's looking at homepage and sees Heading "Messages With 300 Chars"
body = self.browser.find_element_by_tag_name('body')
self.assertIn('Messages With 300 Chars', body.text)
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(FunctionalTest))
unittest.TextTestRunner(verbosity=2).run(suite)
First you have to do some changes in wrong paths in fts/functional_tests.py
search for
'python', '../../web2py.py', 'runserver', '-a "passwd"', '-p 8001'
and change it to
'python', '../../../web2py.py', 'runserver', '-a "passwd"', '-p 8001'
then
tests = unittest.defaultTestLoader.discover('fts')
to
tests = unittest.defaultTestLoader.discover('.')
then
tests = unittest.defaultTestLoader.discover('fts', pattern=pattern_with_globs)
to
tests = unittest.defaultTestLoader.discover('.', pattern=pattern_with_globs)
and
sys.path.append('fts/lib')
to
sys.path.append('./lib')

Categories

Resources