[Tutor] How do I test file operations (Such as opening, reading, writing, etc.)?

boB Stepp robertvstepp at gmail.com
Thu Jan 28 23:20:56 EST 2016


On Wed, Jan 27, 2016 at 11:24 PM, Danny Yoo <dyoo at hashcollision.org> wrote:

> You can make the file input/output interface a parameter of your editor.
>
> ###############################
> class Editor(object):
>     def __init__(self, filesystem):
>         self.filesystem = filesystem
>     ...
> ################################
>
> Since it's an explicit parameter, we can pass in either something that
> does it "for real" by using the built-in input output functions, or we
> can "fake it", by providing something that's convenient for unit
> tests.
>
>
> What do we need out of an input/output interface?  Well, maybe a few
> basic operations.  Let's say that we need to be able to do two things:
>
>
>     1. Open files, which returns a "filelike" object.  If it can't
> find the file, let's have it raise an IOError.
>
>     2. Create new files, which returns a "filelike" object.
>
> This is admittedly bare-bones, but let's demonstrate what this might
> look like.  First, let's see what a real implementation might look
> like:
>
> ################################
> class RealFilesystem(object):
>     def __init__(self):
>         pass
>
>     def open(self, filename):
>         return open(filename, 'r')
>
>     def create(self, filename):
>         return open(filename, 'w')
> ################################
>
> where we're just delegating the methods here to use the built-in
> open() function from Python's standard library.
>
> If we need to construct an editor that works with the real file
> system, that's not too bad:
>
>     editor = Editor(filesystem=RealFilesystem())

I was already planning on designing a class to handle my program's
file I/O.  I will probably need to add an append method, too.  Thanks
for giving my a sound starting point!


> Now what about a test-friendly version of this?  This actually isn't
> bad either; we can make judicious use of the StringIO class, which
> represents in-memory streams:
>
> ###############################################
> from StringIO import StringIO
>
> class FakeFilesystem(object):
>     """Simulate a very simple filesystem."""
>     def __init__(self):
>         self.filecontents = {}
>
>     def _open_as_stringio(self, filename):
>         filelike = StringIO(self.filecontents[filename])
>         real_close = filelike.close
>         def wrapped_close():
>             self.filecontents[filename] = filelike.getvalue()
>             real_close()
>         filelike.close = wrapped_close
>         return filelike
>
>     def open(self, filename):
>         if filename in self.filecontents:
>             return self._open_as_stringio(filename)
>         else:
>             raise IOError, "Not found"
>
>     def create(self, filename):
>         self.filecontents[filename] = None
>         return self._open_as_stringio(filename)
> ################################################
>
> (I'm using Python 2.7; if you're on Python 3, substitute the initial
> import statement with "from io import StringIO").

I was just scanning the docs on io.  A note relevant to IOError:

"Changed in version 3.3: Operations that used to raise IOError now
raise OSError, since IOError is now an alias of OSError."


> This is a class that will look approximately like a filesystem,
> because we can "create" and "open" files, and it'll remember.  All of
> this is in-memory, taking advantage of the StringIO library.  The
> "tricky" part about this is that we need to watch when files close
> down, because then we have to record what the file looked like, so
> that next time we open the file, we can recall it.
>
>
> Let's see how this works:
>
> ################################
>>>> fs = FakeFilesystem()
>>>> fs.open('hello')
> Traceback (most recent call last):
>   File "<stdin>", line 1, in <module>
>   File "fake_filesystem.py", line 21, in open
>     raise IOError, "Not found"
> IOError: Not found
>>>> h = fs.create('hello')
>>>> h.write('hello world')
>>>> h.close()
>>>> h2 = fs.open('hello')
>>>> h2.read()
> 'hello world'
>>>> h2.close()
> ################################

I will need to read the io docs in detail, but I am wondering if the
"with open ..." context manager is still usable to handle simulated
file closing using this technique?

> So for our own unit tests, now we should be able to say something like this:
>
> ##################################################
>     def test_foobar(self):
>         fs = FakeFileSystem()
>         fs.filecontents['classifiers.txt'] = """
> something here to test what happens when classifiers exists.
> """
>         e = Editor(filesystem=fs)
>         # ... fill me in!
> ##################################################

You have given me a substantial hunk of meat to chew on, Danny!  Thank
you very much for your lucid explanation and examples!!  You have
given me a very solid starting point if I choose this route.  I may
very well need to experiment with all of the approaches mentioned in
this thread.  Much to learn!

-- 
boB


More information about the Tutor mailing list