help: Unit test fixture returning the same object

  • Thread starter Michael McCracken
  • Start date
M

Michael McCracken

Hi, I have a problem with unittest.TestCase that I could really use
some help with.

I have a class File in module File. The important thing about File for
this discussion is that it's simple - no pool of objects are involved,
and subsequent invocations of File.File('filename') should return
distinct objects (and indeed, they do on the command line). Also,
__repr__ prints out the value of id(self) for File, so I can tell
what's going on here..

I also have a suite of tests that test File in testFile.py
Within, I have a TestCase subclass that looks like this:

class FileReadingTestCase(unittest.TestCase):
def setUp(self):
self.testfilename = "filename"
self.testfile = File.File(self.testfilename)
print 'setup:', self.testfile

def tearDown(self):
print 'teardown:', self.testfile
self.testfile.close()
self.testfile = None
print 'teardown:', self.testfile

.... followed by a bunch of tests that use self.testfile.

The problem is that in each test case, setUp and tearDown are called
as expected according to the print statements, but they don't seem to
have any effect after the first invocation. self.testfile is always
the same instance as reported by id().

There are two strange things going on here:
1 - in tearDown(), self.testfile is indeed being set to None, but in
the subsequent invocation of setUp(), self.testfile is pointing right
back to the same object.

2 - Of course, you can't print the value of self.testfile in setUp()
before it's assigned, that works as expected, but when calling the
constructor for File, you get the same object with the same id.

This doesn't happen in any other context, so I think there is
something about the unittest framework (or just Python) that I don't
understand. Does anyone have any insight?

my python is 2.3, as distributed with Mac OS X 10.3

Thanks,
-mike
 
R

Roy Smith

The problem is that in each test case, setUp and tearDown are called
as expected according to the print statements, but they don't seem to
have any effect after the first invocation. self.testfile is always
the same instance as reported by id().

Are you really sure they're the same object? id() just returns the
address of the object in memory. It's possible for two objects to have
the same id as long as their lifetimes never overlap. If you repeatedly
create and destroy the same type of objects in the same context (as
happens in a unittest environment), it's reasonably likely that objects
will reuse the same memory locations.

Other than the oddity of id() returning the same value in different
tests, is something unexpected happening?
 
P

Peter Hansen

Michael said:
The problem is that in each test case, setUp and tearDown are called
as expected according to the print statements, but they don't seem to
have any effect after the first invocation. self.testfile is always
the same instance as reported by id().

What Roy said... but to prove it to yourself, if you need to,
just implement a little class-variable-based counter in the
File __init__ method to show that you are getting different
objects each time. (That is, increment a global counter
each time you go through __init__, and store the current
count in the instance, to be retrieved and shown when you
are printing the ids() for the objects.)

-Peter
 
M

Michael McCracken

Peter Hansen said:
What Roy said... but to prove it to yourself, if you need to,
just implement a little class-variable-based counter in the
File __init__ method to show that you are getting different
objects each time. (That is, increment a global counter
each time you go through __init__, and store the current
count in the instance, to be retrieved and shown when you
are printing the ids() for the objects.)

-Peter

Thanks for the responses!
I think I'm getting pretty close to the solution, but I'd appreciate
some more help.

So, I'm still surprised that id() would point to the same object every
time, but I'm willing to believe it. However, I don't think that's the
only thing that's going on - the reason I noticed this in the first
place is that the setUp method was opening a file and populating some
lists in my File object, and those lists were accumulating objects
between test methods.

The first test sees the File as it expects, while the second one sees
a File with list member variables that have twice as many of each
object it reads in, and so forth. That's why I was willing to believe
that the same id() wasn't a fluke.

I did try the global count idea, and I did get an increasing count,
despite the same id oddity. So new instances seem to be getting
created, with the same data structures. I don't think I'm accidentally
using class variables, and that is the only thing I can think of. Any
more tips?

Thanks,
-mike
 
R

Roy Smith

So, I'm still surprised that id() would point to the same object every
time, but I'm willing to believe it. However, I don't think that's the
only thing that's going on - the reason I noticed this in the first
place is that the setUp method was opening a file and populating some
lists in my File object, and those lists were accumulating objects
between test methods.

My first guess would be that you're using class variables instead of
instance variables, but you say:
I don't think I'm accidentally using class variables

so I'm stumped. Can you post your code?
 
P

Peter Otten

Michael said:
I did try the global count idea, and I did get an increasing count,
despite the same id oddity. So new instances seem to be getting
created, with the same data structures. I don't think I'm accidentally
using class variables, and that is the only thing I can think of. Any
more tips?

1 Make a minimal script showing the offensive behaviour
2 Post it on c.l.py

Incidentally, step 2 is often unnecessary, because the error (if any) is
found in 1 :)

More concrete, you could inhibit garbage collection of File instances by
keeping old files around, e. g. in a global list. If ids are still the same
you are definitely seeing the effect of a coding error.

Peter
 
P

Peter Hansen

