[rfc] add a TestFactory class or concept

John Arbash Meinel john at arbash-meinel.com
Tue Aug 18 15:20:14 BST 2009


-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Aaron Bentley wrote:
> Martin Pool wrote:
>>> Martin Pool wrote:
>>>> Lots of tests are run on some specific values; for instance they make
>>>> a branch and then do some operations on it.
>>> We need specific values in order to make assertions about them, and to
>>> provide legibility.  Could you please explain what about this is bad?
>> I will try.
> 
>> It's not quite true that you need specific values in order to make
>> assertions about them: a very common case is that we commit a revision
>> and then check that its id is retrieved later, without ever hardcoding
>> the revision id.
> 
> I think this is a good example, because it gets at a lot of the issues.
> 
> I don't think we're making assertions about specific values in that
> case.  In an example where we were making assertions about specific
> values, we would specify a revision-id, and then later test that the
> literal revision-id had been used.
> 
> So if you'd like to argue instead that we rarely need to make assertions
> about specific values, I'd certainly agree it's not a need.
> 
> We can test almost everything by allowing the the revision-id to be
> auto-generated and storing it in a variable.  (We can't test that
> specifying a revision-id works, of course.)  But I find that the
> indirection through that variable name makes the test more abstract,
> less concrete, and harder to follow.

Sometimes that is certainly true. Interestingly enough, I've found the
opposite to be true with the new 'key' based ids that we have. Specifically:

  wt.commit('foo', rev_id='rev-1')

I would generally use the string form rather than doing:
  rev1 = 'rev-1'
  wt.commit('foo', rev_id=rev1)
or
  rev1 = wt.commit('foo')

However, with keys, I have to do:

  vf.add_lines(('key1',), [], lines)
  vf.add_lines(('key2',), [('key1',)], lines)
  vf.add_lines(('key3',), [('key1',), ('key2',)], lines)

And I find I would rather do:
  key1 = ('key1',)
  key2 = ('key2',)
  key3 = ('key3',)
  vf.add_lines(key1, [], lines)
  vf.add_lines(key2, [key1], lines)
  vf.add_lines(key3, [key1, key2], lines)

I think a lot of it is the sheer amount of noise that is in:
 [('key1',), ('key2',)]

Which has as 5 punctuation characters for every 4 value characters.
('',) vs key1

I will also say that tests are easier to debug when the handle is 4-5
characters long and the *same* with every test run, rather than getting
back:
 'john at arbash-meinel.com-20090817221106-snef4s9g2f7zue60' not in
 set(['john at arbash-meinel.com-20090817220821-wzul2cdupe88xi7t',
      'john at arbash-meinel.com-20090817210105-k1ejuqmyaz4u2zfs'])

vs
 'rev-2a' not in set(['rev1', 'rev2'])

...


> 2. Cleanup must be manually specified.
> 
> Many test fixtures need to be cleaned up-- they need locks released,
> etc.  A method of TestCaseWithTransport can arrange for cleanup using
> addCleanup, but a method of a test factory like Launchpad's cannot use
> this facility.  This means that clients must manually specify cleanup.
> 
> We could alter the design so that the factory was initialized with a
> cleanup facility.  We could alter the design so that the factory also
> provided cleanup, and TestCase.tearDown would call it.  (Though I'd
> worry about doing things out of order then.)

You can simply have the factory constructor take the test case as a
parameter, and then have functions call 'self.test.addCleanup()'.

Alternatively the members themselves:

self.factory.makeFoo(self, ...)

...

> BranchBuilder-- well, no compelling case has been given to me for using
> it.  It wouldn't be appropriate for me to comment further until I
> understand what it's meant for.
> 
> Aaron

At a minimum, it is a *hell* of a lot easier to create ancestry graphs
that have merges in them, rather than using clone/merge/commit. Consider:

builder = self.make_branch_builder('source')
builder.start_series()
builder.build_snapshot('A', None, [
  ('add', ('', 'TREE_ROOT', 'directory', None)),  # [1]
  ])
builder.build_snapshot('B', ['A'], [])
builder.build_snapshot('C', ['A', 'B'], [])
builder.build_snapshot('D', ['B'], [])
builder.build_snapshot('E', ['C', 'D'], [])
builder.finish_series()
b = builder.get_branch()

versus

tree = self.make_branch_and_tree('tree1')
tree.add(['file'], ['file-id'])
# tree.set_root_id()
tree.commit('A', rev_id='A')
tree2 = tree.bzrdir.sprout('tree2').open_workingtree()
tree2.commit('B', rev_id='B')
tree1.merge_from_branch(tree2.branch)
tree1.commit('C', rev_id='C')
tree2.commit('D', rev_id='D')
tree1.merge_from_branch(tree2.branch)
tree1.comimt('E', rev_id='E')

The biggest problem with the 'tree.merge_from_branch' scenario is that
you have to trace back to see what the actual last-modified revision is
for the tree in question. With 2 trees, it isn't terrible. But we have
tests that have 3 and 4 trees.

John
=:->



[1] tree.commit() does automatically version a tree root for you. Which
is something that is a bit of a pain with branch builder. In that this
line gets repeated in pretty much every test.

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkqKuJ4ACgkQJdeBCYSNAANqugCgw252fRheqfIXpd9h3tjNuYiE
SU8AoKVoBN9Ba1HGTI+V+HVLrvhB9d0i
=+yQW
-----END PGP SIGNATURE-----



More information about the bazaar mailing list