Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit testing objects in Python - Object is not over written in setup

I'm unit testing classes in Python using unittest. As I understand it, unittest calls the setUp function before each test so that the state of the unit test objects are the same and the order the test are executed wouldn't matter.

Now I have this class I'm testing...

#! usr/bin/python2

class SpamTest(object):

    def __init__(self, numlist = []):
        self.__numlist = numlist

    @property
    def numlist(self):
        return self.__numlist

    @numlist.setter
    def numlist(self, numlist):
        self.__numlist = numlist

    def add_num(self, num):
        self.__numlist.append(num)

    def incr(self, delta):
        self.numlist = map(lambda x: x + 1, self.numlist)

    def __eq__(self, st2):
        i = 0
        limit = len(self.numlist)

        if limit != len(st2.numlist):
            return False

        while i < limit:
            if self.numlist[i] != st2.numlist[i]:
                return False

            i += 1

        return True

with the following unit tests...

#! usr/bin/python2

from test import SpamTest

import unittest

class Spammer(unittest.TestCase):

    def setUp(self):
        self.st = SpamTest()
        #self.st.numlist = [] <--TAKE NOTE OF ME!
        self.st.add_num(1)
        self.st.add_num(2)
        self.st.add_num(3)
        self.st.add_num(4)

    def test_translate(self):
        eggs = SpamTest([2, 3, 4, 5])
        self.st.incr(1)
        self.assertTrue(self.st.__eq__(eggs))

    def test_set(self):
        nl = [1, 4, 1, 5, 9]
        self.st.numlist = nl
        self.assertEqual(self.st.numlist, nl)

if __name__ == "__main__":
    tests = unittest.TestLoader().loadTestsFromTestCase(Spammer)
    unittest.TextTestRunner(verbosity = 2).run(tests)

This test fails for test_translate.

I can do two things to make the tests succeed:

(1) Uncomment the second line in the setUp function. Or,

(2) Change the names of the tests such that translate occurs first. I noticed that unittest executes tests in alphabetical order. Changing translate to, say, atranslate so that it executes first makes all tests succeed.

For (1), I can't imagine how this affects the tests since at the very first line of setUp, we create a new object for self.st . As for (2), my complaint is similar since, hey, on setUp I assign a new object to self.st so whatever I do to self.st in test_set shouldn't affect the outcome of test_translate.

So, what am I missing here?

like image 623
skytreader Avatar asked Jul 08 '12 09:07

skytreader


2 Answers

Without studying the detais of your solution, you should read the Default Parameter Values in Python by Fredrik Lundh.

It is likely that it explains your problem with your empty list as a default argument. The reason is that the list is empty only for the first time unless you make it empty explicitly later. The initialy empty default list is the single instance of the list type that is reused when no explicit argument is passed.

It is good idea to read the above article to fix your thinking about the default arguments. The reasons are logical, but may be unexpected.

The generally recommended fix is to use None as the default value of the __init__ and set the empty list inside the body if the argument is not passed, like this:

class SpamTest(object):

    def __init__(self, numlist=None):
        if numlist is None:
            numlist = []         # this is the new instance -- the empty list
        self.__numlist = numlist
like image 196
pepr Avatar answered Oct 16 '22 21:10

pepr


This is due to the way default parameters behave in Python when using Mutable objects like lists: Default Parameter Values in Python.

In the line:

def __init__(self, numlist = []):

The default parameter for numlist is only evaluated once so you only have one instance of the list which is shared across all instance of the SpamTest class.

So even though the test setUp is called for every test it never creates a fresh empty list, and your tests which work upon that list instance end up stepping on each others toes.

The fix is to have something like this instead, using a non-mutable object like None:

def __init__(self, numlist = None):
    if numlist is None:
        numlist = []
    self.__numlist = numlist

The reason it works when setting the property is that you provide a brand new empty list there, replacing the list created in the constructor.

like image 24
David Hall Avatar answered Oct 16 '22 19:10

David Hall