← Back to team overview

testtools-dev team mailing list archive

[Merge] lp:~jml/testtools/matcher-fixes into lp:testtools

 

Jonathan Lange has proposed merging lp:~jml/testtools/matcher-fixes into lp:testtools.

Requested reviews:
  testtools developers (testtools-dev)


I was hacking on something else in testtools and wanted to do assertThat(foo, Is(bar)). So I wrote a matcher for Is.

In the process of which, I discovered that we have a bug where assertThat will blow up if the matcher fails, since none of our Mismatch objects provide get_details(). :(

I've fixed this bug, added tests for it, added an Is matcher and done some refactoring to make Equals, NotEquals and Is all pretty much the same in terms of implementation.

-- 
https://code.launchpad.net/~jml/testtools/matcher-fixes/+merge/31663
Your team testtools developers is requested to review the proposed merge of lp:~jml/testtools/matcher-fixes into lp:testtools.
=== modified file 'NEWS'
--- NEWS	2010-08-02 10:38:54 +0000
+++ NEWS	2010-08-03 17:11:07 +0000
@@ -10,6 +10,12 @@
  * jml added a built-in UTF8_TEXT ContentType to make it slightly easier to
    add details to test results.
 
+ * Fix a bug in our built-in matchers where assertThat would blow up if any
+   of them failed. All built-in mismatch objects now provide get_details().
+
+ * New 'Is' matcher, which lets you assert that a thing is identical to
+   another thing.
+
 0.9.5
 ~~~~~
 

=== modified file 'testtools/matchers.py'
--- testtools/matchers.py	2010-07-29 12:20:37 +0000
+++ testtools/matchers.py	2010-08-03 17:11:07 +0000
@@ -15,6 +15,7 @@
     'Annotate',
     'DocTestMatches',
     'Equals',
+    'Is',
     'MatchesAll',
     'MatchesAny',
     'NotEquals',
@@ -22,6 +23,7 @@
     ]
 
 import doctest
+import operator
 
 
 class Matcher(object):
@@ -120,7 +122,7 @@
         return self._checker.output_difference(self, with_nl, self.flags)
 
 
-class DocTestMismatch(object):
+class DocTestMismatch(Mismatch):
     """Mismatch object for DocTestMatches."""
 
     def __init__(self, matcher, with_nl):
@@ -131,60 +133,59 @@
         return self.matcher._describe_difference(self.with_nl)
 
 
-class Equals(object):
+class _BinaryComparison(object):
+    """Matcher that compares an object to another object."""
+
+    def __init__(self, expected):
+        self.expected = expected
+
+    def __str__(self):
+        return "%s(%r)" % (self.__class__.__name__, self.expected)
+
+    def match(self, other):
+        if self.comparator(self.expected, other):
+            return None
+        return _BinaryMismatch(self.expected, self.mismatch_string, other)
+
+    def comparator(self, expected, other):
+        raise NotImplementedError(self.comparator)
+
+
+class _BinaryMismatch(Mismatch):
+    """Two things did not match."""
+
+    def __init__(self, expected, mismatch_string, other):
+        self.expected = expected
+        self._mismatch_string = mismatch_string
+        self.other = other
+
+    def describe(self):
+        return "%r %s %r" % (self.expected, self._mismatch_string, self.other)
+
+
+class Equals(_BinaryComparison):
     """Matches if the items are equal."""
 
-    def __init__(self, expected):
-        self.expected = expected
-
-    def match(self, other):
-        if self.expected == other:
-            return None
-        return EqualsMismatch(self.expected, other)
-
-    def __str__(self):
-        return "Equals(%r)" % self.expected
-
-
-class EqualsMismatch(object):
-    """Two things differed."""
-
-    def __init__(self, expected, other):
-        self.expected = expected
-        self.other = other
-
-    def describe(self):
-        return "%r != %r" % (self.expected, self.other)
-
-
-class NotEquals(object):
+    comparator = operator.eq
+    mismatch_string = '!='
+
+
+class NotEquals(_BinaryComparison):
     """Matches if the items are not equal.
 
     In most cases, this is equivalent to `Not(Equals(foo))`. The difference
     only matters when testing `__ne__` implementations.
     """
 
-    def __init__(self, expected):
-        self.expected = expected
-
-    def __str__(self):
-        return 'NotEquals(%r)' % (self.expected,)
-
-    def match(self, other):
-        if self.expected != other:
-            return None
-        return NotEqualsMismatch(self.expected, other)
-
-
-class NotEqualsMismatch(object):
-    """Two things are the same."""
-
-    def __init__(self, expected, other):
-        self.expected = expected
-        self.other = other
-
-    def describe(self):
-        return '%r == %r' % (self.expected, self.other)
+    comparator = operator.ne
+    mismatch_string = '=='
+
+
+class Is(_BinaryComparison):
+    """Matches if the items are identical."""
+
+    comparator = operator.is_
+    mismatch_string = 'is not'
 
 
 class MatchesAny(object):
@@ -228,7 +229,7 @@
             return None
 
 
-class MismatchesAll(object):
+class MismatchesAll(Mismatch):
     """A mismatch with many child mismatches."""
 
     def __init__(self, mismatches):
@@ -259,7 +260,7 @@
             return None
 
 
-class MatchedUnexpectedly(object):
+class MatchedUnexpectedly(Mismatch):
     """A thing matched when it wasn't supposed to."""
 
     def __init__(self, matcher, other):
@@ -289,7 +290,7 @@
             return AnnotatedMismatch(self.annotation, mismatch)
 
 
-class AnnotatedMismatch(object):
+class AnnotatedMismatch(Mismatch):
     """A mismatch annotated with a descriptive string."""
 
     def __init__(self, annotation, mismatch):

=== modified file 'testtools/tests/test_matchers.py'
--- testtools/tests/test_matchers.py	2010-07-29 12:20:37 +0000
+++ testtools/tests/test_matchers.py	2010-08-03 17:11:07 +0000
@@ -12,12 +12,16 @@
     Annotate,
     Equals,
     DocTestMatches,
+    Is,
     MatchesAny,
     MatchesAll,
     Not,
     NotEquals,
     )
 
+# Silence pyflakes.
+Matcher
+
 
 class TestMatchersInterface(object):
 
@@ -45,6 +49,15 @@
             mismatch = matcher.match(matchee)
             self.assertEqual(difference, mismatch.describe())
 
+    def test_mismatch_details(self):
+        # The mismatch object must provide get_details, which must return a
+        # dictionary mapping names to Content objects.
+        examples = self.describe_examples
+        for difference, matchee, matcher in examples:
+            mismatch = matcher.match(matchee)
+            details = mismatch.get_details()
+            self.assertEqual(dict(details), details)
+
 
 class TestDocTestMatchesInterface(TestCase, TestMatchersInterface):
 
@@ -97,6 +110,20 @@
     describe_examples = [("1 == 1", 1, NotEquals(1))]
 
 
+class TestIsInterface(TestCase, TestMatchersInterface):
+
+    foo = object()
+    bar = object()
+
+    matches_matcher = Is(foo)
+    matches_matches = [foo]
+    matches_mismatches = [bar, 1]
+
+    str_examples = [("Is(2)", Is(2))]
+
+    describe_examples = [("1 is not 2", 2, Is(1))]
+
+
 class TestNotInterface(TestCase, TestMatchersInterface):
 
     matches_matcher = Not(Equals(1))


Follow ups