72

Rubyist writing Python here. I've got some code that looks kinda like this:

result = database.Query('complicated sql with an id: %s' % id)

database.Query is mocked out, and I want to test that the ID gets injected in correctly without hardcoding the entire SQL statement into my test. In Ruby/RR, I would have done this:

mock(database).query(/#{id}/)

But I can't see a way to set up a 'selective mock' like that in unittest.mock, at least without some hairy side_effect logic. So I tried using the regexp in the assertion instead:

with patch(database) as MockDatabase:
  instance = MockDatabase.return_value
  ...
  instance.Query.assert_called_once_with(re.compile("%s" % id))

But that doesn't work either. This approach does work, but it's ugly:

with patch(database) as MockDatabase:
  instance = MockDatabase.return_value
  ...
  self.assertIn(id, instance.Query.call_args[0][0])

Better ideas?

lambshaanxy
  • 22,552
  • 10
  • 68
  • 92

5 Answers5

107
import mock

class AnyStringWith(str):
    def __eq__(self, other):
        return self in other

...
result = database.Query('complicated sql with an id: %s' % id)
database.Query.assert_called_once_with(AnyStringWith(id))
...

Preemptively requires a matching string

def arg_should_contain(x):
    def wrapper(arg):
        assert str(x) in arg, "'%s' does not contain '%s'" % (arg, x)
    return wrapper

...
database.Query = arg_should_contain(id)
result = database.Query('complicated sql with an id: %s' % id)

UPDATE

Using libraries like callee, you don't need to implement AnyStringWith.

from callee import Contains

database.Query.assert_called_once_with(Contains(id))

https://callee.readthedocs.io/en/latest/reference/operators.html#callee.operators.Contains

falsetru
  • 357,413
  • 63
  • 732
  • 636
  • Not bad... but is there a way to set up the mock so it preemptively requires a matching string, instead of having to assert afterwards? – lambshaanxy Jun 11 '13 at 05:03
  • @jpatokal, added another version. – falsetru Jun 11 '13 at 06:47
  • Looks like your new version is effectively your own implementation of mocking? Not that that's necessarily wrong, I just continue to be surprised that unittest.mock doesn't do this kind of thing... – lambshaanxy Jun 12 '13 at 04:05
  • 1
    Solution with __eq__ overriding is cool, but relies on "small trick". I wonder if there's any better solution out of the box... but so far this one looks the best. – Marek Lewandowski Dec 04 '14 at 20:59
  • that's what we usually do; does anybody know a library that implements a bunch of these? like `AnyStringWith`, `StartsWith`, `OneOf` etc? – RomanI Nov 13 '15 at 16:50
  • 3
    Maybe a little late to the party, but I just published a library that's exactly this :) Hence a shameless plug: https://github.com/Xion/callee – Xion Mar 21 '16 at 03:43
  • I would recommend raising AssertionError over returning False from the matchers __eq__ method to allow writing a custom error message – tkruse Jun 19 '19 at 02:12
  • @tkruse, `wrapper` in `arg_should_contain` use `aasert` which will raise AssertionError. – falsetru Jun 19 '19 at 23:54
  • not sure how that helps when only using assert_called_once_with – tkruse Jun 20 '19 at 01:19
  • @tkruse, I suggested `arg_should_contain` as a alternative to `assert_called_once_with` + `AnyStringWith`. – falsetru Jun 20 '19 at 01:49
  • sure, but sometimes people need to use assert_called..._with(Matcher), and in those cases it is better if the matcher raises an error than returning false. you arg_should_contain solution is fine. – tkruse Jun 20 '19 at 02:22
  • @tkruse, `__eq_-` is expected to return True / False, not raising an exception. [Python data model documentation](https://docs.python.org/3/reference/datamodel.html#object.__eq__) – falsetru Jun 20 '19 at 08:51
  • The documentation to which you link does not confirm your claim. It says explicitly "However, these methods can return any value..." And does not forbid exceptions. – tkruse Jun 20 '19 at 15:15
  • @tkruse, `return` is not `raise` ;) – falsetru Jun 20 '19 at 15:23
  • The documentation also says: __eq__() and __ne__() are their own reflection. That means a.__eq__(b) = b.__eq__(a). I don't think using Matchers can stay consistent with that requirement anyway. – tkruse Jun 20 '19 at 15:26
  • @tkruse, Try `AnyStringWith() == 'any-string-you-want'` and `'any-string-you-want' == AnyStringWith()`. (https://ideone.com/EdrISH) Seems I don't get your point. – falsetru Jun 20 '19 at 15:32
  • Try `AnyStringWith('ny') == AnyStringWith('any')` and inverse – tkruse Jun 20 '19 at 16:18
33

You can just use unittest.mock.ANY :)

from unittest.mock import Mock, ANY

def foo(some_string):
    print(some_string)

foo = Mock()
foo("bla")
foo.assert_called_with(ANY)

As described here - https://docs.python.org/3/library/unittest.mock.html#any

Kfir Eisner
  • 493
  • 5
  • 3
6

You can use match_equality from PyHamcrest library to wrap the matches_regexp matcher from the same library:

from hamcrest.library.integration import match_equality

with patch(database) as MockDatabase:
  instance = MockDatabase.return_value
  ...
  expected_arg = matches_regexp(id)
  instance.Query.assert_called_once_with(match_equality(expected_arg))

This method is mentioned also in Python's unittest.mock documentation:

As of version 1.5, the Python testing library PyHamcrest provides similar functionality, that may be useful here, in the form of its equality matcher (hamcrest.library.integration.match_equality).

If you don't want to use PyHamcrest, the documentation linked above also shows how to write a custom matcher by defining a class with an __eq__ method (as suggested in falsetrus answer):

class Matcher:
    def __init__(self, compare, expected):
        self.compare = compare
        self.expected = expected

    def __eq__(self, actual):
        return self.compare(self.expected, actual)

match_foo = Matcher(compare, Foo(1, 2))
mock.assert_called_with(match_foo)

You could replace the call to self.compare here with your own regex matching and return False if none found or raise an AssertionError with a descriptive error message of your choice.

saaskis
  • 191
  • 3
  • 9
1

The chosen answer is absolutely wonderful.

However, the original question seemed to want to match on the basis of a regex. I offer the following, which I would never have been able to devise without falsetru's chosen answer:

class AnyStringWithRegex(str):
    def __init__(self, case_insensitive=True):
        self.case_insensitive = case_insensitive
    def __eq__(self, other):
        if self.case_insensitive:
            return len(re.findall(self.lower(), other.lower(), re.DOTALL)) != 0
        return len(re.findall(self, other, re.DOTALL)) != 0

No doubt many variations on this theme are possible. This compares two objects on the basis of specified attributes:

class AnyEquivalent():
    # compares two objects on basis of specified attributes
    def __init__(self, compared_object, *attrs):
        self.compared_object = compared_object
        self.attrs = attrs
        
    def __eq__(self, other):
        equal_objects = True
        for attr in self.attrs:
            if hasattr(other, attr):
                if getattr(self.compared_object, attr) != getattr(other, attr):
                    equal_objects = False
                    break
            else:
                equal_objects = False
                break
        return equal_objects

For example, this fails even when the file is correct (slightly confusingly, as the error message says the f values are the same in terms of their str(f) output). The explanation being that the two file objects are different ones:

f = open(FILENAME, 'w')
mock_run.assert_called_once_with(['pip', 'freeze'], stdout=f)

But this passes (explicitly comparing only on the basis of the values of the specified 3 attributes):

f = open(FILENAME, 'w')
mock_run.assert_called_once_with(['pip', 'freeze'], stdout=AnyEquivalent(f, 'name', 'mode', 'encoding'))
mike rodent
  • 14,126
  • 11
  • 103
  • 157
-3

I always write my unit tests so they reflect the 'real world'. I don't really know what you want to test except for the ID gets injected in correctly.

I don't know what the database.Query is supposed to do, but I guess it's supposed to create a query object you can call or pass to a connection later?

The best way you can test this to take a real world example. Doing something simple like checking if the id occurs in the query is too error prone. I often see people wanting to do magic stuff in their unit tests, this always leads to problems. Keep your unit tests simple and static. In your case you could do:

class QueryTest(unittest.TestCase):
    def test_insert_id_simple(self):
        expected = 'a simple query with an id: 2'
        query = database.Query('a simple query with an id: %s' % 2)
        self.assertEqual(query, expected)

    def test_insert_id_complex(self):
        expected = 'some complex query with an id: 6'
        query = database.Query('some complex query with an id: %s' 6)
        self.assertEqual(query, expected)

If database.Query directly executes a query in the database, you might want to consider using something like database.query or database.execute instead. The capital in the Query implies you create an object if it's all lowercase it implies you call a function. It's more a naming convention and my opinion, but I'm just throwing it out there. ;-)

If the database.Query directly queries you can best patch the method it is calling. For example, if it looks like this:

def Query(self, query):
    self.executeSQL(query)
    return query

You can use mock.patch to prevent the unit test from going to the database:

@mock.patch('database.executeSQL')
def test_insert_id_simple(self, mck):
    expected = 'a simple query with an id: 2'
    query = database.Query('a simple query with an id: %s' % 2)
    self.assertEqual(query, expected)

As an extra tip, try to use the str.format method. The % formatting may go away in the future. See this question for more info.

I also cannot help but feel testing string formatting is redundant. If 'test %s' % 'test' doesn't work it would mean something is wrong with Python. It would only make sense if you wanted to test custom query building. e.g. inserting strings should be quoted, numbers shouldn't, escape special characters, etc.

Community
  • 1
  • 1
siebz0r
  • 18,867
  • 14
  • 64
  • 107
  • 3
    This is a unit test, not an integration test: I care that the ID has been passed into the method call correctly. (The sample has been simplified, there's more than just a string substitution going on in the real thing.) What the called method does internally does not belong at this level of test, and (IMHO) it's bad form to start patching implementation details of other libraries -- if I want a 'real world' test, I'll write an integration test that goes through the whole stack and doesn't mock out bits in the middle. – lambshaanxy Jun 12 '13 at 04:03