pythonpython-3.xtddpytestpython-cmd

Create automated tests for interactive shell based on Python's cmd module


I am building an interactive shell using Python 3 and the cmd module. I have already written simple unit tests using py.test to test the individual functions, such as the do_* functions. I'd like to create more comprehensive tests that actually interact with the shell itself by simulating a user's input. For example, how could I test the following simulated session:

bash$ console-app.py
md:> show options
  Available Options:
  ------------------
  HOST      The IP address or hostname of the machine to interact with
  PORT      The TCP port number of the server on the HOST
md:> set HOST localhost
  HOST => 'localhost'
md:> set PORT 2222
  PORT => '2222'
md:>

Solution

  • You can mock input or input stream passed to cmd to inject user input but I find more simple and flexible test it by onecmd() Cmd API method and trust how Cmd read input. In this way you cannot care how Cmd do the dirty work and test directly by users command: I use cmd both by console and socket and this I cannot care where the stream come from.

    Moreover I use onecmd() to test even do_* (and occasionally help_*) methods and make my test less coupled to the code.

    Follow a simple example of how I use it. create() and _last_write() are helper methods to build a MyCLI instance and take the last output lines respectively.

    from mymodule import MyCLI
    from unittest.mock import create_autospec
    
    class TestMyCLI(unittest.TestCase):
        def setUp(self):
            self.mock_stdin = create_autospec(sys.stdin)
            self.mock_stdout = create_autospec(sys.stdout)
    
        def create(self, server=None):
            return MyCLI(stdin=self.mock_stdin, stdout=self.mock_stdout)
    
        def _last_write(self, nr=None):
            """:return: last `n` output lines"""
            if nr is None:
                return self.mock_stdout.write.call_args[0][0]
            return "".join(map(lambda c: c[0][0], self.mock_stdout.write.call_args_list[-nr:]))
    
        def test_active(self):
            """Tesing `active` command"""
            cli = self.create()
            self.assertFalse(cli.onecmd("active"))
            self.assertTrue(self.mock_stdout.flush.called)
            self.assertEqual("Autogain active=False\n", self._last_write())
            self.mock_stdout.reset_mock()
            self.assertFalse(cli.onecmd("active TRue"))
            self.assertTrue(self.mock_stdout.flush.called)
            self.assertEqual("Autogain active=True\n", self._last_write())
            self.assertFalse(cli.onecmd("active 0"))
            self.assertTrue(self.mock_stdout.flush.called)
            self.assertEqual("Autogain active=False\n", self._last_write())
    
        def test_exit(self):
            """exit command"""
            cli = self.create()
            self.assertTrue(cli.onecmd("exit"))
            self.assertEqual("Goodbay\n", self._last_write())
    

    Take care that onecmd() return True if your cli should terminate, False otherwise.