Michael said:
So, I'm still surprised that id() would point to the same object every
time, but I'm willing to believe it.

Don't think of it using those words. Think of it as "the new
object is being created at the same address as the old object
was at, so it's likely the first object created since the old
one was destroyed", or something like that. At least that way
it's much easier to believe, much less of a coincidence, and
actually something that a number of people have encountered
over the years, though often with similar expressions of surprise
or disbelief. :)

-Peter
 
M

Michael McCracken

Roy Smith said:
My first guess would be that you're using class variables instead of
instance variables, but you say:


so I'm stumped. Can you post your code?

I managed to fix my problem, but I don't understand why the fix works
yet, so I'm not quite satisfied.

I am trying to narrow it down so I can post more informatively.

So far my attempts at making a minimal example don't exhibit the same
problem. While I'm trying to get the smallest example with the
problem, I will describe what happened and how I 'fixed' it.

The File id() was indeed a red herring.

The problem was that in __init__, File reads a bunch of stuff (in XML
using libxml2) and populates some internal lists. The one that had the
problem was a list of Modules, which has a list of Procedures which
has a list of Blocks. Each Block has predecessors and successors.

Block looks like this:

class Block:
def __init__(self, name, preds = [], succs = []):
self.name = name

self.predecessors = preds
self.successors = succs
self.metrics = []

for pred in self.predecessors:
pred.addSuccessor(self)
for succ in self.successors:
succ.addPredecessor(self)

# add* just append to the lists, avoiding cycles.

def __repr__(self):
s = "{%d -> %s (0x%d) -> %d (%d)}" %\
(len(self.predecessors), self.name,\
id(self), len(self.successors),
len(self.metrics))
return s

In File, Blocks are created in a method that gets called as we're
parsing out the Procedures and Modules. I make a new Block with empty
preds and succs initially, then link them up after they're all
created:

in File.py:

def blockForXMLNode(self, n):
name = safeGetAttribute(n, "label", xlinkNSURI)
# this is called during the first pass, can't link up yet
preds = []
succs = []
block = Block.Block(name)# preds, succs)
print 'DEBUG: Making new block: ', block

The problem: if I run it as is, I get this output:
(During the first testCase:)
DEBUG: Making new block: {0 -> no_exit.1 (0x2714520) -> 0 (0)}
DEBUG: Making new block: {0 -> loopexit.1 (0x2714840) -> 0 (0)}
DEBUG: Making new block: {0 -> loopexit.0 (0x2715000) -> 0 (0)}
(During the next testCase:)
DEBUG: Making new block: {3 -> no_exit.1 (0x2735080) -> 5 (0)}
DEBUG: Making new block: {4 -> loopexit.1 (0x2735400) -> 6 (0)}
DEBUG: Making new block: {5 -> loopexit.0 (0x2735560) -> 7 (0)}
(and so on...)

So it does look like a class variable vs. instance variable problem,
but I can't say why. The fix is to uncomment the "# preds, succs)" and
pass in those empty lists to the Block constructor. That gives the
expected results, but I can't explain the difference.

-mike


--
 
P

Peter Otten

Michael said:
I managed to fix my problem, but I don't understand why the fix works
yet, so I'm not quite satisfied.

[...]
class Block:
def __init__(self, name, preds = [], succs = []):

Heureka! Providing mutables as default values is a well-known pitfall.

http://www.python.org/doc/faq/general.html#why-are-default-values-shared-between-objects

The default values are evaluated only once, so you end up sharing
predecessors and successors between all Block instances that are created
without explicitly passing preds and succs. A minimal example:
.... alist.append(a)
.... print alist
....
[1, 2, 3]

The standard workaround is to initialize the would-be list with None like
followed by a test in the function body:
.... if alist is None: alist = []
.... alist.append(a)
.... print alist
....
f(1) [1]
f(2) [2]
alist = []
f(1, alist) [1]
f(2, alist)
[1, 2]

Peter
 
M

Michael McCracken

Peter Otten said:
Michael said:
I managed to fix my problem, but I don't understand why the fix works
yet, so I'm not quite satisfied.

[...]
class Block:
def __init__(self, name, preds = [], succs = []):

Heureka! Providing mutables as default values is a well-known pitfall.

http://www.python.org/doc/faq/general.html#why-are-default-values-shared-betwe
en-objects

Oh, excellent - thanks for the pointer. Time to read the rest of the
FAQ. :)

-mike
 
T

Terry Reedy

Peter Hansen said:
Don't think of it using those words. Think of it as "the new
object is being created at the same address as the old object
was at, so it's likely the first object created since the old
one was destroyed", or something like that. At least that way
it's much easier to believe, much less of a coincidence, and
actually something that a number of people have encountered
over the years, though often with similar expressions of surprise
or disbelief. :)

Yes, yes. When an object is destroyed, its id is free for reuse, whatever
the implementation. For an unambiguous example:
(9692456, 9692456)

Terry J. Reedy
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,774
Messages
2,569,598
Members
45,159
Latest member
SweetCalmCBDGummies
Top