47

Suppose I have a class in python set up like this.

from somewhere import sendmail

class MyClass:

    def __init__(self, **kargs):
        self.sendmail = kwargs.get("sendmail", sendmail)  #if we can't find it, use imported def

    def publish():

        #lots of irrelevant code
        #and then

        self.sendmail(mail_to, mail_from, subject, body, format= 'html')

So as you can see, I have sort of given myself the option to parameterize which function I use for self.sendmail

Now in the test file.

Class Tester():

    kwargs = {"sendmail": MagicMock(mail_from= None, mail_to= None, subject= None, body= None, format= None)}
    self.myclass = MyClass(**kwargs)

    ##later on
    def testDefaultEmailHeader():

        default_subject = "Hello World"
        self.myclass.publish()

        self.myclass.sendmail.assert_called()  #this is doing just fine
        self.myclass.sendmail.assert_called_with(default_subject)  #this is having issues

For some reason I am getting the error message

AssertionError: Expected call: mock('Hello World')
                Actual Call : mock('defaultmt', 'defaultmf', 'Hello World', 'default_body', format= 'html')

So basically, the assert is expecting sendmail to be called with only one variable, when it ends up being called with all 5. The thing is, I don't care about what the other 4 variables are! I just want to make sure it is called with the correct subject.

I tried the mock place holder ANY, and got the same thing

self.myclass.sendmail.assert_called_with(ANY, ANY, 'Hello World', ANY, ANY)

AssertionError: Expected call: mock(<ANY>, <ANY>, 'Hello World', <ANY>, <ANY>)
Actual Call : mock('defaultmt', 'defaultmf', 'Hello World', 'default_body, 'format= 'html') 

Really unsure on how to proceed with this one. Anyone have any advice if we only care about one of the variable and want to ignore the rest?

Zack
  • 13,454
  • 24
  • 75
  • 113
  • 1
    Did you mean `format='html'` instead of `'format='html'`, latter is ambiguous? Try `self.myclass.sendmail.assert_called_with(ANY, ANY, 'Hello World', ANY, format=ANY)` – alko Dec 06 '13 at 16:30

3 Answers3

51

If you're calling sendmail with a named parameter subject then it's better to check whether the named argument matches what you expect:

args, kwargs = self.myclass.sendmail.call_args
self.assertEqual(kwargs['subject'], "Hello World")

This does assume both implementations of sendmail have a named parameter called subject. If that's not the case you can do the same with a positional parameter:

args, kwargs = self.myclass.sendmail.call_args
self.assertTrue("Hello World" in args)

You can be explicit about the position of the argument (i.e., the first argument or the third argument that's passed to sendmail but that depends on the implementation of sendmail being tested).

Simeon Visser
  • 118,920
  • 18
  • 185
  • 180
  • 1
    In the first example your MagicMock has five named parameters while you're passing one positional argument so it won't know that your argument is actually the third named parameter (subject). In the second example you're using the ANY placeholder but I haven't used that thus far. – Simeon Visser Dec 06 '13 at 17:14
  • This does not answer the question. In many cases (e.g. checking if a logger gets called, but not making the test verify a particular log message string), you need to check if a mock has been called, but you do not care (and may not know) with what arguments it has been called. – ely Nov 10 '16 at 16:42
  • The question states that one argument is relevant for the test, it's not just about asserting any logging call. – Simeon Visser Nov 10 '16 at 20:09
21

Using ANY from unittest.mock, wild cards are possible with assert_called_with:

from unittest.mock import ANY

self.myclass.sendmail.assert_called_with(
    subject="Hello World",
    mail_from=ANY,
    mail_to=ANY,
    body=ANY,
    format=ANY,
)

https://docs.python.org/3/library/unittest.mock.html#any

Markis
  • 1,703
  • 15
  • 14
  • this answer was the cleaner solution for my issue. interesting that none of the libraries haven't implemented it.. – aydow May 04 '20 at 03:57
  • Creative and pythonic – Freek Wiekmeijer Jun 24 '20 at 11:44
  • 1
    Great suggestion! Works perfectly when doing asserts of JSON/dicts, where some values are hard to determine (for example, some generated items IDs). I used this pattern to implement `AnyInt()` and `AnyISODate()` which made my Django/DRF unittest much cleaner. – Ihor Pomaranskyy Aug 02 '20 at 09:34
  • `unittest.mock` supports `ANY` since at least Python 3.5, which implements `__eq__` identically to this answer. See https://docs.python.org/3/library/unittest.mock.html#any – Brendan Batliner Jan 26 '22 at 22:36
  • Thanks @BrendanBatliner, I updated it. – Markis Jan 27 '22 at 22:37
1
args, _ = your_mock.call_args
assert "must-exist-string" in str(args)
kujiy
  • 5,833
  • 1
  • 29
  • 35