[TIP] 5 lines of code equals 30+ lines of single test

Eric Larson eric at ionrock.org
Tue Jul 17 23:15:11 PDT 2012


John Wong <gokoproject at gmail.com> writes:

> Hi guys,
>
> Here is a simple function.
>
> def readfile(*args, **kwargs):
>     local = kwargs.get('local', os.getcwd())
>     filename = args[0]
>     f_path = os.path.join(local, filename)
>     with open(f_path, 'r') as f:
>         return f.read()
>
>
> Now here is just ONE case of the readfile test code. For readabilty, I will put
> in pastebin.com
> http://pastebin.com/P45uc2TV
>
> I am verifying how many times X Y C dependencies are called and what parameters
> are passed to them. I think these are necessary to check (what if one of the
> dependency functions called with the wrong variable).
>
> But is this really the nature of unittest? I've read many tests code and they
> all seen to be 5-20 lines..... I faint when I am reading my test
> code.............. SIGH
>
> I have some 10 lines function and it took 50 lines to test! Let alone for each
> of these functions, I need to write a a few tests for each branch case...
> So if there are 10 functions, each 5 lines, I can end up with 1000 lines of
> test code....
>
> Last question, I find my patching is really really long and inconvince. How
> should I patch (say they all come from os package)?
>
> Thanks.
> John

I know how you feel as I've felt the same way too. Personally, when I'm
working with an API that doesn't change much, I'll assume it works as
expected. Python's open function, and more generally the standard
library, falls in this category. When I hit this sort of code, I make a
decision on whether or not to mock based on how difficult it is.

Using the above example, I wouldn't mock/patch os.path.join and instead,
only patch the open function. The open function I would only mock/patch
in order to verify that it was passed the right arguments (the 'r' flag
and the correct f_path). 

As for the os.getcwd, that seems trickier to test b/c it means you
should probably have two tests. One where you pass in a 'local' kwarg
and one where you don't. Seeing as the local variable is set at the
beginning of the function, the tests would be rather repetative. That to
me means it might benefit from some refactoring where you always pass in
a valid value for the 'local' variable. The same goes for accepting *args.

With that in mind, this is how I might test the code:

  import os
  import mock
  import readfile_mod
   
   
  @mock.patch.object(readfile_mod, 'open')
  def test_readfile(mock_open):
      filename = 'foo'
      local = 'bar'
      fullpath = os.path.join(local, filename)
   
      result = readfile_mod.readfile(filename, local)
   
      # make sure we os.path.join'd and used the read flag when opening
      # the file
      mock_open.assert_called_with(fullpath, 'r')
      fobj = mock_open(fullpath, 'r')
   
      # make sure we read the entire file into memory after opening it.
      assert fobj.read.called
   
      # make sure we're get the result of the read as-is
      assert result == fobj.read()
         

I'm sure there are a ton of things that could be improved in my quick
example (haven't tested it!). At the very least my hope is to help to
provide a practical example of when and where to mock. I've found that
if it seems like I'm mocking like crazy and it is really confusing,
there is a good chance I'm either 1) mocking something that doesn't need
mocking or 2) the code really does need some refactoring.

Hope it helps!

Eric




More information about the testing-in-python mailing list