unit testing a python function which invokes a vim subprocess - python

I've written a function which opens a vim editor with the given filename when called.. How can I do the unittest of these types of operations....

To unit test something like this you must mock/stub out your dependencies. In this case lets say you are launching vim by calling os.system("vim").
In your unit test you can stub out that function call doing something like:
def launchVim():
os.system("vim")
def testThatVimIsLaunched():
try:
realSystem = os.system
called = []
def stubSystem(command):
if command == "vim":
called.append(True)
os.system = stubSystem
launchVim() # function under test
assert(called == [True])
finally:
os.system = realSystem
For more details on mocking and stubbing take a look at this article
Update: I added the try/finally to restore the original system function as suggested by Dave Kirby

This is no longer unittesting but integration testing. Why do you need to launch vim? Usually, you would 'mock' this, simulate the process spawning and depend on the fact that python's subprocess module is well tested.
To accomplish this in your code, you can, for example, subclass the class that implements your functionality and override the method that's responsible for spawning. Then test this subclass. I.e.
class VimSpawner(object): # your actual code, to be tested
...
def spawn(self):
... do subprocess magic
def other_logic(self):
...
self.spawn()
class TestableVimSpawner(VimSpawner):
def spawn(self):
... mock the spawning
self.ididit = True
class Test(..):
def test_spawning(self):
t = TestableVimSpawner()
t.other_logic()
self.failUnless(t.ididit)

Related

Pytest collection warning __init__ constructor

