Testing in Python: using nose & mocks

Here on the AppNexus Optimization team, we write code to optimize algorithms for our customers. We want to write good great code, so of course we test our code. We write and unit test our code in Python and run continuous integration using Jenkins. We have one Jenkins instance up that covers many of the different engineering teams. Our check-ins automatically trigger tests to run in Jenkins, and if we break pre-existing unit tests, we should know right away.

The actual test command called by Jenkins makes use of the Python nose module. Nose picks up any appropriate test looking modules, classes, and functions that correspond to the running code while looking at the directory structure. It is mostly smart and very useful. We can also import nose into individual test modules and just run the files. This is by far the preferred use for us because it makes debugging a whole lot easier.

Whenever we write tests in Python, we have choices on what to use for mocks and stubs. For development, we usually use the mock module. Mocking allows for “fake” objects upon which we can impose certain behaviors. This allows us to separate the function we are testing from the functionality called by the function we are testing.

Our team also runs a few internal websites using Django. In this entry I will go over the basics of Python test-writing using the nose and mock classes. Check back later this week for my post on how we run our tests using the Django framework.

Nose

Before I dive in to how to test your code, let’s quickly discuss nose, a module we use for Python testing. Nose is a pretty clever test module, and it will look for all the test like files when you run it and call the matching functions in the code base, calculate their coverage, etc. You can install it using pip: https://nose.readthedocs.org/en/latest/

Run the following (from a root directory):

1
$ nosetests

or in the code, append the following:

1
2
3
4
if __name__  ==  '__main__':
    import nose
    nose. runmodule (argv = [__file__ , '-vvs' , '-x' , '--pdb' ,  'pdb-failure' ] ,
exit = False )

And type:

1
$ python  test / code / with /nose/imported/testscript. py

It should run the tests defined in that file only.

Background on Mocking

As described above, mocking is a must if you want to keep your tests clean and strictly dependent on the function of interest.  The basic idea is that if we have a function:

def the_function():
    something = function_one()
    return function_two(something)

and we want to test the functionality of only the_function (and not the that of function_one and function_two), we mock function_one and function_two and make sure that they were called. That fullfills the functionality of the_function. function_one and function_two should be tested elsewhere, in a different function.

The Mock Class

You can read about mock at http://www.voidspace.org.uk/python/mock/.

The documentation is pretty good, so it should explain most of the cases you encounter. Here, I will share some of the cases not covered in the documentation.

The Basics

How To Mock the Functions

To mock functions, I use a Python module called mock. You can install it using pip. For any object that you want to mock, create a mock object. You can then associate mock objects with mocks and set their return value.

1
2
3
myobject  = mock. Mock ( )
mock_return_value  =  { "fake""value" }
myobject. function  = mock. Mock (return_value =mock_return_value )

Now, if you call:

1
2
returned  = myobject. function ( )
assert returned  == mock_return_value

The above assert statement should return True.

How do we test if a function was called with the right arguments?

Consider the following:

class Foo: 
    def  __init__ ( self ): 
        pass 
    def another_func ( self ): 
        # do something 
    def somefunc ( self ): 
        self. another_func ( ) 

class TestFoo: 
    def test_somefunc ( ): 
        foo  = Foo ( ) 
        foo. another_func  = mock. Mock ( ) 
        foo. somefunc ( ) 
        foo. another_func. assert_called_once_with ( )

If you want to test for arguments, change the assert line to:

1
foo. another_func. assert_called_once_with ( "arg1" , "arg2" )

or for call counts:

1
assert foo. another_func. call_count  ==  1

To check arguments for multiple calls, change it to

1
assert foo. another_func. call_args_list [ 0 ] [ 0 ]  =  [something , something ]

or

1
assert foo. another_func. call_args_list  =  [mock. call (arg1 ) , mock. call (arg2 ) ]

Now, say you want to run successive tests and check for arguments each time.

class Foo: 
    def  __init__ ( self ): 
         pass 
    def function( self , arg1 ): 
        self.innerfunction(arg1 ) 

    def innerfunction( self , arg1 ): 
        # do something 

class TestFoo: 
    def  __init__ ( self ): 
        self.myobj  = Foo( ) 

    def test_function ( self ): 
        self.myobj.innerfunction  = mock.Mock( ) 
        self.myobj.function( "input_one" ) 
        self.myobj.innerfunction.assert_called_once_with( "input_one" ) 
        self.myobj.function( "input_two" ) 
        self.myobj.innerfunction.assert_called_once_with( "input_two" )

Here, the second assertion will fail. That’s because the self.myobj.innerfunction mock object has been called twice.

So what we need, instead, is the following:

def test_function ( self ): 
    self. myobj. innerfunction  = mock. Mock ( ) 
    self. myobj. function ( "input_one" ) 
    self. myobj. innerfunction. assert_called_once_with ( "input_one" ) 
    self. myobj. innerfunction. reset_mock ( ) 
    self. myobj. function ( "input_two" ) 
    self. myobj. innerfunction. assert_called_once_with ( "input_two" )

The reset_mock() function resets the call counts and its arguments. Now, the test will pass.

How To Unmock Functions

Sometimes, in one test function you mock a function.

For example, assume you define:

class TestFoo: 
    def  __init__ ( self ): 
        self. myobj  = Foo ( ) 

    def test_function ( self ): 
        self. myobj. innerfunction  = mock. Mock (return_value = True ) 
        # more test code 

    def test_inner_function 
        # some test code 
        assert  False  ==  self. myobj. innerfunction (some_arg )


But self.myobj.innerfunction is already associated with a mock, and it will return True. How do we get around it?

One clean solution is to create a function that returns an object not associated with mocks within the TestFoo class.

def make_object( self ): 
    return Foo( )

Then you can use the make_object function as below:

def test_function ( self ): 
    myobj  =  self.make_object( ) 
    myobj.innerfunction  = mock.Mock(return_value = True ) 
    # more test code 

def test_inner_function(self)
    myobj  =  self.make_object( ) 
    # some test code 
    assert  False  == myobj.innerfunction(some_arg )

How To Mock builtin Functions

Sometimes you need to mock functions such as open() that opens a file.

def read_from_file(somefile ): 
    myarray  =  [ ] 
    for line  in  open(somefile , "r" ): 
        myarray.append(line ) 
    return myarray

How do we mock the function open?

The following will do the trick:

1
2
import  __builtin__
builtin__. open  = mock. Mock (return_value = [ "asdfasd" , "asdfasd" ] )

Make sure to reload the module:

1
reload ( __builtin__ )

If other functions use open, unless you reload the __builtin__ module, the function will keep using mocked function.

How To Mock Module Functions

You sometimes have import statements. For example:

# myscript.py 
from anotherscript  import another_function 

class Foo: 
    def function( self , arg1 ): 
    arg1 * =  2 
    return another_function(arg1 )

Say we want to test the function in the Foo class. But we want to stub another_function. How we do that?

The solution is:

import myscript 
class TestFoo: 
    def  __init__( self ): 
        self.myobj  = Foo( ) 

    def test_function ( self ): 
        myscript.another_function  = mock.Mock ( ) 
        self.myobj.function( 2 ) 
        myscript.another_function.assert_called_once_with( 4 ) 
reload (myscript )

You must reload the module because the module’s function is now a mocked object, and it won’t work if called from other functions within the module.

That is it for the mock class.  But we also run internal websites, and we use Django on Apache for that. And it comes with a test suite, so we should write tests and use them!

你可能感兴趣的:(python,test,mock)