Python Testing for the Lazy Dev

Sun Dec 23 2018

If you want to get your Python tests to save you time and make sure you are not going to be woken up in the middle of the night by painful calls to fix a bug, please, continue on. Writing tests can be quite time-consuming, but if you know what you are doing and understand the different ways for mocking your code, you are on the right path. I’ll elaborate quickly on the few different mock abilities python provides.
First, here is a link to the code discussed below:
[GitHub tzookb/python_mocks_post][1] These are the file paths:

othersrc
  other_module.py
src
  main.py
  some_module.py
test
  ...
...

other_module.py

def some_method():
    return "real result"

def method_with_param(p1, p2):
    return p1

some_module.py

def some_method():
  return "real result"

def method_with_param(p1, p2):
    return p1

main.py

from src.some_module import some_method
import othersrc.other_module as om

def main1():
    return some_method()

def main2():
    return om.other_method()

def code_with_exception_check():
    try:
        some_method()
    except Exception as ex:
        return "error"

def main3():
    return om.method_with_param("a_param", "b_param")

def main4():
    res = {
        "a": some_method(),
        "b": om.other_method()
    }
    return res

class HasDependency:
    def __init__(self, dependency):
        self.dependency = dependency
    def run(self):
        return self.dependency.run()

Now, lets start by showing the various ways you can test these.

Simple module mocking {#simplemodulemocking}

def test_simple_patch_in(self):
  with patch('src.main.some_method', return_value="mocked resp"):
      res = main_module.main1()
  self.assertEqual(res, "mocked resp")

As you can see, here we are using “with patch”.

We need to pass as the first argument the “path” of the function we want to mock. The “return_value” named parameter is one of the options we are going to explore today. It merely means that when the mocked function is called, it will return what we set here.
Now, when our test subject imports “some_method”, instead of getting the real code it will get our mocked object.

Module mocking and method parameters {#modulemockingandmethodparameters}

@patch('src.main.some_method', return_value="mocked resp")
def test_simple_patch_on_method(self, some_method_mocked):
    res = main_module.main1()
    self.assertEqual(res, "mocked resp”)

This way, we annotate our test method just like we did in the above example. Both ways do the same thing, but this way, its much easier to mock several objects without the need for several indents.

Dynamic mock action {#dynamicmockaction}

@patch('src.main.some_method')
def test_simple_patch_on_method_internal(self, some_method_mocked):
    some_method_mocked.return_value="mocked resp"
    res = main_module.main1()
    self.assertEqual(res, "mocked resp")

Same as the above mock annotation, but here we define how the mock will behave or specifically what it will return inside the code and not outside of it.

Several mocks {#severalmocks}

@patch('src.main.some_method')
@patch('src.main.om')
def test_simple_patch_on_method_internal(self, om_mock, some_method_mocked):
    om_mock.other_method.return_value = "am_mocked"
    some_method_mocked.return_value="mocked resp"
    res = main_module.main4()
    self.assertEqual(res["a"], "mocked resp")
    self.assertEqual(res["b"], "am_mocked")

This example just shows how you can mock several functions or modules. As you can see, the order of the annotations is opposite to the order of the variables inside the test method param list.

Testing Exceptions happening {#testingexceptionshappening}

@patch('src.main.some_method')
def test_mock_exception(self, some_method_mocked):
    some_method_mocked.side_effect = Exception()
    res = main_module.code_with_exception_check()
    self.assertEqual(res, "error")

As we already got to know the “return_value”, here we have the “side_effect” this is where you can set an Exception if you want your mocked function to raise an exception.

Create a mock and pass it on as a dependency {#createamockandpassitonasadependency}

def test_dependency(self):
    dependency = MagicMock()
    dependency.run.return_value = 55
    res = (main_module.HasDependency(dependency)).run()
    self.assertEqual(res, 55)

Here you can see we are not using “patch” as we are not patching any python paths. We create a “MagicMock” (exactly what patch creates), and then we can define what we need it to do and pass it anywhere we need it to go.

Mocking an object globally {#mockinganobjectglobally}

class MainGlobalMockTest(unittest.TestCase):

def setUp(self):
    self.patcher = patch('src.main.some_method')
    self.some_method_mock = self.patcher.start()

def tearDown(self):
    self.patcher.stop()

def test_simple(self):
    self.some_method_mock.return_value="mocked resp"
    res = main_module.main1()
    self.assertEqual(res, "mocked resp")


The two new methods “setUp” and “tearDown” will run at the start and the end of each test respectively. Inside “setUp”, we define the “patcher” and then “start” it. Inside “tearDown”, we stop it so it won’t affect other tests.

  
After the above, inside “test_simple” everything works exactly the same. {{< youtube mKF2Swmqyu0 >}} Originally posted on <a href="https://x-team.com/blog/python-testing-lazy-dev/" target="_blank" rel="noreferrer noopener" aria-label="X-Team Blog (opens in a new tab)">X-Team Blog</a>

 [1]: https://github.com/tzookb/python_mocks_post


export const _frontmatter = {"title":"Python Testing for the Lazy Dev","author":"tzookb","type":"post","date":"2018-12-23T15:03:18.000Z","url":"/2018/12/python-testing-for-the-lazy-dev/","categories":["Python"],"tags":["python","testing","workflow"]}