2017/05/30

Unit test on python: How to mock out stdout and stderr

About

This article talks about how to mock out stdout and stderr in unit test by the help of python mock.

Prerequisites

pip is the easiest way to install mock.
pip install mock
Source codes can be found here

Basic usage

Trivial class

We need a trivial class for demonstration how to write unit tests with mock.
import sys

class Person(object):
    def speak(self, message):
        """Speak out given message in stdout

                Parameters:

                - `message`: A string going to speak

                Return:

                - `self`: This instance
               """
        sys.stdout.write('%s\n' % message)
        return self

    def yell(self, message):
        """Yell out given message in both stdout and stderr

                Parameters:

                - `message`: A string going to yell

                Return:

                - `self`: This instance
               """
        sys.stdout.write('%s\n' % message)
        sys.stderr.write('%s!\n' % message)
        return self
Then, we would like to test whether speak() and yell() have been print to stdout and stderr.

Test speak()

    @mock.patch('use_mock.person.sys.stdout')
    def test_speak(self, mockStdout):
        self.myPerson.speak('haha')
        mockStdout.write.assert_has_calls([
            mock.call('haha\n')
        ])
Let’s go through this snippet line by line.
@mock.patch('use_mock.person.sys.stdout')
This is a function decorator which mocks out sys.stdout inside module use_mock.person ONLY.
Mock out sys.stdout is critical when writing unit tests as it provides an easy way for us to manipulate outcomes of running them. For example, throwing a system exception like Out Of Memory etc. We can verify our codes can survive any specified strange scenarios as a result.
Back into our case, mocking sys.stdout allows us to capture any arguments passing to them an avoid them to be printed on console.
def test_speak(self, mockStdout):
Prefix test_ is required by the unittest library. Otherwise, it won’t count as a test method. Reference
mockStdout is the mock outcome from previous decorator.
        self.myPerson.speak('haha')
Runs speak() from the instance self.myPerson with argument haha.
Expect to see haha\n in sys.stdout.write after running this line.
        mockStdout.write.assert_has_calls([
            mock.call('haha\n')
        ])
Verify argument haha\n has been passed to sys.stdout.write.

Test yell()

    @mock.patch('use_mock.person.sys')
    def test_yell(self, mockSys):
        self.myPerson.yell('haha')
        mockSys.stdout.write.assert_has_calls([
            mock.call('haha\n')
        ])
        mockSys.stderr.write.assert_has_calls([
            mock.call('haha!\n')
        ])
Contents are almost the same with previous section. However, there are slightly differences.
  • We mock use_mock.person.sys instead of use_mock.person.sys.stdout
  • assert runs onmockSys.stdout and mockSys.stderr
The take away is that you can define how deep to mock by giving a concise path to patch().
In our case, we mock out sys directly as we need to test sys.stderr and sys.stdout.

Something extra

References

沒有留言:

張貼留言