5

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?

skytreader
  • 11,467
  • 7
  • 43
  • 61

2 Answers2

10

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
pepr
  • 20,112
  • 15
  • 76
  • 139
  • So...educate me a bit more. What's the difference between `is None` and `== None`? I always used `is None`; didn't know `== None` works. – skytreader Jul 08 '12 at 15:21
  • 1
    @skytreader: The operator `is` tests for the object identity. The `None` value is represented by the single instance of the NoneType class. Having the value `None` means that you are sharing the reference to the identical object. The `numlist is None` means that you are testing whether the identical object is shared. The `==` operator is more complicated. If the `numlist` were the (reference to the) instance of a class that defines its own `.__eq__` method, the `==` could produce an unexpected boolean value. However, it is about the same in simple cases. – pepr Jul 08 '12 at 15:40
4

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.

David Hall
  • 32,624
  • 10
  • 90
  • 127
  • Oh shit. My +1 >:) It looks as if I copy/pasted your solution. But it is really only the coincidence. Probably only `numlist is None` would be better. – pepr Jul 08 '12 at 09:45
  • @pepr no problems +1 from me to you too for good form, and the right answer also :) The article by Fredrik Lundh really is great so no surprise we both mentioned it. – David Hall Jul 08 '12 at 09:46