Mocking __init__() for unittesting

Question:

I have a class:

class DatabaseThing():
     def __init__(self, dbName, user, password):
          self.connection = ibm_db_dbi.connect(dbName, user, password)

I want to test this class but with a test database. So in my test class I am doing something like this:

import sqlite3 as lite
import unittest
from DatabaseThing import *

class DatabaseThingTestCase(unittest.TestCase):

    def setUp(self):
        self.connection = lite.connect(":memory:")
        self.cur = self.connection.cursor()
        self.cur.executescript ('''CREATE TABLE APPLE (VERSION INT, AMNT SMALLINT);
            INSERT INTO APPLE VALUES(16,0);
            INSERT INTO APPLE VALUES(17,5);
            INSERT INTO APPLE VALUES(18,1);
            INSERT INTO APPLE VALUES(19,15);
            INSERT INTO APPLE VALUES(20,20);
            INSERT INTO APPLE VALUES(21,25);''')

How would I go about using this connection than the connection from the class I want to test? Meaning using the connection from setUp(self) instead of the connection from DatabaseThing. I cannot test the functions without instantiating the class. I want to mock the __init__ method somehow in the Test Class, but I didn’t find anything that seemed useful in the documentation.

Asked By: user1853788

||

Answers:

Rather than try to replace the init function which is messy, fragile and hacky, try passing a function to your database constructor as shown below:

# Test connection creation
def connect_lite(dbName=None, user=None, password=None):
    connection = lite.connect(":memory:")
    cur = self.connection.cursor()
    cur.executescript ('''CREATE TABLE APPLE (VERSION INT, AMNT SMALLINT);
                          INSERT INTO APPLE VALUES(16,0);
                          INSERT INTO APPLE VALUES(17,5);
                          INSERT INTO APPLE VALUES(18,1);
                          INSERT INTO APPLE VALUES(19,15);
                          INSERT INTO APPLE VALUES(20,20);
                          INSERT INTO APPLE VALUES(21,25);''')
    return cur


# Production connection creation
def connect_ibm(dbName, user, password):
    return ibm_db_dbi.connect(dbName, user, password)

# Your DatabaseThing becomes:
class DatabaseThing():
    def __init__(self, connect, dbName, user, password):
        self.connection = connect(dbName, user, password)

# In your test create a DatabaseThing
t = DatabaseThing(connect_lite, dbName, user, password)

# In your production code create a DatabaseThing
p = DatabaseThing(connect_ibm, dbName, user, password)      

This has the side benefit of slightly decoupling your code from the database technology you are using.

Answered By: Jonathan

Considering ibm_db_dbi and lite share the same interfaces, this should do the trick:

import mock
import sqlite3 as lite

class DatabaseThingTestCase(unittest.TestCase):

    def setUp(self):
        self.patch = mock.patch('module_with_DatabaseThing_definition.ibm_db_dbi', lite)
        self.patch.start()

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

I.e. your DatabaseThing file is named database/things.py then the patch would look like this database.things.ibm_db_dbi.

Example of the mocking:

moduleA.py

def connection(*args):
    print 'The original connection. My args', args

moduleB.py

def connection(*args):
    print 'The mocked connection. My args', args

myClass.py

import moduleA


class MyClass(object):
    def __init__(self):
        self.connection = moduleA.connection('Test', 'Connection')

test.py

import mock
import moduleB

from myClass import MyClass


def regular_call():
    MyClass()


def mocked_call():
    def wrapped_connection(*args):
        return moduleB.connection(':memory:')

    my_mock = mock.Mock(wraps=moduleB)
    my_mock.connection = wrapped_connection
    with mock.patch('myClass.moduleA', my_mock):
        MyClass()

    MyClass()

regular_call()
mocked_call()

Running test.py gives:

The original connection. My args ('Test', 'Connection')
The mocked connection. My args (':memory:',)
The original connection. My args ('Test', 'Connection')
Answered By: Maciej Gol

Instead of mocking, you could simply subclass the database class and test against that:

class TestingDatabaseThing(DatabaseThing):
     def __init__(self, connection):
          self.connection = connection

and instantiate that class instead of DatabaseThing for your tests. The methods are still the same, the behaviour will still be the same, but now all methods using self.connection use your test-supplied connection instead.

Answered By: Martijn Pieters

Method 1: Subclass

Please refer to @Martijn Pieters’ answer.

Method 2: Inversion of Control

A long term solution is to have the client create the connection and hand it over to DatabaseThing to use. Using the Single Responsibility principle, I don’t think DatabaseThing should be responsible for making the connection in this case.

This technique cuts dependencies and gives you a lot more flexibility e.g. you can setup a connection pool and give each new instance of DatabaseThing a connection from the pool, without changing anything in DatabaseThing.

Answered By: James Lim

You should use mock package to mock the method __init__ of the class:

from mock import patch


def test_database_thing(self):
    def __init__(self, dbName, user, password):
        # do something else
    with patch.object(DatabaseThing, '__init__', __init__):
        # assert something

Answered By: Julio Marins

If you want to return a mock when you initialize a class, mock out the__new__ method, not init.

new makes the new instance and init initializes it, but can only return None.

If you mock new, it can return a mock you can assert on to simulate instance creation in the test.

@mock.patch('Car.__new__')
def test_create_car(self, mock_Car):
    mock_inst = mock.MagickMock()
    mock_Car.return_value = mock_inst

    create_car()

    # Assert class was called
    mock_Car.assert_called_once()
    # __new__ is called with actual class as first arg
    mock_Car.assert_called_with(Car) 

    # Assert instance method is called as expected
    mock_inst.set_miles.assert_called_with(0)
Answered By: mauricio777