[Product-Developers] Re: Mock testing

Martin Aspeli optilude at gmx.net
Wed Jul 2 20:41:34 UTC 2008


Hi Maurits,

> I have not tried any code (also not from Dexterity), only read your
> article.  Some things are not clear to me.
> 
> - At the end of the first mock example you say: "We must remember to put
>   the test case into replay mode, using self.replay()."  Why?  What does
>   that do?

Before you put the mock into replay mode, it's in "record" mode. All 
operations you do on it are essentially expectations. For example:

  mymock = self.mocker.mock()
  self.expect(mymock.foo()).result(True)

Now, if I do

  mymock.foo()

without putthing the mock in replay mode, it's basically just recording 
an expectation that foo will be called. In fact, the above could have 
been rewritten as:

  mymock = self.mocker.mock()
  mymock.foo()
  self.mocker.result(True)

though I prefer the self.expect() chaining syntax as it's more compact.

Only when the mock is put in replay mode will it actually exhibit the 
behaviour and check that it's being called in the expected way. You do 
that with

  self.replay()

This is how most mock libraries work. You start off in record mode, 
doing operations on the object that "record" expectations about how it 
should be called. You then put it into "replay" mode and call the code 
under test (initialised to use the mock, obviously). The mock library 
then checks that recorded operations actually happen (in the way that 
they were recorded) and throws assertion errors if they're not. Finally, 
you do a verify + restore (implicit in the unit test) that ensures no 
steps were missed and unpatches anything patched for the test.

> - In the third mock example you say that we "set the expectation that
>   lookup_schema() should be called on it once", stressing the word
>   'once'.  Which part of the test code does that?  From the paragraphs
>   after that I *guess* that whenever the code is this:
> 
>     self.expect(...).result(...)
> 
>   it implicitly means this:
> 
>     self.expect(...).result(...).count(1, 1)
 >
>   Is that a correct guess?

Yes.

>  If so, I think you should mention that
>   explicitly, perhaps already in the first mock example.  

In record mode, the assumption is that if you record an action once, you 
meant for it to happen once. Let's say I have a mock and I expect it to 
be called twice, returning one thing the first time and something else 
the second time:

  magic_eight_ball = self.mocker.mock()
  self.expect(magic_eight_ball()).result("No")
  self.expect(magic_eight_ball()).result("Yes")

>   BTW, if that is correct then I would find "count(0, None)" a more
>   logical default, so by default no restriction on the number of times
>   a mocked method is called.

Most mock libraries don't work like this. They fail when a method is 
called too many times (and will tell you so). I think it's (marginally) 
better to be explicit if you don't care how many times something's 
called. It's likely that if you expected something to be called, and 
it's called 0 times, then it's an error, and if it's called dozens of 
times, it's also an error.

> And a question: is there next to mock_utility also something like
> mock_tool, so you could you this to mock for example the
> portal_catalog?  I guess something along these lines might work:
> 
>   from whereever import CatalogTool
>   catalog_mock = self.mocker.proxy(CatalogTool())
>   context_mock = self.mocker.mock()
>   self.expect(context_mock.portal_catalog).result(catalog_mock).count(0,None)

No. There could be, but bear in mind that mock tests are not running in 
a PloneTestCase (which is why they're so quick) - at least not normally 
(no reason they couldn't be, but then you're really doing an integration 
test). As such, if your code calls getToolByName(), you'd probably do a 
replace() on this.

A generic mock_tool() could look like this, though:

  from mocker import ANY

  class MockTestCase(...):
      ...
      getToolByName_mock = None

      def mock_tool(self, tool_mock, name, expected_context=ANY,
	            min=1, max=None):
          mock = self.getToolByName_mock
          if mock is None:
              self.getToolByName_mock = mock = \
              self.mocker.replace('Products.CMFCore.utils.getToolByName')
          self.expect(mock(expected_context,
                              name)).result(tool_mock).count(min, max)

Untested and possibly refactorable of course.

Martin

-- 
Author of `Professional Plone Development`, a book for developers who
want to work with Plone. See http://martinaspeli.net/plone-book





More information about the Product-Developers mailing list