testtools-dev team mailing list archive
-
testtools-dev team
-
Mailing list archive
-
Message #00911
[Merge] lp:~lifeless/testtools/matchers into lp:testtools
Robert Collins has proposed merging lp:~lifeless/testtools/matchers into lp:testtools.
Requested reviews:
testtools committers (testtools-committers)
For more details, see:
https://code.launchpad.net/~lifeless/testtools/matchers/+merge/71477
Migrate all the implementations of assert* to be matcher based. Add additional matchers (and tweak existing ones [compatibly] as needed).
Also changed assertThat to take a message; we haven't done a release with verbose= yet, so I put the parameter in in the 'natural' place.
--
https://code.launchpad.net/~lifeless/testtools/matchers/+merge/71477
Your team testtools developers is subscribed to branch lp:testtools.
=== modified file 'NEWS'
--- NEWS 2011-08-09 13:12:39 +0000
+++ NEWS 2011-08-14 12:18:23 +0000
@@ -34,6 +34,9 @@
tells us to display. Old-style verbose output can be had by passing
``verbose=True`` to assertThat. (Jonathan Lange, #675323, #593190)
+* assertThat accepts a message which will be used to annotate the matcher. This
+ can be given as a third parameter or as a keyword parameter. (Robert Collins)
+
* Automated the Launchpad part of the release process.
(Jonathan Lange, #623486)
@@ -62,6 +65,9 @@
* ``MatchesException`` now allows you to match exceptions against any matcher,
rather than just regular expressions. (Jonathan Lange, #791889)
+* ``MatchesException`` now permits a tuple of types rather than a single type
+ (when using the type matching mode). (Robert Collins)
+
* ``MatchesStructure.byEquality`` added to make the common case of matching
many attributes by equality much easier. ``MatchesStructure.byMatcher``
added in case folk want to match by things other than equality.
@@ -75,6 +81,8 @@
* ``AllMatch`` matches many values against a single matcher.
(Jonathan Lange, #615108)
+ * ``Contains``. (Robert Collins)
+
* ``GreaterThan``. (Christian Kampka)
* New helper, ``safe_hasattr`` added. (Jonathan Lange)
=== modified file 'doc/for-test-authors.rst'
--- doc/for-test-authors.rst 2011-07-27 20:21:53 +0000
+++ doc/for-test-authors.rst 2011-08-14 12:18:23 +0000
@@ -312,6 +312,17 @@
self.assertThat(foo, Is(foo))
+Is
+~~~
+
+Adapts isinstance() to use as a matcher. For example::
+
+ def test_isinstance_example(self):
+ class MyClass:pass
+ self.assertThat(MyClass(), IsInstance(MyClass))
+ self.assertThat(MyClass(), IsInstance(MyClass, str))
+
+
The raises helper
~~~~~~~~~~~~~~~~~
@@ -374,6 +385,16 @@
self.assertThat('underground', EndsWith('und'))
+Contains
+~~~~~~~~
+
+This matcher checks to see if the given thing contains the thing in the
+matcher. For example::
+
+ def test_contains_example(self):
+ self.assertThat('abc', Contains('b'))
+
+
MatchesException
~~~~~~~~~~~~~~~~
@@ -474,6 +495,11 @@
def test_annotate_example_2(self):
self.assertThat("orange", PoliticallyEquals("yellow"))
+You can have assertThat perform the annotation for you as a convenience::
+
+ def test_annotate_example_3(self):
+ self.assertThat("orange", Equals("yellow"), "Death to the aristos!")
+
AfterPreprocessing
~~~~~~~~~~~~~~~~~~
=== modified file 'testtools/matchers.py'
--- testtools/matchers.py 2011-08-08 11:14:01 +0000
+++ testtools/matchers.py 2011-08-14 12:18:23 +0000
@@ -15,11 +15,13 @@
'AfterPreprocessing',
'AllMatch',
'Annotate',
+ 'Contains',
'DocTestMatches',
'EndsWith',
'Equals',
'GreaterThan',
'Is',
+ 'IsInstance',
'KeysEqual',
'LessThan',
'MatchesAll',
@@ -242,6 +244,21 @@
return self.matcher._describe_difference(self.with_nl)
+class DoesNotContain(Mismatch):
+
+ def __init__(self, matchee, needle):
+ """Create a DoesNotContain Mismatch.
+
+ :param matchee: the object that did not contain needle.
+ :param needle: the needle that 'matchee' was expected to contain.
+ """
+ self.matchee = matchee
+ self.needle = needle
+
+ def describe(self):
+ return "%r not present in %r" % (self.needle, self.matchee)
+
+
class DoesNotStartWith(Mismatch):
def __init__(self, matchee, expected):
@@ -343,6 +360,42 @@
mismatch_string = 'is not'
+class IsInstance(object):
+ """Matcher that wraps isinstance."""
+
+ def __init__(self, *types):
+ self.types = tuple(types)
+
+ def __str__(self):
+ return "%s(%s)" % (self.__class__.__name__,
+ ', '.join(type.__name__ for type in self.types))
+
+ def match(self, other):
+ if isinstance(other, self.types):
+ return None
+ return NotAnInstance(other, self.types)
+
+
+class NotAnInstance(Mismatch):
+
+ def __init__(self, matchee, types):
+ """Create a NotAnInstance Mismatch.
+
+ :param matchee: the thing which is not an instance of any of types.
+ :param types: A tuple of the types which were expected.
+ """
+ self.matchee = matchee
+ self.types = types
+
+ def describe(self):
+ if len(self.types) == 1:
+ typestr = self.types[0].__name__
+ else:
+ typestr = 'any of (%s)' % ', '.join(type.__name__ for type in
+ self.types)
+ return "'%s' is not an instance of %s" % (self.matchee, typestr)
+
+
class LessThan(_BinaryComparison):
"""Matches if the item is less than the matchers reference object."""
@@ -449,7 +502,8 @@
:param exception: Either an exception instance or type.
If an instance is given, the type and arguments of the exception
are checked. If a type is given only the type of the exception is
- checked.
+ checked. If a tuple is given, then as with isinstance, any of the
+ types in the tuple matching is sufficient to match.
:param value_re: If 'exception' is a type, and the matchee exception
is of the right type, then match against this. If value_re is a
string, then assume value_re is a regular expression and match
@@ -461,7 +515,7 @@
if istext(value_re):
value_re = AfterPreproccessing(str, MatchesRegex(value_re), False)
self.value_re = value_re
- self._is_instance = type(self.expected) not in classtypes()
+ self._is_instance = type(self.expected) not in classtypes() + (tuple,)
def match(self, other):
if type(other) != tuple:
@@ -484,6 +538,29 @@
return "MatchesException(%s)" % repr(self.expected)
+class Contains(Matcher):
+ """Checks whether something is container in another thing."""
+
+ def __init__(self, needle):
+ """Create a Contains Matcher.
+
+ :param needle: the thing that needs to be contained by matchees.
+ """
+ self.needle = needle
+
+ def __str__(self):
+ return "Contains(%r)" % (self.needle,)
+
+ def match(self, matchee):
+ try:
+ if self.needle not in matchee:
+ return DoesNotContain(matchee, self.needle)
+ except TypeError:
+ # e.g. 1 in 2 will raise TypeError
+ return DoesNotContain(matchee, self.needle)
+ return None
+
+
class StartsWith(Matcher):
"""Checks whether one string starts with another."""
@@ -613,16 +690,19 @@
# Catch all exceptions: Raises() should be able to match a
# KeyboardInterrupt or SystemExit.
except:
+ exc_info = sys.exc_info()
if self.exception_matcher:
- mismatch = self.exception_matcher.match(sys.exc_info())
+ mismatch = self.exception_matcher.match(exc_info)
if not mismatch:
+ del exc_info
return
else:
mismatch = None
# The exception did not match, or no explicit matching logic was
# performed. If the exception is a non-user exception (that is, not
# a subclass of Exception on Python 2.5+) then propogate it.
- if isbaseexception(sys.exc_info()[1]):
+ if isbaseexception(exc_info[1]):
+ del exc_info
raise
return mismatch
=== modified file 'testtools/testcase.py'
--- testtools/testcase.py 2011-07-27 19:47:22 +0000
+++ testtools/testcase.py 2011-08-14 12:18:23 +0000
@@ -27,10 +27,14 @@
from testtools.compat import advance_iterator
from testtools.matchers import (
Annotate,
+ Contains,
Equals,
+ MatchesAll,
MatchesException,
Is,
+ IsInstance,
Not,
+ Raises,
)
from testtools.monkey import patch
from testtools.runtest import RunTest
@@ -304,16 +308,14 @@
:param observed: The observed value.
:param message: An optional message to include in the error.
"""
- matcher = Annotate.if_message(message, Equals(expected))
- self.assertThat(observed, matcher)
+ matcher = Equals(expected)
+ self.assertThat(observed, matcher, message)
failUnlessEqual = assertEquals = assertEqual
def assertIn(self, needle, haystack):
"""Assert that needle is in haystack."""
- # XXX: Re-implement with matchers.
- if needle not in haystack:
- self.fail('%r not in %r' % (needle, haystack))
+ self.assertThat(haystack, Contains(needle))
def assertIsNone(self, observed, message=''):
"""Assert that 'observed' is equal to None.
@@ -321,8 +323,8 @@
:param observed: The observed value.
:param message: An optional message describing the error.
"""
- matcher = Annotate.if_message(message, Is(None))
- self.assertThat(observed, matcher)
+ matcher = Is(None)
+ self.assertThat(observed, matcher, message)
def assertIsNotNone(self, observed, message=''):
"""Assert that 'observed' is not equal to None.
@@ -330,8 +332,8 @@
:param observed: The observed value.
:param message: An optional message describing the error.
"""
- matcher = Annotate.if_message(message, Not(Is(None)))
- self.assertThat(observed, matcher)
+ matcher = Not(Is(None))
+ self.assertThat(observed, matcher, message)
def assertIs(self, expected, observed, message=''):
"""Assert that 'expected' is 'observed'.
@@ -340,33 +342,25 @@
:param observed: The observed value.
:param message: An optional message describing the error.
"""
- # XXX: Re-implement with matchers.
- if message:
- message = ': ' + message
- if expected is not observed:
- self.fail('%r is not %r%s' % (expected, observed, message))
+ matcher = Is(expected)
+ self.assertThat(observed, matcher, message)
def assertIsNot(self, expected, observed, message=''):
"""Assert that 'expected' is not 'observed'."""
- # XXX: Re-implement with matchers.
- if message:
- message = ': ' + message
- if expected is observed:
- self.fail('%r is %r%s' % (expected, observed, message))
+ matcher = Not(Is(expected))
+ self.assertThat(observed, matcher, message)
def assertNotIn(self, needle, haystack):
"""Assert that needle is not in haystack."""
- # XXX: Re-implement with matchers.
- if needle in haystack:
- self.fail('%r in %r' % (needle, haystack))
+ matcher = Not(Contains(needle))
+ self.assertThat(haystack, matcher)
def assertIsInstance(self, obj, klass, msg=None):
- # XXX: Re-implement with matchers.
- if msg is None:
- msg = '%r is not an instance of %s' % (
- obj, self._formatTypes(klass))
- if not isinstance(obj, klass):
- self.fail(msg)
+ if isinstance(klass, tuple):
+ matcher = IsInstance(*klass)
+ else:
+ matcher = IsInstance(klass)
+ self.assertThat(obj, matcher, msg)
def assertRaises(self, excClass, callableObj, *args, **kwargs):
"""Fail unless an exception of class excClass is thrown
@@ -376,17 +370,22 @@
deemed to have suffered an error, exactly as for an
unexpected exception.
"""
- # XXX: Re-implement with matchers.
- try:
- ret = callableObj(*args, **kwargs)
- except excClass:
- return sys.exc_info()[1]
- else:
- excName = self._formatTypes(excClass)
- self.fail("%s not raised, %r returned instead." % (excName, ret))
+ class ReRaiseOtherTypes(object):
+ def match(self, matchee):
+ if not issubclass(matchee[0], excClass):
+ raise matchee[0], matchee[1], matchee[2]
+ class CaptureMatchee(object):
+ def match(self, matchee):
+ self.matchee = matchee[1]
+ capture = CaptureMatchee()
+ matcher = Raises(MatchesAll(ReRaiseOtherTypes(),
+ MatchesException(excClass), capture))
+
+ self.assertThat(lambda:callableObj(*args, **kwargs), matcher)
+ return capture.matchee
failUnlessRaises = assertRaises
- def assertThat(self, matchee, matcher, verbose=False):
+ def assertThat(self, matchee, matcher, message='', verbose=False):
"""Assert that matchee is matched by matcher.
:param matchee: An object to match with matcher.
@@ -395,6 +394,7 @@
"""
# XXX: Should this take an optional 'message' parameter? Would kind of
# make sense. The hamcrest one does.
+ matcher = Annotate.if_message(message, matcher)
mismatch = matcher.match(matchee)
if not mismatch:
return
@@ -433,6 +433,7 @@
be removed. This separation preserves the original intent of the test
while it is in the expectFailure mode.
"""
+ # TODO: implement with matchers.
self._add_reason(reason)
try:
predicate(*args, **kwargs)
=== modified file 'testtools/tests/test_matchers.py'
--- testtools/tests/test_matchers.py 2011-08-08 11:14:01 +0000
+++ testtools/tests/test_matchers.py 2011-08-14 12:18:23 +0000
@@ -19,6 +19,7 @@
AllMatch,
Annotate,
AnnotatedMismatch,
+ Contains,
Equals,
DocTestMatches,
DoesNotEndWith,
@@ -26,6 +27,7 @@
EndsWith,
KeysEqual,
Is,
+ IsInstance,
LessThan,
GreaterThan,
MatchesAny,
@@ -179,6 +181,26 @@
describe_examples = [("1 is not 2", 2, Is(1))]
+class TestIsInstanceInterface(TestCase, TestMatchersInterface):
+
+ class Foo:pass
+
+ matches_matcher = IsInstance(Foo)
+ matches_matches = [Foo()]
+ matches_mismatches = [object(), 1, Foo]
+
+ str_examples = [
+ ("IsInstance(str)", IsInstance(str)),
+ ("IsInstance(str, int)", IsInstance(str, int)),
+ ]
+
+ describe_examples = [
+ ("'foo' is not an instance of int", 'foo', IsInstance(int)),
+ ("'foo' is not an instance of any of (int, type)", 'foo',
+ IsInstance(int, type)),
+ ]
+
+
class TestLessThanInterface(TestCase, TestMatchersInterface):
matches_matcher = LessThan(4)
@@ -211,6 +233,20 @@
]
+class TestContainsInterface(TestCase, TestMatchersInterface):
+
+ matches_matcher = Contains('foo')
+ matches_matches = ['foo', 'afoo', 'fooa']
+ matches_mismatches = ['f', 'fo', 'oo', 'faoo', 'foao']
+
+ str_examples = [
+ ("Contains(1)", Contains(1)),
+ ("Contains('foo')", Contains('foo')),
+ ]
+
+ describe_examples = [("1 not present in 2", 2, Contains(1))]
+
+
def make_error(type, *args, **kwargs):
try:
raise type(*args, **kwargs)
=== modified file 'testtools/tests/test_testcase.py'
--- testtools/tests/test_testcase.py 2011-07-26 23:48:48 +0000
+++ testtools/tests/test_testcase.py 2011-08-14 12:18:23 +0000
@@ -2,6 +2,7 @@
"""Tests for extensions to the base test library."""
+from doctest import ELLIPSIS
from pprint import pformat
import sys
import unittest
@@ -20,6 +21,8 @@
)
from testtools.compat import _b
from testtools.matchers import (
+ Annotate,
+ DocTestMatches,
Equals,
MatchesException,
Raises,
@@ -244,16 +247,8 @@
# assertRaises raises self.failureException when it's passed a
# callable that raises no error.
ret = ('orange', 42)
- try:
- self.assertRaises(RuntimeError, lambda: ret)
- except self.failureException:
- # We expected assertRaises to raise this exception.
- e = sys.exc_info()[1]
- self.assertEqual(
- '%s not raised, %r returned instead.'
- % (self._formatTypes(RuntimeError), ret), str(e))
- else:
- self.fail('Expected assertRaises to fail, but it did not.')
+ self.assertFails("<function <lambda> at ...> returned ('orange', 42)",
+ self.assertRaises, RuntimeError, lambda: ret)
def test_assertRaises_fails_when_different_error_raised(self):
# assertRaises re-raises an exception that it didn't expect.
@@ -298,15 +293,14 @@
failure = self.assertRaises(
self.failureException,
self.assertRaises, expectedExceptions, lambda: None)
- self.assertEqual(
- '%s not raised, None returned instead.'
- % self._formatTypes(expectedExceptions), str(failure))
+ self.assertFails('<function <lambda> at ...> returned None',
+ self.assertRaises, expectedExceptions, lambda: None)
def assertFails(self, message, function, *args, **kwargs):
"""Assert that function raises a failure with the given message."""
failure = self.assertRaises(
self.failureException, function, *args, **kwargs)
- self.assertEqual(message, str(failure))
+ self.assertThat(failure, DocTestMatches(message, ELLIPSIS))
def test_assertIn_success(self):
# assertIn(needle, haystack) asserts that 'needle' is in 'haystack'.
@@ -317,9 +311,9 @@
def test_assertIn_failure(self):
# assertIn(needle, haystack) fails the test when 'needle' is not in
# 'haystack'.
- self.assertFails('3 not in [0, 1, 2]', self.assertIn, 3, [0, 1, 2])
+ self.assertFails('3 not present in [0, 1, 2]', self.assertIn, 3, [0, 1, 2])
self.assertFails(
- '%r not in %r' % ('qux', 'foo bar baz'),
+ '%r not present in %r' % ('qux', 'foo bar baz'),
self.assertIn, 'qux', 'foo bar baz')
def test_assertNotIn_success(self):
@@ -331,9 +325,10 @@
def test_assertNotIn_failure(self):
# assertNotIn(needle, haystack) fails the test when 'needle' is in
# 'haystack'.
- self.assertFails('3 in [1, 2, 3]', self.assertNotIn, 3, [1, 2, 3])
+ self.assertFails('[1, 2, 3] matches Contains(3)', self.assertNotIn,
+ 3, [1, 2, 3])
self.assertFails(
- '%r in %r' % ('foo', 'foo bar baz'),
+ "'foo bar baz' matches Contains('foo')",
self.assertNotIn, 'foo', 'foo bar baz')
def test_assertIsInstance(self):
@@ -367,7 +362,7 @@
"""Simple class for testing assertIsInstance."""
self.assertFails(
- '42 is not an instance of %s' % self._formatTypes(Foo),
+ "'42' is not an instance of %s" % self._formatTypes(Foo),
self.assertIsInstance, 42, Foo)
def test_assertIsInstance_failure_multiple_classes(self):
@@ -381,12 +376,13 @@
"""Another simple class for testing assertIsInstance."""
self.assertFails(
- '42 is not an instance of %s' % self._formatTypes([Foo, Bar]),
+ "'42' is not an instance of any of (%s)" % self._formatTypes([Foo, Bar]),
self.assertIsInstance, 42, (Foo, Bar))
def test_assertIsInstance_overridden_message(self):
# assertIsInstance(obj, klass, msg) permits a custom message.
- self.assertFails("foo", self.assertIsInstance, 42, str, "foo")
+ self.assertFails("'42' is not an instance of str: foo",
+ self.assertIsInstance, 42, str, "foo")
def test_assertIs(self):
# assertIs asserts that an object is identical to another object.
@@ -418,16 +414,17 @@
def test_assertIsNot_fails(self):
# assertIsNot raises assertion errors if one object is identical to
# another.
- self.assertFails('None is None', self.assertIsNot, None, None)
+ self.assertFails('None matches Is(None)', self.assertIsNot, None, None)
some_list = [42]
self.assertFails(
- '[42] is [42]', self.assertIsNot, some_list, some_list)
+ '[42] matches Is([42])', self.assertIsNot, some_list, some_list)
def test_assertIsNot_fails_with_message(self):
# assertIsNot raises assertion errors if one object is identical to
# another, and includes a user-supplied message if it's provided.
self.assertFails(
- 'None is None: foo bar', self.assertIsNot, None, None, "foo bar")
+ 'None matches Is(None): foo bar', self.assertIsNot, None, None,
+ "foo bar")
def test_assertThat_matches_clean(self):
class Matcher(object):
@@ -468,6 +465,12 @@
expected = matcher.match(matchee).describe()
self.assertFails(expected, self.assertThat, matchee, matcher)
+ def test_assertThat_message_is_annotated(self):
+ matchee = 'foo'
+ matcher = Equals('bar')
+ expected = Annotate('woo', matcher).match(matchee).describe()
+ self.assertFails(expected, self.assertThat, matchee, matcher, 'woo')
+
def test_assertThat_verbose_output(self):
matchee = 'foo'
matcher = Equals('bar')
Follow ups