Python Unit Test : How to unit test the module which contains database operations?

Viewed 28065

I am using pymysql client library to connect to the real database. I have a function in module, where I connect to the database using pymysql and do only database insert operations.How to unit test this function in python without hitting the real database?

import pymysql

def connectDB(self):

# Connect to the database
connection = pymysql.connect(host='localhost',
                             user='user',
                             password='passwd',
                             db='db')

try:
    with connection.cursor() as cursor:
        # Create a new record
        sql = "INSERT INTO `users` (`email`, `password`) VALUES (%s, %s)"
        cursor.execute(sql, ('newuser@some.com', 'newpassword'))


    connection.commit()

My python version is 2.7.

3 Answers

You can use patch, like this:

from unittest.mock import patch, MagicMock

@patch('mypackage.mymodule.pymysql')
def test(self, mock_sql):
    self.assertIs(mypackage.mymodule.pymysql, mock_sql)

    conn = Mock()
    mock_sql.connect.return_value = conn

    cursor      = MagicMock()
    mock_result = MagicMock()

    cursor.__enter__.return_value = mock_result
    cursor.__exit___              = MagicMock()

    conn.cursor.return_value = cursor

    connectDB()

    mock_sql.connect.assert_called_with(host='localhost',
                                        user='user',
                                        password='passwd',
                                        db='db')

    mock_result.execute.assert_called_with("sql request", ("user", "pass"))

You need a series of fake databases, called stubs, which return hardcoded values. During the test these stubs are used instead of the real database. I am not familiar with Python, but one way to do this in C++ is to make your object to receive the database as a constructor parameter. In production code you use a real database parameter, in the test the stub. This can be done because the constructor expects a pointer to a common base class. Even it is not written for Python I suggest to read the first chapters from Roy Osherove: The art of unit testing. The book clearly explains why these fake databases are stubs and not mocks.

You've just rediscovered one of the most compelling reasons why testing is important: it tells you when your design is bad.

To put it slightly differently, testability is a good first-order proxy for quality. Consider the following:

class DB(object):
    def __init__(self, **credentials):
        self._connect = partial(pymysql.connect, **credentials)

    def query(self, q_str, params):
        with self._connect as conn:
            with conn.cursor() as cur:
                cur.execute(q_str, params)
                return cur.fetchall()

# now for usage

test_credentials = {
    # use credentials to a fake database
}

test_db = DB(**test_credentials)
test_db.query(write_query, list_of_fake_params)
results = test_db.query(read_query)
assert results = what_the_results_should_be

If you work with multiple databases you could use polymorphism or depending on API similarity make the specific DB be a constructor parameter to your object.

Related