1

I have problems to test exceptions which would be raised within a with in python 3.4. I just can't get the tests run for this peace of code:

import logging
...
class Foo(object):
    ...
    def foo(self, src, dst):
        try:
            with pysftp.Connection(self._host, username=self._username, password=self._password) as connection:
                connection.put(src, dst)
                connection.close()
        except (
                ConnectionException,
                CredentialException,
                SSHException,
                AuthenticationException,
                HostKeysException,
                PasswordRequiredException
        ) as e:
            self._log.error(e)

And this is how I want to test it:

import logging
...
class TestFoo(TestCase):
    @parameterized.expand([
        ('ConnectionException', ConnectionException),
        ('CredentialException', CredentialException),
        ('SSHException', SSHException),
        ('AuthenticationException', AuthenticationException),
        ('HostKeysException', HostKeysException),
        ('PasswordRequiredException', PasswordRequiredException),
    ])
    @patch('pysftp.Connection', spec_set=pysftp.Connection)
    def test_foo_exceptions(self, _, ex, sftp_mock):
        """
        NOTE: take a look at:
              http://stackoverflow.com/questions/37014904/mocking-python-class-in-unit-test-and-verifying-an-instance
              to get an understanding of __enter__ and __exit__
        """
        sftp_mock.return_value = Mock(
            spec=pysftp.Connection,
            side_effect=ex,
            __enter__ = lambda self: self,
            __exit__ = lambda *args: None
        )
        foo = Foo('host', 'user', 'pass', Mock(spec_set=logging.Logger))
        foo.foo('src', 'dst')
        self.assertEqual(foo._log.error.call_count, 1)

But it fails - output:

Failure
...
AssertionError: 0 != 1
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Lars Grundei
  • 101
  • 5
  • Do you mean 'exceptions in `pysftp.Connection` instantiation' or when calling `connection.something()`? – ForceBru Aug 09 '16 at 15:31
  • What do you mean by *with a `with`*? What raises the exception? The context manager you create for use in `with`? Or is the context manager supposed to handle an exception raised in the block managed by `with`? – Martijn Pieters Aug 09 '16 at 15:50
  • You are raising the exception as a side-effect of *creating* your context manager (the `pysftp.Connection(..)` call raises it). The `with` statement never gets to use it as a context manager, nor does the block managed by `with` ever get executed. So your code is correctly exercising the `try:...except ..` statement, but you don't need any context manager knowledge. What is going wrong then is how the logger is called it appears. – Martijn Pieters Aug 09 '16 at 15:55
  • What exactly does `Foo.__init__` do with the 4th positional argument? Is it called or just stored straight as `self._log`? – Martijn Pieters Aug 09 '16 at 16:02
  • @ForceBru I mean exception in pysftp.Connection – Lars Grundei Aug 09 '16 at 16:39
  • @MartijnPieters the 4th argument is just stored – Lars Grundei Aug 09 '16 at 16:39
  • @LarsGrundei: It turns out to be moot; see my answer. – Martijn Pieters Aug 09 '16 at 16:40

1 Answers1

1

Your sftp_mock.return_value object is never called, so the side_effect is never triggered and no exception is raised. It would only be called if the return value of pysftp.Connection(...) was itself called again.

Set the side effect directly on the mock:

sftp_mock.side_effect = ex

Note that now the pysftp.Connection(...) expression raises the exception and it no longer matters that the return value of that expression would have been used as a context manager in a with statement.

Note that your exceptions will complain about not getting any arguments; pass in instances of your exceptions, not the type:

@parameterized.expand([
    ('ConnectionException', ConnectionException('host', 1234)),
    # ... etc.
])
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • results in: `TypeError: __init__() missing 2 required positional arguments: 'host' and 'port'` – Lars Grundei Aug 09 '16 at 17:08
  • @LarsGrundei: ah, your *exception* raises that; you'll have to provide instances, not the class. You can reproduce that error with `raise pysftp.ConnectionException`, for example. – Martijn Pieters Aug 09 '16 at 17:53