I really couldn't find a solution for :
PytestCollectionWarning: cannot collect test class 'TestBlaBla' because it has a init constructor
Here is my testing module and it needs to take the arguments outside of this file because at the end of the day I'm gonna call all test modules in a single file and run them all over different names and there is a bunch of names. I have to init them but when I run pytest it always ignores these classes. Idk how to handle without initializing them. If there is any suggestions I would be glad to hear.
tests/test_bla_bla.py
class TestBlaBla():
def __init__(self, **kwargs):
self.name1 = kwargs.get("name1")
self.name2 = kwargs.get("name2")
#pytest.fixture(scope='session')
def load_data_here(self):
return load_data(self.name1) # a function comes from a utils file. it only stands for load data and it needs take a name for path
..
"continue with test_ functions that use output of load_data_here"
tests/main.py
class TestingAll:
def __init__(self, *args, **kwargs):
self.name1 = kwargs.get("name1")
self.name2 = kwargs.get("name2")
self._process()
def _process(self):
TestBlaBla(name1 = self.name1, name2= self.name2)
TestBlaBla2(name1 = self.name1, name2= self.name2)
if __name__ == "__main__":
Test = TestingAll(name1 = "name1", name2= "name2")
Python test modules cannot have init methods as python test instantiates the class itself and there is not any way (IMHO?) to extend the instantiation and add arguments.
Yes, it is a natural idea to want to make your tests flexible by passing in command-line arguments. But you can't :-P. So you need to find another way of doing this.
Note also the if name == 'main': can work if you call the test file with python and add some code to explicitly call a py test runner. Note you do not just call your test class. A python test runner needs to be instantiated itself and run tests in a particular way.
e.g. we can have this which will allow python to instantiate and run tests in a TestingAll class (as long as it doesn't have an init method).
This uses the unittest.TextTestRunner
Note there are all sorts of python test runners, also runners like nose2 or like py.test which use a different test library.
if __name__ == '__main__':
unittest.main()
suite = unittest.TestLoader().loadTestsFromTestCase(TestingAll)
unittest.TextTestRunner(verbosity=3).run(suite)
You could maybe have an ArgsProcess class to process command line args.
Then iterate and set a global var with each value to be used and call the test runner each time.
But it depends on how your tests will be used.
The answers on this question already mentioned in comments explain and link to documentation for this warning:
py.test skips test class if constructor is defined
The answer on this question shows how an init method is replaced by using fixture:
Pytest collection warning due to __init__ constructor
Maybe something like this would work for you.

use mock.patch.dict() to patch os.environ in an imported class

I want to write some tests on a class that calls os.environ in its __init__() function and want to use mock.patch.dict() for calls to os.environ. I'm pretty sure I've done this successfully in the past but I can't get it working in this case.
I've put together a repro of the problem. Here's the class under test:
import os
class Widget():
def __init__(
self,
foo: str = os.environ['SOME_VAR']
):
self.bar = foo
def func1(self):
return self.bar
and my test class (in dir tests):
import unittest
import mock
from widget import Widget
#mock.patch.dict(
"widget.os.environ",
{"SOME_VAR": "qwerty"}
)
class TestWidget(unittest.TestCase):
def test1(self):
widget1 = Widget()
assert widget1.func1() == "qwerty"
I'm using pytest as my test runner. When I run the tests I get error:
tests/test_widget.py:3: in
from widget import Widget
widget.py:4: in
class Widget():
widget.py:7: in Widget
foo: str = os.environ['SOME_VAR']
../../../../.virtualenvs/pytest-issue-demo-5A-IlS7_/lib/python3.7/os.py:678: in getitem
raise KeyError(key) from None
E KeyError: 'SOME_VAR'
so it seems the call to os.environ wasn't patched. Try as I might I haven't been able to patch it.
My repro is available at https://github.com/jamiekt/pytest-issue-demo. Would appreciate someone taking a look and telling me how I can patch the call to os.environ so that I can successfully run my tests.
It uses pipenv to create a virtualenv. Assuming pipenv & make are installed running
git clone git#github.com:jamiekt/pytest-issue-demo.git
cd pytest-issue-demo
make init test
should run the tests.
The call to os.environ is part of the argument list to this function, not its body. As such, the expression os.environ["SOME_VAR"] is evaluated immediately when the module is loaded -- it's impossible to patch after the fact, because the error occurs immediately on import!
This is a poor design pattern, and should be avoided -- default values for function arguments should be limited to simple expressions which don't have side effects. But if you can't change this code, probably the easiest way to work around this will be to modify the "real" os.environ while loading the module:
try:
os.environ["SOME_VAR"] = "qwerty"
from widget import Widget
finally:
del os.environ["SOME_VAR"]
(If you expect that the environment variable might already be set, you'll need to be a little more careful about saving and restoring its state.)

unittest blacklist namespace and fail any attempt to reference it

in the case of unit testing a wrapper library, testing the wrapper without depending/exercising the upstream library is a goal; In a known case, all calls to the upstream library can be mocked and that's what I've done, but I've been frustrated by changes to the wrapper that introduce more calls to the upstream library being missed by the mock tools;
How can I best fail any test that tries to use a given namespace?
My idea currently is to change all the unittest methods to have a monkey patch like
#unittest.mock.patch('wrapper_namespace.upsteam_namespace')
and reply the upstream library with a mock that can be asserted untouched; I'm hoping for an option that works globally, so that I
don't have to add a monkeypatch to every test method, though this level of granularity is acceptable; but also don't have to perform the assertion that the mock was never used in the test methods (or make a decorator to do all that either)
prohibits access to the upstream library from any part of the software
(e.g, Wrapper calls B calls Upstream, B's call to upstream might not be caught)
You don't have to patch every test method. You can easily patch over the class if you're using unittest, or just assign the module to whatever you want to patch over it with. Here's a workable example:
A fake lib in some_lib.py:
def some_lib_func():
raise ValueError("I've been called.")
def some_other_lib_func():
raise ValueError("I've been called.")
class SomeClass:
def __init__(self):
raise ValueError("I've been constructed.")
wrapper.py:
import some_lib
def wrapper1():
some_lib.some_lib_func()
def wrapper2():
some_lib.some_other_lib_func()
def wrapper3():
x = some_lib.SomeClass()
test.py:
from unittest.mock import patch, MagicMock
import unittest
import wrapper
# Alternative:
# wrapper.some_lib = MagicMock()
# Can patch an entire class
#patch('wrapper.some_lib', MagicMock())
class TestWrapper(unittest.TestCase):
def test_wrapper1(self):
wrapper.wrapper1()
def test_wrapper2(self):
wrapper.wrapper2()
def test_wrapper3(self):
wrapper.wrapper3()
if __name__ == "__main__":
unittest.main()
We would explode if the functions/classes in some_lib were called, but they aren't:
Matthews-MacBook-Pro:stackoverflow matt$ python test.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
Feel free to comment out the patch and comment in wrapper.some_lib = MagicMock(). You'll get the same result in this toy example, but there is a major difference between the two approaches:
When using #patch('wrapper.some_lib', MagicMock()) the patch is only live for that Test Case class.
When using wrapper.some_lib = MagicMock(), however, that patch will stay live for the entire length of your python program, unless you save off the original module and patch it back manually at some point. Everything that is using the wrapper module will get the mocked version.
So you could so something like:
original_lib = wrapper.some_lib
wrapper.some_lib = MagicMock()
...
# call some test suite, every call to the wrapper module will be mocked out
...
wrapper.some_lib = original_lib
...
# call some other test suite that actually needs the real thing
...
HTH.
EDIT: Misread your question slightly, but you can inspect MagicMock objects to see if they've been called, and if so, fail the test. Or just patch over with something that fails when called (instead of MagicMock). I can provide code to do this if requested (just leave a comment), but hopefully the above can get you started. I think the crux of the question was really about the global patching. Cheers!

Skipping code in python if in a unittest

My current script calls an external script to perform some task. I want to check the code up to that point in a unittest, but not actually run the external script. Is there some way I can tell the script to effectively skip the following block IF the code is being run as part of a unit test?
The unittest package has extensive support for "mocking" functions and methods. Encapsulate the call to an external program in a simple function that your unit tests can override ("mock out") without modifying the structure of your program. Example:
Here is part of your program, in the module realcode.py
def internal_function_calling_exec(arg1):
"""The real thing"""
print("I am executing an external program")
def bigger_function_being_tested(arg1, arg2):
"""
A complex function with one or more calls to `internal_function_calling_exec`
"""
print("I will now call `internal_function_calling_exec()`")
internal_function_calling_exec(42)
Your unit test can then look like this:
import unittest
from unittest.mock import patch
import realcode
class MyTest(unittest.TestCase):
#patch("realcode.internal_function_calling_exec")
def test_something(self, mocked_func):
realcode.bigger_function_being_tested(1, 2)
mocked_func.assert_called_with(42)
This will never call the original internal_function_calling_exec(). Instead, this will trigger a call to the mock object; your test can then query the object to confirm that it was called properly.
There are ways to mock class methods etc., so you could mock subprocess.call instead, for example. But I think the above is the better pattern.
One possible approach is to set an environment variable in the unit test, and check for that environment variable in the script being tested.
For example, in unittest.py:
os.environ["testing"] = "1"
And in script-to-be-tested.py:
testing = os.environ["testing"]
... do stuff based on the testing variable
Since script-to-be-tested.py will be called from unittest.py, it should inherit the environment variables.
Possibly not the cleanest solution, but it should work.

Nose ignores test with custom decorator

I have some relatively complex integration tests in my Python code. I simplified them greatly with a custom decorator and I'm really happy with the result. Here's a simple example of what my decorator looks like:
def specialTest(fn):
def wrapTest(self):
#do some some important stuff
pass
return wrapTest
Here's what a test may look like:
class Test_special_stuff(unittest.TestCase):
#specialTest
def test_something_special(self):
pass
This works great and is executed by PyCharm's test runner without a problem. However, when I run a test from the commandline using Nose, it skips any test with the #specialTest decorator.
I have tried to name the decorator as testSpecial, so it matches default rules, but then my FN parameter doesn't get passed.
How can I get Nose to execute those test methods and treat the decorator as it is intended?
SOLUTION
Thanks to madjar, I got this working by restructuring my code to look like this, using functools.wraps and changing the name of the wrapper:
from functools import wraps
def specialTest(fn):
#wraps(fn)
def test_wrapper(self,*args,**kwargs):
#do some some important stuff
pass
return test_wrapper
class Test_special_stuff(unittest.TestCase):
#specialTest
def test_something_special(self):
pass
If I remember correctly, nose loads the test based on their names (functions whose name begins with test_). In the snippet you posted, you do not copy the __name__ attribute of the function in your wrapper function, so the name of the function returned is wrapTest and nose decides it's not a test.
An easy way to copy the attributes of the function to the new one is to used functools.wraps.

Categories