5

I have a module I need to test that calls a function on import but I cannot call this function for various reasons. So I am mocking this function but even mocking it calls import.

For example I am testing mod1.py that looks like this:

import os

def bar():
    return 'foo'

def dont_call():
    os.listdir("C:\\tmp")

dont_call()

And my test looks something like this:

import mock

@mock.patch("mod1.dont_call")
def test_mod1(mock_dont_call):
    import mod1
    assert mod1.bar()=='foo'

if __name__=="__main__":
    test_mod1()

The problem is os.listdir is called.

I cannot change mod1 so what can I do?

I am using python2.7.

To put this in context I am testing a module that opens a database connection on import which I do not agree with but I can see the reasoning behind it. Unfortunately I cannot access this database on my QA machine.

vfxGer
  • 311
  • 3
  • 12
  • Why would "hello" *not* be printed? – kindall Mar 24 '14 at 17:54
  • 1
    You cannot mock a function that is called at the global level. The simple answer is *don't do that*. The answer by Ramu below shows you one way to do that, if `dont_call()` should only ever be executed when run as a script. – Martijn Pieters Mar 24 '14 at 17:58
  • @MartijnPieters This would be a problem for writing tests around anything that uses a borg/singleton-like pattern which initializes the main instance in module scope, for example, the core `logging` module and the logger manager instance. Yeah, if it can be avoided, it's probably better, but it can't always. – Silas Ray Mar 24 '14 at 18:07
  • 1
    @SilasRay: Yes, modules that call anything at the global level on import make mocking the called object impossible. That's not always a problem for testing in general, but it *is* a problem when you did want to mock that call. – Martijn Pieters Mar 24 '14 at 18:09
  • @MartijnPieters Couldn't you write an instrumented `import` method that does a line by line eval of the module and replaces objects in the module namespace at declaration time? Sure it wouldn't be perfect, but it should at least be able to get the job done in many cases. – Silas Ray Mar 24 '14 at 18:14
  • @SilasRay: there are easier ways to solve this, you know. :-) A custom import hook that does that is no trivial task, far, far from it. – Martijn Pieters Mar 24 '14 at 18:16
  • Of course not trivial, but possible. :) And sure there are simpler ways to do it, provided you can modify the code that's giving you the problem, but it sounds like that's not an option on OP's case. – Silas Ray Mar 24 '14 at 18:17

3 Answers3

2

If you want code to 'not' be executed on import put them inside the following condition:

In mod1.py, do the following:

if __name__=="__main__":
    dont_call()

This is because, by default when you import a python module, all the code in it gets executed. By adding the above condition, you are explicitly stating that dont_call() is to be called only when the file it run as a script and not when it is imported in other modules.

Alagappan Ramu
  • 2,270
  • 7
  • 27
  • 37
  • 4
    I don't think this addresses the OP's problem -- which is where the module will be imported in two situations and in one the function shouldn't be executed. – martineau Mar 24 '14 at 18:17
  • 1
    Thank you for your answer but as I said, I cannot edit mod1.py. – vfxGer Mar 25 '14 at 11:52
0

The workaround I found was to mock what dont_call was calling giving me something like this:

import mock

@mock.patch("os.listdir")
def test_mod1(mock_dont_call):
    import mod1
    assert mod1.bar()=='foo'

if __name__=="__main__":
    test_mod1()
vfxGer
  • 311
  • 3
  • 12
  • Mocking some import path as generic as `os.listdir` can get you into troubles. Maybe, it works for this case, but please, don't do that in general. – mike_k Sep 01 '22 at 15:50
0

Check your dir

$tree.
test_shot/
├── mod1.py
├── __pycache__
│   └── mod1.cpython-310.pyc
└── test.py

Below code works fine for me.

mod1.py
import os

def bar():
    return 'foo'

def dont_call():
    os.listdir(".")

def call_this():
    print('called this')

call_this()
dont_call()

test.py

import mock

@mock.patch("mod1.dont_call")
def test_mod1(mock_dont_call):
    import mod1
    assert mod1.bar()=='foo'

if __name__=="__main__":
    test_mod1()

Here is output:

$cd test_shot
$python3 test.py
called this