├── .github └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.rst ├── examples └── examples.py ├── makefile ├── precisely ├── __init__.py ├── base.py ├── coercion.py ├── comparison_matchers.py ├── core_matchers.py ├── feature_matchers.py ├── function_matchers.py ├── hamcrest.py ├── iterable_matchers.py ├── mapping_matchers.py ├── object_matchers.py └── results.py ├── setup.cfg ├── setup.py ├── test-requirements.txt ├── tests ├── all_elements_tests.py ├── all_of_tests.py ├── any_of_tests.py ├── anything_tests.py ├── assert_that_tests.py ├── close_to_tests.py ├── contains_exactly_tests.py ├── contains_string_tests.py ├── equal_to_tests.py ├── has_attr_tests.py ├── has_attrs_tests.py ├── has_feature_tests.py ├── includes_tests.py ├── is_instance_tests.py ├── is_mapping_tests.py ├── is_sequence_tests.py ├── mapping_includes_tests.py ├── not_tests.py ├── numeric_comparison_tests.py ├── raises_tests.py ├── results_tests.py └── starts_with_tests.py └── tox.ini /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-20.04 8 | 9 | strategy: 10 | matrix: 11 | python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "pypy2.7", "pypy3.9"] 12 | 13 | steps: 14 | 15 | - uses: actions/checkout@v3 16 | 17 | - name: Use Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | 22 | - run: pip install tox 23 | 24 | - run: tox -e py 25 | 26 | docs: 27 | runs-on: ubuntu-20.04 28 | 29 | strategy: 30 | matrix: 31 | python-version: ["3.9"] 32 | 33 | steps: 34 | 35 | - uses: actions/checkout@v3 36 | 37 | - name: Use Python ${{ matrix.python-version }} 38 | uses: actions/setup-python@v4 39 | with: 40 | python-version: ${{ matrix.python-version }} 41 | 42 | - run: pip install tox 43 | 44 | - run: tox -e docs 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /_virtualenv 3 | /*.egg-info 4 | /.tox 5 | /MANIFEST 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Michael Williamson 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Precisely: better assertions for Python tests 2 | ============================================= 3 | 4 | Precisely allows you to write precise assertions so you only test the behaviour you're really interested in. 5 | This makes it clearer to the reader what the expected behaviour is, 6 | and makes tests less brittle. 7 | This also allows better error messages to be generated when assertions fail. 8 | Inspired by Hamcrest_. 9 | 10 | .. _Hamcrest: http://hamcrest.org 11 | 12 | For instance, suppose we want to make sure that a ``unique`` function removes duplicates from a list. 13 | We might write a test like so: 14 | 15 | .. code:: python 16 | 17 | from precisely import assert_that, contains_exactly 18 | 19 | def test_unique_removes_duplicates(): 20 | result = unique(["a", "a", "b", "a", "b"]) 21 | assert_that(result, contains_exactly("a", "b")) 22 | 23 | The assertion will pass so long as ``result`` contains ``"a"`` and ``"b"`` in any order, 24 | but no other items. 25 | Unlike, say, ``assert result == ["a", "b"]``, our assertion ignores the ordering of elements. 26 | This is useful when: 27 | 28 | * the ordering of the result is non-determistic, 29 | such as the results of SQL SELECT queries without an ORDER BY clause. 30 | 31 | * the ordering isn't specified in the contract of ``unique``. 32 | If we assert a particular ordering, then we'd be testing the implementation rather than the contract. 33 | 34 | * the ordering is specified in the contract of ``unique``, 35 | but the ordering is tested in a separate test case. 36 | 37 | When the assertion fails, 38 | rather than just stating the two values weren't equal, 39 | the error message will describe the failure in more detail. 40 | For instance, if ``result`` has the value ``["a", "a", "b"]``, 41 | we'd get the failure message:: 42 | 43 | Expected: iterable containing in any order: 44 | * 'a' 45 | * 'b' 46 | but: had extra elements: 47 | * 'a' 48 | 49 | Installation 50 | ------------ 51 | 52 | :: 53 | 54 | pip install precisely 55 | 56 | API 57 | --- 58 | 59 | Use ``assert_that(value, matcher)`` to assert that a value satisfies a matcher. 60 | 61 | Many matchers are composed of other matchers. 62 | If they are given a value instead of a matcher, 63 | then that value is wrapped in ``equal_to()``. 64 | For instance, ``has_attrs(name="bob")`` is equivalent to ``has_attrs(name=equal_to("bob"))``. 65 | 66 | * ``equal_to(value)``: matches a value if it is equal to ``value`` using ``==``. 67 | 68 | * ``has_attrs(**kwargs)``: matches a value if it has the specified attributes. 69 | For instance: 70 | 71 | .. code:: python 72 | 73 | assert_that(result, has_attrs(id=is_instance(int), name="bob")) 74 | 75 | * ``has_attr(attribute_name, matcher)``: matches a value if it has the specified attribute. 76 | Using ``has_attrs`` is generally considered more idiomatic when the attribute name is constant. 77 | For instance, instead of: 78 | 79 | .. code:: python 80 | 81 | assert_that(result, has_attr("id", is_instance(int))) 82 | 83 | use: 84 | 85 | .. code:: python 86 | 87 | assert_that(result, has_attrs(id=is_instance(int))) 88 | 89 | * ``contains_exactly(*args)``: matches an iterable if it has the same elements in any order. 90 | For instance: 91 | 92 | .. code:: python 93 | 94 | assert_that(result, contains_exactly("a", "b")) 95 | # Matches ["a", "b"] and ["b", "a"], 96 | # but not ["a", "a", "b"] nor ["a"] nor ["a", "b", "c"] 97 | 98 | * ``is_sequence(*args)``: matches an iterable if it has the same elements in the same order. 99 | For instance: 100 | 101 | .. code:: python 102 | 103 | assert_that(result, is_sequence("a", "b")) 104 | # Matches ["a", "b"] 105 | # but not ["b", "a"] nor ["a", "b", "c"] nor ["c", "a", "b"] 106 | 107 | * ``includes(*args)``: matches an iterable if it includes all of the elements. 108 | For instance: 109 | 110 | .. code:: python 111 | 112 | assert_that(result, includes("a", "b")) 113 | # Matches ["a", "b"], ["b", "a"] and ["a", "c", "b"] 114 | # but not ["a", "c"] nor ["a"] 115 | assert_that(result, includes("a", "a")) 116 | # Matches ["a", "a"] and ["a", "a", "a"] 117 | # but not ["a"] 118 | 119 | * ``all_elements(matcher)``: matches an iterable if every element matches `matcher`. 120 | For instance: 121 | 122 | .. code:: python 123 | 124 | assert_that(result, all_elements(equal_to(42))) 125 | # Matches [42], [42, 42, 42] and [] 126 | # but not [42, 43] 127 | 128 | * ``is_mapping(matchers)``: matches a mapping, such as a ``dict``, if it has the same keys with matching values. 129 | An error will be raised if the mapping is missing any keys, or has any extra keys. 130 | For instance: 131 | 132 | .. code:: python 133 | 134 | assert_that(result, is_mapping({ 135 | "a": equal_to(1), 136 | "b": equal_to(4), 137 | })) 138 | 139 | * ``mapping_includes(matchers)``: matches a mapping, such as a ``dict``, if it has the same keys with matching values. 140 | An error will be raised if the mapping is missing any keys, but extra keys are allowed. 141 | For instance: 142 | 143 | .. code:: python 144 | 145 | assert_that(result, mapping_includes({ 146 | "a": equal_to(1), 147 | "b": equal_to(4), 148 | })) 149 | # Matches {"a": 1, "b": 4} and {"a": 1, "b": 4, "c": 5} 150 | # but not {"a": 1} nor {"a": 1, "b": 5} 151 | 152 | * ``anything``: matches all values. 153 | 154 | * ``is_instance(type)``: matches any value where ``isinstance(value, type)``. 155 | 156 | * ``all_of(*matchers)``: matches a value if all sub-matchers match. 157 | For instance: 158 | 159 | .. code:: python 160 | 161 | assert_that(result, all_of( 162 | is_instance(User), 163 | has_attrs(name="bob"), 164 | )) 165 | 166 | * ``any_of(*matchers)``: matches a value if any sub-matcher matches. 167 | For instance: 168 | 169 | .. code:: python 170 | 171 | assert_that(result, any_of( 172 | equal_to("x=1, y=2"), 173 | equal_to("y=2, x=1"), 174 | )) 175 | 176 | * ``not_(matcher)``: negates a matcher. 177 | For instance: 178 | 179 | .. code:: python 180 | 181 | assert_that(result, not_(equal_to("hello"))) 182 | 183 | * ``starts_with(prefix)``: matches a string if it starts with ``prefix``. 184 | 185 | * ``contains_string(substring)``: matches a string if it contains ``substring``. 186 | 187 | * ``greater_than(value)``: matches values greater than ``value``. 188 | 189 | * ``greater_than_or_equal_to(value)``: matches values greater than or equal to ``value``. 190 | 191 | * ``less_than(value)``: matches values less than ``value``. 192 | 193 | * ``less_than_or_equal_to(value)``: matches values less than or equal to ``value``. 194 | 195 | * ``close_to(value, delta)``: matches values close to ``value`` within a tolerance of +/- ``delta``. 196 | 197 | * ``has_feature(name, extract, matcher)``: matches ``value`` if ``extract(value)`` matches ``matcher``. 198 | For instance: 199 | 200 | .. code:: python 201 | 202 | assert_that(result, has_feature("len", len, equal_to(2))) 203 | 204 | For clarity, it often helps to extract the use of ``has_feature`` into its own function: 205 | 206 | .. code:: python 207 | 208 | def has_len(matcher): 209 | return has_feature("len", len, matcher) 210 | 211 | assert_that(result, has_len(equal_to(2))) 212 | 213 | * ``raises(matcher)``: matches ``value`` if ``value()`` raises an exception matched by ``matcher``. 214 | For instance: 215 | 216 | .. code:: python 217 | 218 | assert_that(lambda: func("arg"), raises(is_instance(ValueError))) 219 | 220 | Alternatives 221 | ------------ 222 | 223 | PyHamcrest is another Python implemention of matchers. I prefer the error 224 | messages that this project produces, but feel free to judge for yourself: 225 | 226 | .. code:: python 227 | 228 | # Precisely 229 | from precisely import assert_that, is_sequence, has_attrs 230 | 231 | assert_that( 232 | [ 233 | User("bob", "jim@example.com"), 234 | User("jim", "bob@example.com"), 235 | ], 236 | is_sequence( 237 | has_attrs(username="bob", email_address="bob@example.com"), 238 | has_attrs(username="jim", email_address="jim@example.com"), 239 | ) 240 | ) 241 | 242 | # Expected: iterable containing in order: 243 | # 0: attributes: 244 | # * username: 'bob' 245 | # * email_address: 'bob@example.com' 246 | # 1: attributes: 247 | # * username: 'jim' 248 | # * email_address: 'jim@example.com' 249 | # but: element at index 0 mismatched: 250 | # * attribute email_address: was 'jim@example.com' 251 | 252 | # Hamcrest 253 | from hamcrest import assert_that, contains, has_properties 254 | 255 | assert_that( 256 | [ 257 | User("bob", "jim@example.com"), 258 | User("jim", "bob@example.com"), 259 | ], 260 | contains( 261 | has_properties(username="bob", email_address="bob@example.com"), 262 | has_properties(username="jim", email_address="jim@example.com"), 263 | ) 264 | ) 265 | 266 | # Hamcrest error: 267 | # Expected: a sequence containing [(an object with a property 'username' matching 'bob' and an object with a property 'email_address' matching 'bob@example.com'), (an object with a property 'username' matching 'jim' and an object with a property 'email_address' matching 'jim@example.com')] 268 | # but: item 0: an object with a property 'email_address' matching 'bob@example.com' property 'email_address' was 'jim@example.com' 269 | -------------------------------------------------------------------------------- /examples/examples.py: -------------------------------------------------------------------------------- 1 | import os 2 | import collections 3 | 4 | from nose.tools import istest 5 | 6 | if os.environ.get("HAMCREST"): 7 | from hamcrest import * 8 | else: 9 | from precisely.hamcrest import * 10 | 11 | 12 | User = collections.namedtuple("User", ["username", "email_address"]) 13 | 14 | 15 | @istest 16 | def test_anything(): 17 | assert_that(1, anything()) 18 | 19 | @istest 20 | def test_equal_to(): 21 | assert_that(1, equal_to(2)) 22 | 23 | 24 | @istest 25 | def test_has_property_wrong_value(): 26 | assert_that(User("bob", None), has_property("username", "bobbity")) 27 | 28 | 29 | @istest 30 | def test_has_property_missing(): 31 | assert_that("bob", has_property("username", "bobbity")) 32 | 33 | 34 | @istest 35 | def test_has_properties_wrong_value(): 36 | assert_that(User("bob", "bob@example.com"), has_properties( 37 | username="bob", 38 | email_address="bobbity@example.com", 39 | )) 40 | 41 | 42 | @istest 43 | def test_all_of(): 44 | assert_that(User("bob", "bob@example.com"), all_of( 45 | has_property("username", "bob"), 46 | has_property("email_address", "bobbity@example.com"), 47 | )) 48 | 49 | 50 | @istest 51 | def test_contains_inanyorder_missing_elements(): 52 | assert_that( 53 | [ 54 | User("bob", "jim@example.com"), 55 | User("jim", "bob@example.com"), 56 | ], 57 | contains_inanyorder( 58 | has_properties(username="bob", email_address="bob@example.com"), 59 | has_properties(username="jim", email_address="jim@example.com"), 60 | ) 61 | ) 62 | 63 | 64 | @istest 65 | def test_contains_inanyorder_extra_elements(): 66 | assert_that( 67 | ["apple", "banana"], 68 | contains_inanyorder("apple"), 69 | ) 70 | 71 | 72 | 73 | @istest 74 | def test_contains_missing_elements(): 75 | assert_that( 76 | [ 77 | User("bob", "jim@example.com"), 78 | User("jim", "bob@example.com"), 79 | ], 80 | contains( 81 | has_properties(username="bob", email_address="bob@example.com"), 82 | has_properties(username="jim", email_address="jim@example.com"), 83 | ) 84 | ) 85 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test upload clean bootstrap 2 | 3 | test: 4 | sh -c '. _virtualenv/bin/activate; nosetests tests' 5 | _virtualenv/bin/pyflakes precisely tests 6 | 7 | test-all: 8 | tox 9 | 10 | upload: test-all 11 | python setup.py sdist bdist_wheel upload 12 | make clean 13 | 14 | register: 15 | python setup.py register 16 | 17 | clean: 18 | rm -f MANIFEST 19 | rm -rf dist 20 | 21 | bootstrap: _virtualenv 22 | _virtualenv/bin/pip install -e . 23 | ifneq ($(wildcard test-requirements.txt),) 24 | _virtualenv/bin/pip install -r test-requirements.txt 25 | endif 26 | make clean 27 | 28 | _virtualenv: 29 | python3 -m venv _virtualenv 30 | _virtualenv/bin/pip install --upgrade pip 31 | _virtualenv/bin/pip install --upgrade setuptools 32 | -------------------------------------------------------------------------------- /precisely/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Matcher, is_matcher 2 | from .comparison_matchers import contains_string, greater_than, greater_than_or_equal_to, less_than, less_than_or_equal_to, starts_with, close_to 3 | from .core_matchers import equal_to, anything, all_of, any_of, not_ 4 | from .object_matchers import has_attr, has_attrs, is_instance 5 | from .iterable_matchers import all_elements, contains_exactly, includes, is_sequence 6 | from .feature_matchers import has_feature 7 | from .function_matchers import raises 8 | from .mapping_matchers import is_mapping, mapping_includes 9 | from .results import indent as _indent 10 | 11 | 12 | __all__ = [ 13 | "assert_that", 14 | "Matcher", 15 | "is_matcher", 16 | "contains_string", 17 | "greater_than", 18 | "greater_than_or_equal_to", 19 | "less_than", 20 | "less_than_or_equal_to", 21 | "close_to", 22 | "starts_with", 23 | "equal_to", 24 | "anything", 25 | "all_of", 26 | "any_of", 27 | "not_", 28 | "has_attr", 29 | "has_attrs", 30 | "is_instance", 31 | "all_elements", 32 | "contains_exactly", 33 | "includes", 34 | "is_same_sequence", 35 | "is_sequence", 36 | "has_feature", 37 | "is_mapping", 38 | "mapping_includes", 39 | "raises", 40 | ] 41 | 42 | # Deprecated 43 | is_same_sequence = is_sequence 44 | instance_of = is_instance 45 | 46 | 47 | def assert_that(value, matcher): 48 | result = matcher.match(value) 49 | if not result.is_match: 50 | raise AssertionError("\nExpected:{0}\nbut:{1}".format( 51 | _indent("\n" + matcher.describe()), 52 | _indent("\n" + result.explanation), 53 | )) 54 | -------------------------------------------------------------------------------- /precisely/base.py: -------------------------------------------------------------------------------- 1 | class Matcher(object): 2 | pass 3 | 4 | 5 | def is_matcher(value): 6 | return isinstance(value, Matcher) 7 | -------------------------------------------------------------------------------- /precisely/coercion.py: -------------------------------------------------------------------------------- 1 | from .base import is_matcher 2 | from .core_matchers import equal_to 3 | 4 | 5 | def to_matcher(value): 6 | if is_matcher(value): 7 | return value 8 | else: 9 | return equal_to(value) 10 | -------------------------------------------------------------------------------- /precisely/comparison_matchers.py: -------------------------------------------------------------------------------- 1 | import operator 2 | 3 | from .base import Matcher 4 | from .results import matched, unmatched 5 | 6 | 7 | def contains_string(value): 8 | return ComparisonMatcher(operator.contains, "contains the string", value) 9 | 10 | 11 | def greater_than(value): 12 | return ComparisonMatcher(operator.gt, "greater than", value) 13 | 14 | 15 | def greater_than_or_equal_to(value): 16 | return ComparisonMatcher(operator.ge, "greater than or equal to", value) 17 | 18 | 19 | def less_than(value): 20 | return ComparisonMatcher(operator.lt, "less than", value) 21 | 22 | 23 | def less_than_or_equal_to(value): 24 | return ComparisonMatcher(operator.le, "less than or equal to", value) 25 | 26 | 27 | def starts_with(value): 28 | return ComparisonMatcher(lambda actual, prefix: actual.startswith(prefix), "starts with", value) 29 | 30 | 31 | def _comparison_matcher(operator, operator_description, value): 32 | return ComparisonMatcher(operator, operator_description, value) 33 | 34 | 35 | class ComparisonMatcher(Matcher): 36 | def __init__(self, operator, operator_description, value): 37 | self._operator = operator 38 | self._operator_description = operator_description 39 | self._value = value 40 | 41 | def match(self, actual): 42 | if self._operator(actual, self._value): 43 | return matched() 44 | else: 45 | return unmatched("was {0!r}".format(actual)) 46 | 47 | def describe(self): 48 | return "{0} {1!r}".format(self._operator_description, self._value) 49 | 50 | 51 | def close_to(value, delta): 52 | return IsCloseToMatcher(value, delta) 53 | 54 | 55 | class IsCloseToMatcher(Matcher): 56 | def __init__(self, value, delta): 57 | self._value = value 58 | self._delta = delta 59 | 60 | def match(self, actual): 61 | difference = abs(self._value - actual) 62 | if difference > self._delta: 63 | return unmatched("was {0!r} ({1!r} away from {2!r})".format(actual, difference, self._value)) 64 | else: 65 | return matched() 66 | 67 | def describe(self): 68 | return "close to {0!r} +/- {1!r}".format(self._value, self._delta) 69 | -------------------------------------------------------------------------------- /precisely/core_matchers.py: -------------------------------------------------------------------------------- 1 | from .base import Matcher 2 | from .results import matched, unmatched, indented_list 3 | 4 | 5 | def equal_to(value): 6 | return EqualToMatcher(value) 7 | 8 | 9 | class EqualToMatcher(Matcher): 10 | def __init__(self, value): 11 | self._value = value 12 | 13 | def match(self, actual): 14 | if self._value == actual: 15 | return matched() 16 | else: 17 | return unmatched("was {0!r}".format(actual)) 18 | 19 | def describe(self): 20 | return repr(self._value) 21 | 22 | 23 | class AnyThingMatcher(Matcher): 24 | def match(self, actual): 25 | return matched() 26 | 27 | def describe(self): 28 | return "anything" 29 | 30 | 31 | anything = AnyThingMatcher() 32 | 33 | 34 | def all_of(*matchers): 35 | return AllOfMatcher(matchers) 36 | 37 | class AllOfMatcher(Matcher): 38 | def __init__(self, matchers): 39 | self._matchers = matchers 40 | 41 | def match(self, actual): 42 | for matcher in self._matchers: 43 | result = matcher.match(actual) 44 | if not result.is_match: 45 | return result 46 | 47 | return matched() 48 | 49 | def describe(self): 50 | return "all of:{0}".format(indented_list( 51 | matcher.describe() 52 | for matcher in self._matchers 53 | )) 54 | 55 | 56 | def any_of(*matchers): 57 | return AnyOfMatcher(matchers) 58 | 59 | class AnyOfMatcher(Matcher): 60 | def __init__(self, matchers): 61 | self._matchers = matchers 62 | 63 | def match(self, actual): 64 | results = [] 65 | for matcher in self._matchers: 66 | result = matcher.match(actual) 67 | if result.is_match: 68 | return result 69 | else: 70 | results.append(result) 71 | 72 | return unmatched("did not match any of:{0}".format(indented_list( 73 | "{0} [{1}]".format(matcher.describe(), result.explanation) 74 | for result, matcher in zip(results, self._matchers) 75 | ))) 76 | 77 | def describe(self): 78 | return "any of:{0}".format(indented_list( 79 | matcher.describe() 80 | for matcher in self._matchers 81 | )) 82 | 83 | 84 | def not_(matcher): 85 | return NotMatcher(matcher) 86 | 87 | class NotMatcher(Matcher): 88 | def __init__(self, matcher): 89 | self._matcher = matcher 90 | 91 | def match(self, actual): 92 | result = self._matcher.match(actual) 93 | if result.is_match: 94 | return unmatched("matched: {0}".format(self._matcher.describe())) 95 | else: 96 | return matched() 97 | 98 | def describe(self): 99 | return "not: {0}".format(self._matcher.describe()) 100 | -------------------------------------------------------------------------------- /precisely/feature_matchers.py: -------------------------------------------------------------------------------- 1 | from .base import Matcher 2 | from .results import matched, unmatched 3 | from .coercion import to_matcher 4 | 5 | 6 | def has_feature(name, extract, matcher): 7 | return HasFeatureMatcher(name, extract, to_matcher(matcher)) 8 | 9 | class HasFeatureMatcher(Matcher): 10 | def __init__(self, name, extract, matcher): 11 | self._name = name 12 | self._extract = extract 13 | self._matcher = matcher 14 | 15 | def match(self, actual): 16 | actual_feature = self._extract(actual) 17 | feature_result = self._matcher.match(actual_feature) 18 | if feature_result.is_match: 19 | return matched() 20 | else: 21 | return unmatched(self._description(feature_result.explanation)) 22 | 23 | def describe(self): 24 | return self._description(self._matcher.describe()) 25 | 26 | def _description(self, value): 27 | return "{0}: {1}".format(self._name, value) 28 | -------------------------------------------------------------------------------- /precisely/function_matchers.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | from .base import Matcher 4 | from .results import matched, unmatched 5 | 6 | 7 | def raises(exception_matcher): 8 | return RaisesMatcher(exception_matcher) 9 | 10 | 11 | class RaisesMatcher(Matcher): 12 | def __init__(self, exception_matcher): 13 | self._exception_matcher = exception_matcher 14 | 15 | def match(self, actual): 16 | if not callable(actual): 17 | return unmatched("was not callable") 18 | 19 | try: 20 | actual() 21 | except Exception as error: 22 | result = self._exception_matcher.match(error) 23 | if result.is_match: 24 | return matched() 25 | else: 26 | return unmatched("exception did not match: {0}\n\n{1}".format( 27 | result.explanation, 28 | traceback.format_exc(), 29 | )) 30 | 31 | return unmatched("did not raise exception") 32 | 33 | def describe(self): 34 | return "a callable raising: {0}".format(self._exception_matcher.describe()) 35 | -------------------------------------------------------------------------------- /precisely/hamcrest.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | assert_that, 3 | all_of, 4 | anything as _anything, 5 | contains_exactly as contains_inanyorder, 6 | equal_to, 7 | has_attr as has_property, 8 | has_attrs as has_properties, 9 | is_sequence as contains, 10 | ) 11 | 12 | 13 | __all__ = [ 14 | "assert_that", 15 | "all_of", 16 | "anything", 17 | "contains_inanyorder", 18 | "equal_to", 19 | "has_property", 20 | "has_properties", 21 | "contains", 22 | ] 23 | 24 | def anything(): 25 | return _anything 26 | -------------------------------------------------------------------------------- /precisely/iterable_matchers.py: -------------------------------------------------------------------------------- 1 | try: 2 | from itertools import zip_longest 3 | except ImportError: 4 | from itertools import izip_longest as zip_longest 5 | 6 | from .base import Matcher 7 | from .results import matched, unmatched, indented_list, indexed_indented_list, Result 8 | from .coercion import to_matcher 9 | 10 | 11 | def contains_exactly(*matchers): 12 | return ContainsExactlyMatcher([to_matcher(matcher) for matcher in matchers]) 13 | 14 | 15 | class ContainsExactlyMatcher(Matcher): 16 | def __init__(self, matchers): 17 | self._matchers = matchers 18 | 19 | def match(self, actual): 20 | values = _to_list_or_mismatch(actual) 21 | 22 | if isinstance(values, Result): 23 | return values 24 | elif len(values) == 0 and len(self._matchers) != 0: 25 | return unmatched("iterable was empty") 26 | else: 27 | matches = _Matches(values) 28 | for matcher in self._matchers: 29 | result = matches.match(matcher) 30 | if not result.is_match: 31 | return result 32 | return matches.match_remaining() 33 | 34 | def describe(self): 35 | elements_description = indented_list( 36 | matcher.describe() 37 | for matcher in self._matchers 38 | ) 39 | 40 | if len(self._matchers) == 0: 41 | return _empty_iterable_description 42 | elif len(self._matchers) == 1: 43 | return "iterable containing 1 element:{0}".format( 44 | elements_description, 45 | ) 46 | else: 47 | return "iterable containing these {0} elements in any order:{1}".format( 48 | len(self._matchers), 49 | elements_description, 50 | ) 51 | 52 | 53 | def includes(*matchers): 54 | return IncludesMatcher([to_matcher(matcher) for matcher in matchers]) 55 | 56 | 57 | class IncludesMatcher(Matcher): 58 | def __init__(self, matchers): 59 | self._matchers = matchers 60 | 61 | def match(self, actual): 62 | values = _to_list_or_mismatch(actual) 63 | 64 | if isinstance(values, Result): 65 | return values 66 | elif len(values) == 0 and len(self._matchers) != 0: 67 | return unmatched("iterable was empty") 68 | 69 | matches = _Matches(values) 70 | for matcher in self._matchers: 71 | result = matches.match(matcher) 72 | if not result.is_match: 73 | return result 74 | return matched() 75 | 76 | def describe(self): 77 | return "iterable including elements:{0}".format(indented_list( 78 | matcher.describe() 79 | for matcher in self._matchers 80 | )) 81 | 82 | 83 | class _Matches(object): 84 | def __init__(self, values): 85 | self._values = values 86 | self._is_matched = [False] * len(values) 87 | 88 | def match(self, matcher): 89 | mismatches = [] 90 | for index, (is_matched, value) in enumerate(zip(self._is_matched, self._values)): 91 | if is_matched: 92 | result = unmatched("already matched") 93 | else: 94 | result = matcher.match(value) 95 | 96 | if result.is_match: 97 | self._is_matched[index] = True 98 | return result 99 | else: 100 | mismatches.append(result) 101 | 102 | return unmatched("was missing element:{0}\nThese elements were in the iterable, but did not match the missing element:{1}".format( 103 | indented_list([matcher.describe()]), 104 | indented_list("{0}: {1}".format(repr(value), mismatch.explanation) for value, mismatch in zip(self._values, mismatches)), 105 | )) 106 | 107 | def match_remaining(self): 108 | if all(self._is_matched): 109 | return matched() 110 | else: 111 | return unmatched("had extra elements:{0}".format(indented_list( 112 | repr(value) 113 | for is_matched, value in zip(self._is_matched, self._values) 114 | if not is_matched 115 | ))) 116 | 117 | 118 | def is_sequence(*matchers): 119 | return IsSequenceMatcher([to_matcher(matcher) for matcher in matchers]) 120 | 121 | 122 | class IsSequenceMatcher(Matcher): 123 | _missing = object() 124 | 125 | def __init__(self, matchers): 126 | self._matchers = matchers 127 | 128 | def match(self, actual): 129 | values = _to_list_or_mismatch(actual) 130 | 131 | if isinstance(values, Result): 132 | return values 133 | 134 | elif len(values) == 0 and len(self._matchers) != 0: 135 | return unmatched("iterable was empty") 136 | 137 | extra = [] 138 | for index, (matcher, value) in enumerate(zip_longest(self._matchers, values, fillvalue=self._missing)): 139 | if matcher is self._missing: 140 | extra.append(value) 141 | elif value is self._missing: 142 | return unmatched("element at index {0} was missing".format(index)) 143 | else: 144 | result = matcher.match(value) 145 | if not result.is_match: 146 | return unmatched("element at index {0} mismatched:{1}".format(index, indented_list([result.explanation]))) 147 | 148 | if extra: 149 | return unmatched("had extra elements:{0}".format(indented_list(map(repr, extra)))) 150 | else: 151 | return matched() 152 | 153 | def describe(self): 154 | if len(self._matchers) == 0: 155 | return _empty_iterable_description 156 | else: 157 | return "iterable containing in order:{0}".format(indexed_indented_list( 158 | matcher.describe() 159 | for matcher in self._matchers 160 | )) 161 | 162 | 163 | def all_elements(matcher): 164 | return AllElementsMatcher(matcher) 165 | 166 | 167 | class AllElementsMatcher(Matcher): 168 | 169 | def __init__(self, matcher): 170 | self._element_matcher = matcher 171 | 172 | def match(self, actual): 173 | values = _to_list_or_mismatch(actual) 174 | 175 | if isinstance(values, Result): 176 | return values 177 | 178 | for index, value in enumerate(values): 179 | result = self._element_matcher.match(value) 180 | if not result.is_match: 181 | return unmatched("element at index {0} mismatched: {1}".format(index, result.explanation)) 182 | 183 | return matched() 184 | 185 | def describe(self): 186 | return "all elements of iterable match: {0}".format(self._element_matcher.describe()) 187 | 188 | 189 | _empty_iterable_description = "empty iterable" 190 | 191 | 192 | def _to_list_or_mismatch(iterable): 193 | try: 194 | iterator = iter(iterable) 195 | except TypeError: 196 | return unmatched("was not iterable\nwas {0}".format(repr(iterable))) 197 | 198 | return list(iterator) 199 | -------------------------------------------------------------------------------- /precisely/mapping_matchers.py: -------------------------------------------------------------------------------- 1 | from .base import Matcher 2 | from .coercion import to_matcher 3 | from .results import matched, unmatched, indented_list 4 | 5 | 6 | def is_mapping(matchers): 7 | return IsMappingMatcher(_values_to_matchers(matchers), allow_extra=False) 8 | 9 | 10 | def mapping_includes(matchers): 11 | return IsMappingMatcher(_values_to_matchers(matchers), allow_extra=True) 12 | 13 | 14 | def _values_to_matchers(matchers): 15 | return dict( 16 | (key, to_matcher(matcher)) 17 | for key, matcher in matchers.items() 18 | ) 19 | 20 | 21 | class IsMappingMatcher(Matcher): 22 | def __init__(self, matchers, allow_extra): 23 | self._allow_extra = allow_extra 24 | self._matchers = matchers 25 | 26 | def match(self, actual): 27 | undefined = object() 28 | for key, matcher in self._matchers.items(): 29 | value = actual.get(key, undefined) 30 | if value is undefined: 31 | return unmatched("was missing key: {0!r}".format(key)) 32 | 33 | value_result = matcher.match(value) 34 | if not value_result.is_match: 35 | return unmatched("value for key {0!r} mismatched:{1}".format(key, indented_list([value_result.explanation]))) 36 | 37 | if not self._allow_extra: 38 | extra_keys = set(actual.keys()) - set(self._matchers.keys()) 39 | if extra_keys: 40 | return unmatched("had extra keys:{0}".format(indented_list(sorted(map(repr, extra_keys))))) 41 | 42 | return matched() 43 | 44 | def describe(self): 45 | items_description = indented_list(sorted( 46 | "{0!r}: {1}".format(key, matcher.describe()) 47 | for key, matcher in self._matchers.items() 48 | )) 49 | 50 | if self._allow_extra: 51 | return "mapping including items:{0}".format(items_description) 52 | else: 53 | return "mapping with items:{0}".format(items_description) 54 | -------------------------------------------------------------------------------- /precisely/object_matchers.py: -------------------------------------------------------------------------------- 1 | from .base import Matcher 2 | from .results import matched, unmatched, indented_list 3 | from .coercion import to_matcher 4 | 5 | 6 | def has_attr(name, matcher): 7 | return HasAttr(name, to_matcher(matcher)) 8 | 9 | class HasAttr(Matcher): 10 | def __init__(self, name, matcher): 11 | self._name = name 12 | self._matcher = matcher 13 | 14 | def match(self, actual): 15 | if not hasattr(actual, self._name): 16 | return unmatched("was missing attribute {0}".format(self._name)) 17 | else: 18 | actual_property = getattr(actual, self._name) 19 | property_result = self._matcher.match(actual_property) 20 | if property_result.is_match: 21 | return matched() 22 | else: 23 | return unmatched("attribute {0} {1}".format(self._name, property_result.explanation)) 24 | 25 | def describe(self): 26 | return "object with attribute {0}: {1}".format(self._name, self._matcher.describe()) 27 | 28 | 29 | def has_attrs(*args, **kwargs): 30 | attrs = [] 31 | if attrs is not None: 32 | for arg in args: 33 | if isinstance(arg, dict): 34 | attrs += arg.items() 35 | else: 36 | attrs.append(arg) 37 | 38 | attrs += kwargs.items() 39 | 40 | return HasAttrs(attrs) 41 | 42 | class HasAttrs(Matcher): 43 | def __init__(self, matchers): 44 | self._matchers = [ 45 | has_attr(name, matcher) 46 | for name, matcher in matchers 47 | ] 48 | 49 | def match(self, actual): 50 | for matcher in self._matchers: 51 | result = matcher.match(actual) 52 | if not result.is_match: 53 | return result 54 | return matched() 55 | 56 | def describe(self): 57 | return "object with attributes:{0}".format(indented_list( 58 | "{0}: {1}".format(matcher._name, matcher._matcher.describe()) 59 | for matcher in self._matchers 60 | )) 61 | 62 | 63 | def is_instance(type_): 64 | return IsInstance(type_) 65 | 66 | class IsInstance(Matcher): 67 | def __init__(self, type_): 68 | self._type = type_ 69 | 70 | def match(self, actual): 71 | if isinstance(actual, self._type): 72 | return matched() 73 | else: 74 | return unmatched("had type {0}".format(type(actual).__name__)) 75 | 76 | def describe(self): 77 | return "is instance of {0}".format(self._type.__name__) 78 | -------------------------------------------------------------------------------- /precisely/results.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | 4 | Result = collections.namedtuple("_Result", ["is_match", "explanation"]) 5 | 6 | 7 | def matched(): 8 | return Result(True, None) 9 | 10 | 11 | def unmatched(explanation): 12 | return Result(False, explanation) 13 | 14 | 15 | def indented_list(items, bullet=None): 16 | if bullet is None: 17 | bullet = lambda index: "*" 18 | 19 | 20 | def format_item(index, item): 21 | prefix = " {0} ".format(bullet(index)) 22 | return "\n{0}{1}".format(prefix, indent(item, width=len(prefix))) 23 | 24 | return "".join( 25 | format_item(index, item) 26 | for index, item in enumerate(items) 27 | ) 28 | 29 | 30 | def indexed_indented_list(items): 31 | return indented_list(items, bullet=lambda index: "{0}:".format(index)) 32 | 33 | 34 | def indent(text, width=None): 35 | if width is None: 36 | width = 2 37 | 38 | return text.replace("\n", "\n" + " " * width) 39 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | from setuptools import setup 6 | 7 | def read(fname): 8 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 9 | 10 | 11 | 12 | setup( 13 | name='precisely', 14 | version='0.1.9', 15 | description='Rich matchers, useful for assertions in tests. Inspired by Hamcrest.', 16 | long_description=read("README.rst"), 17 | author='Michael Williamson', 18 | author_email='mike@zwobble.org', 19 | url='https://github.com/mwilliamson/python-precisely', 20 | packages=['precisely'], 21 | keywords="matcher matchers", 22 | install_requires=[], 23 | python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', 24 | license="BSD-2-Clause", 25 | classifiers=[ 26 | 'Development Status :: 5 - Production/Stable', 27 | 'Intended Audience :: Developers', 28 | 'License :: OSI Approved :: BSD License', 29 | 'Programming Language :: Python', 30 | 'Programming Language :: Python :: 2', 31 | 'Programming Language :: Python :: 2.7', 32 | 'Programming Language :: Python :: 3', 33 | 'Programming Language :: Python :: 3.5', 34 | 'Programming Language :: Python :: 3.6', 35 | 'Programming Language :: Python :: 3.7', 36 | 'Programming Language :: Python :: 3.8', 37 | 'Programming Language :: Python :: 3.9', 38 | ], 39 | ) 40 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | nose>1,<2 2 | PyHamcrest==1.9.0 3 | pyflakes==2.2.0 4 | -------------------------------------------------------------------------------- /tests/all_elements_tests.py: -------------------------------------------------------------------------------- 1 | from nose.tools import istest, assert_equal 2 | 3 | from precisely import all_elements, equal_to 4 | from precisely.results import matched, unmatched 5 | 6 | 7 | @istest 8 | def matches_when_all_items_in_iterable_match(): 9 | matcher = all_elements(equal_to("apple")) 10 | 11 | assert_equal(matched(), matcher.match(["apple", "apple"])) 12 | 13 | 14 | @istest 15 | def mismatches_when_actual_is_not_iterable(): 16 | matcher = all_elements(equal_to("apple")) 17 | 18 | assert_equal( 19 | unmatched("was not iterable\nwas 0"), 20 | matcher.match(0) 21 | ) 22 | 23 | 24 | @istest 25 | def mismatches_when_item_in_iterable_does_not_match(): 26 | matcher = all_elements(equal_to("apple")) 27 | 28 | assert_equal( 29 | unmatched("element at index 1 mismatched: was 'orange'"), 30 | matcher.match(["apple", "orange"]) 31 | ) 32 | 33 | 34 | @istest 35 | def matches_when_iterable_is_empty(): 36 | matcher = all_elements(equal_to("apple")) 37 | 38 | assert_equal(matched(), matcher.match([])) 39 | 40 | 41 | @istest 42 | def description_contains_descriptions_of_submatcher(): 43 | matcher = all_elements(equal_to("apple")) 44 | 45 | assert_equal( 46 | "all elements of iterable match: 'apple'", 47 | matcher.describe() 48 | ) 49 | -------------------------------------------------------------------------------- /tests/all_of_tests.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from nose.tools import istest, assert_equal 4 | 5 | from precisely import all_of, has_attr, equal_to 6 | from precisely.results import matched, unmatched 7 | 8 | 9 | User = collections.namedtuple("User", ["username", "email_address"]) 10 | 11 | @istest 12 | def matches_when_submatchers_all_match(): 13 | matcher = all_of( 14 | has_attr("username", equal_to("bob")), 15 | has_attr("email_address", equal_to("bob@example.com")), 16 | ) 17 | 18 | assert_equal(matched(), matcher.match(User("bob", "bob@example.com"))) 19 | 20 | 21 | @istest 22 | def mismatches_when_submatcher_mismatches(): 23 | matcher = all_of( 24 | has_attr("username", equal_to("bob")), 25 | has_attr("email_address", equal_to("bob@example.com")), 26 | ) 27 | 28 | assert_equal( 29 | unmatched("was missing attribute username"), 30 | matcher.match("bobbity") 31 | ) 32 | 33 | 34 | @istest 35 | def description_contains_descriptions_of_submatchers(): 36 | matcher = all_of( 37 | has_attr("username", equal_to("bob")), 38 | has_attr("email_address", equal_to("bob@example.com")), 39 | ) 40 | 41 | assert_equal( 42 | "all of:\n * object with attribute username: 'bob'\n * object with attribute email_address: 'bob@example.com'", 43 | matcher.describe() 44 | ) 45 | 46 | -------------------------------------------------------------------------------- /tests/any_of_tests.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from nose.tools import istest, assert_equal 4 | 5 | from precisely import any_of, has_attr, equal_to 6 | from precisely.results import matched, unmatched 7 | 8 | 9 | User = collections.namedtuple("User", ["username", "email_address"]) 10 | 11 | @istest 12 | def matches_when_submatchers_all_match(): 13 | matcher = any_of( 14 | has_attr("username", equal_to("bob")), 15 | has_attr("email_address", equal_to("bob@example.com")), 16 | ) 17 | 18 | assert_equal(matched(), matcher.match(User("bob", "bob@example.com"))) 19 | 20 | 21 | @istest 22 | def matches_when_any_submatchers_match(): 23 | matcher = any_of( 24 | equal_to("bob"), 25 | equal_to("jim"), 26 | ) 27 | 28 | assert_equal( 29 | matched(), 30 | matcher.match("bob"), 31 | ) 32 | 33 | 34 | @istest 35 | def mismatches_when_no_submatchers_match(): 36 | matcher = any_of( 37 | equal_to("bob"), 38 | equal_to("jim"), 39 | ) 40 | 41 | assert_equal( 42 | unmatched("did not match any of:\n * 'bob' [was 'alice']\n * 'jim' [was 'alice']"), 43 | matcher.match("alice"), 44 | ) 45 | 46 | 47 | @istest 48 | def description_contains_descriptions_of_submatchers(): 49 | matcher = any_of( 50 | has_attr("username", equal_to("bob")), 51 | has_attr("email_address", equal_to("bob@example.com")), 52 | ) 53 | 54 | assert_equal( 55 | "any of:\n * object with attribute username: 'bob'\n * object with attribute email_address: 'bob@example.com'", 56 | matcher.describe() 57 | ) 58 | 59 | -------------------------------------------------------------------------------- /tests/anything_tests.py: -------------------------------------------------------------------------------- 1 | from nose.tools import istest, assert_equal 2 | 3 | from precisely import anything 4 | from precisely.results import matched 5 | 6 | 7 | @istest 8 | def matches_anything(): 9 | assert_equal(matched(), anything.match(4)) 10 | assert_equal(matched(), anything.match(None)) 11 | assert_equal(matched(), anything.match("Hello")) 12 | 13 | 14 | @istest 15 | def description_is_anything(): 16 | assert_equal("anything", anything.describe()) 17 | 18 | -------------------------------------------------------------------------------- /tests/assert_that_tests.py: -------------------------------------------------------------------------------- 1 | from nose.tools import istest, assert_equal 2 | 3 | from precisely import assert_that, equal_to 4 | 5 | 6 | @istest 7 | def assert_that_does_nothing_if_matcher_matches(): 8 | assert_that(1, equal_to(1)) 9 | 10 | 11 | @istest 12 | def assert_that_raises_assertion_error_if_match_fails(): 13 | try: 14 | assert_that(1, equal_to(2)) 15 | assert False, "Expected AssertionError" 16 | except AssertionError as error: 17 | assert_equal("\nExpected:\n 2\nbut:\n was 1", str(error)) 18 | -------------------------------------------------------------------------------- /tests/close_to_tests.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from nose.tools import istest, assert_equal 4 | 5 | from precisely import close_to, is_sequence 6 | from precisely.results import matched, unmatched 7 | 8 | 9 | @istest 10 | def close_to_matches_when_actual_is_close_to_value_plus_delta(): 11 | matcher = close_to(42, 1) 12 | assert_equal(matched(), matcher.match(43)) 13 | assert_equal(matched(), matcher.match(42.5)) 14 | assert_equal(matched(), matcher.match(42)) 15 | assert_equal(matched(), matcher.match(41.5)) 16 | assert_equal(matched(), matcher.match(41)) 17 | assert_equal(unmatched("was 40 (2 away from 42)"), matcher.match(40)) 18 | 19 | 20 | @istest 21 | def close_to_matches_any_types_supporting_comparison_and_addition_and_subtraction(): 22 | class Instant(object): 23 | def __init__(self, seconds_since_epoch): 24 | self.seconds_since_epoch = seconds_since_epoch 25 | 26 | def __sub__(self, other): 27 | if isinstance(other, Instant): 28 | return Interval(self.seconds_since_epoch - other.seconds_since_epoch) 29 | else: 30 | return NotImplemented 31 | 32 | def __repr__(self): 33 | return "Instant({})".format(self.seconds_since_epoch) 34 | 35 | @functools.total_ordering 36 | class Interval(object): 37 | def __init__(self, seconds): 38 | self.seconds = seconds 39 | 40 | def __abs__(self): 41 | return Interval(abs(self.seconds)) 42 | 43 | def __eq__(self, other): 44 | if isinstance(other, Interval): 45 | return self.seconds == other.seconds 46 | else: 47 | return NotImplemented 48 | 49 | def __lt__(self, other): 50 | if isinstance(other, Interval): 51 | return self.seconds < other.seconds 52 | else: 53 | return NotImplemented 54 | 55 | def __repr__(self): 56 | return "Interval({})".format(self.seconds) 57 | 58 | matcher = close_to(Instant(42), Interval(1)) 59 | assert_equal(matched(), matcher.match(Instant(43))) 60 | assert_equal(matched(), matcher.match(Instant(42.5))) 61 | assert_equal(matched(), matcher.match(Instant(42))) 62 | assert_equal(matched(), matcher.match(Instant(41.5))) 63 | assert_equal(matched(), matcher.match(Instant(41))) 64 | assert_equal(unmatched("was Instant(40) (Interval(2) away from Instant(42))"), matcher.match(Instant(40))) 65 | 66 | 67 | @istest 68 | def close_to_description_describes_value(): 69 | matcher = close_to(42, 1) 70 | assert_equal("close to 42 +/- 1", matcher.describe()) 71 | 72 | 73 | @istest 74 | def close_to_can_be_used_in_composite_matcher(): 75 | matcher = is_sequence("a", "b", close_to(42, 1)) 76 | assert_equal(matched(), matcher.match(("a", "b", 42))) 77 | -------------------------------------------------------------------------------- /tests/contains_exactly_tests.py: -------------------------------------------------------------------------------- 1 | from nose.tools import istest, assert_equal 2 | 3 | from precisely import contains_exactly, equal_to 4 | from precisely.results import matched, unmatched 5 | 6 | 7 | @istest 8 | def matches_when_all_submatchers_match_one_item_with_no_items_leftover(): 9 | matcher = contains_exactly(equal_to("apple"), equal_to("banana")) 10 | 11 | assert_equal(matched(), matcher.match(["banana", "apple"])) 12 | 13 | 14 | @istest 15 | def mismatches_when_actual_is_not_iterable(): 16 | matcher = contains_exactly() 17 | 18 | assert_equal( 19 | unmatched("was not iterable\nwas 0"), 20 | matcher.match(0) 21 | ) 22 | 23 | 24 | @istest 25 | def mismatches_when_item_is_missing(): 26 | matcher = contains_exactly(equal_to("apple"), equal_to("banana"), equal_to("coconut")) 27 | 28 | assert_equal( 29 | unmatched("was missing element:\n * 'banana'\nThese elements were in the iterable, but did not match the missing element:\n * 'coconut': was 'coconut'\n * 'apple': already matched"), 30 | matcher.match(["coconut", "apple"]) 31 | ) 32 | 33 | 34 | @istest 35 | def mismatches_when_duplicate_is_missing(): 36 | matcher = contains_exactly(equal_to("apple"), equal_to("apple")) 37 | 38 | assert_equal( 39 | unmatched("was missing element:\n * 'apple'\nThese elements were in the iterable, but did not match the missing element:\n * 'apple': already matched"), 40 | matcher.match(["apple"]) 41 | ) 42 | 43 | 44 | @istest 45 | def mismatches_when_item_is_expected_but_iterable_is_empty(): 46 | matcher = contains_exactly(equal_to("apple")) 47 | 48 | assert_equal( 49 | unmatched("iterable was empty"), 50 | matcher.match([]) 51 | ) 52 | 53 | 54 | @istest 55 | def when_empty_iterable_is_expected_then_empty_iterable_matches(): 56 | matcher = contains_exactly() 57 | 58 | assert_equal( 59 | matched(), 60 | matcher.match([]) 61 | ) 62 | 63 | 64 | @istest 65 | def mismatches_when_contains_extra_item(): 66 | matcher = contains_exactly(equal_to("apple")) 67 | 68 | assert_equal( 69 | unmatched("had extra elements:\n * 'coconut'"), 70 | matcher.match(["coconut", "apple"]) 71 | ) 72 | 73 | 74 | @istest 75 | def description_is_of_empty_iterable_when_there_are_zero_submatchers(): 76 | matcher = contains_exactly() 77 | 78 | assert_equal("empty iterable", matcher.describe()) 79 | 80 | 81 | @istest 82 | def description_uses_singular_when_there_is_one_submatcher(): 83 | matcher = contains_exactly(equal_to("apple")) 84 | 85 | assert_equal( 86 | "iterable containing 1 element:\n * 'apple'", 87 | matcher.describe() 88 | ) 89 | 90 | 91 | @istest 92 | def description_contains_descriptions_of_submatchers(): 93 | matcher = contains_exactly(equal_to("apple"), equal_to("banana")) 94 | 95 | assert_equal( 96 | "iterable containing these 2 elements in any order:\n * 'apple'\n * 'banana'", 97 | matcher.describe() 98 | ) 99 | 100 | 101 | @istest 102 | def elements_are_coerced_to_matchers(): 103 | matcher = contains_exactly("apple", "banana") 104 | 105 | assert_equal( 106 | "iterable containing these 2 elements in any order:\n * 'apple'\n * 'banana'", 107 | matcher.describe() 108 | ) 109 | 110 | -------------------------------------------------------------------------------- /tests/contains_string_tests.py: -------------------------------------------------------------------------------- 1 | from nose.tools import istest, assert_equal 2 | 3 | from precisely import contains_string 4 | from precisely.results import matched, unmatched 5 | 6 | 7 | @istest 8 | def contains_string_matches_when_actual_string_contains_value_passed_to_matcher(): 9 | matcher = contains_string("ab") 10 | assert_equal(matched(), matcher.match("ab")) 11 | assert_equal(matched(), matcher.match("abc")) 12 | assert_equal(matched(), matcher.match("abcd")) 13 | assert_equal(matched(), matcher.match("cabd")) 14 | assert_equal(matched(), matcher.match("cdab")) 15 | assert_equal(unmatched("was 'a'"), matcher.match("a")) 16 | 17 | 18 | @istest 19 | def contains_string_description_describes_value(): 20 | matcher = contains_string("ab") 21 | assert_equal("contains the string 'ab'", matcher.describe()) 22 | -------------------------------------------------------------------------------- /tests/equal_to_tests.py: -------------------------------------------------------------------------------- 1 | from nose.tools import istest, assert_equal 2 | 3 | from precisely import equal_to 4 | from precisely.results import matched, unmatched 5 | 6 | 7 | @istest 8 | def matches_when_values_are_equal(): 9 | assert_equal(matched(), equal_to(1).match(1)) 10 | 11 | 12 | @istest 13 | def explanation_of_mismatch_contains_repr_of_actual(): 14 | assert_equal(unmatched("was 2"), equal_to(1).match(2)) 15 | assert_equal(unmatched("was 'hello'"), equal_to(1).match("hello")) 16 | 17 | 18 | @istest 19 | def description_is_repr_of_value(): 20 | assert_equal("'hello'", equal_to("hello").describe()) 21 | -------------------------------------------------------------------------------- /tests/has_attr_tests.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from nose.tools import istest, assert_equal 4 | 5 | from precisely import has_attr, equal_to 6 | from precisely.results import matched, unmatched 7 | 8 | 9 | User = collections.namedtuple("User", ["username"]) 10 | 11 | 12 | @istest 13 | def matches_when_property_has_correct_value(): 14 | assert_equal(matched(), has_attr("username", equal_to("bob")).match(User("bob"))) 15 | 16 | 17 | @istest 18 | def mismatches_when_property_is_missing(): 19 | assert_equal( 20 | unmatched("was missing attribute username"), 21 | has_attr("username", equal_to("bob")).match("bobbity") 22 | ) 23 | 24 | 25 | @istest 26 | def explanation_of_mismatch_contains_mismatch_of_property(): 27 | assert_equal( 28 | unmatched("attribute username was 'bobbity'"), 29 | has_attr("username", equal_to("bob")).match(User("bobbity")) 30 | ) 31 | 32 | 33 | @istest 34 | def submatcher_is_coerced_to_matcher(): 35 | assert_equal( 36 | unmatched("attribute username was 'bobbity'"), 37 | has_attr("username", "bob").match(User("bobbity")) 38 | ) 39 | 40 | 41 | @istest 42 | def description_contains_description_of_property(): 43 | assert_equal( 44 | "object with attribute username: 'bob'", 45 | has_attr("username", equal_to("bob")).describe() 46 | ) 47 | 48 | -------------------------------------------------------------------------------- /tests/has_attrs_tests.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from nose.tools import istest, assert_equal 4 | 5 | from precisely import has_attrs, equal_to 6 | from precisely.results import matched, unmatched 7 | 8 | 9 | User = collections.namedtuple("User", ["username", "email_address"]) 10 | 11 | @istest 12 | def matches_when_properties_all_match(): 13 | matcher = has_attrs( 14 | username=equal_to("bob"), 15 | email_address=equal_to("bob@example.com"), 16 | ) 17 | 18 | assert_equal(matched(), matcher.match(User("bob", "bob@example.com"))) 19 | 20 | 21 | @istest 22 | def mismatches_when_property_is_missing(): 23 | matcher = has_attrs( 24 | ("username", equal_to("bob")), 25 | ("email_address", equal_to("bob@example.com")), 26 | ) 27 | 28 | assert_equal( 29 | unmatched("was missing attribute username"), 30 | matcher.match("bobbity") 31 | ) 32 | 33 | 34 | @istest 35 | def explanation_of_mismatch_contains_mismatch_of_property(): 36 | matcher = has_attrs( 37 | username=equal_to("bob"), 38 | email_address=equal_to("bob@example.com"), 39 | ) 40 | 41 | assert_equal( 42 | unmatched("attribute email_address was 'bobbity@example.com'"), 43 | matcher.match(User("bob", "bobbity@example.com")) 44 | ) 45 | 46 | 47 | @istest 48 | def submatcher_is_coerced_to_matcher(): 49 | matcher = has_attrs(username="bob") 50 | 51 | assert_equal( 52 | unmatched("attribute username was 'bobbity'"), 53 | matcher.match(User("bobbity", None)) 54 | ) 55 | 56 | 57 | @istest 58 | def description_contains_descriptions_of_properties(): 59 | matcher = has_attrs( 60 | username=equal_to("bob"), 61 | ) 62 | 63 | assert_equal( 64 | "object with attributes:\n * username: 'bob'", 65 | matcher.describe() 66 | ) 67 | 68 | 69 | @istest 70 | def can_pass_properties_as_list_of_tuples(): 71 | matcher = has_attrs( 72 | ("username", equal_to("bob")), 73 | ("email_address", equal_to("bob@example.com")), 74 | ) 75 | 76 | assert_equal( 77 | "object with attributes:\n * username: 'bob'\n * email_address: 'bob@example.com'", 78 | matcher.describe() 79 | ) 80 | 81 | 82 | @istest 83 | def can_pass_properties_as_dictionary(): 84 | matcher = has_attrs({ 85 | "username": equal_to("bob"), 86 | }) 87 | 88 | assert_equal( 89 | "object with attributes:\n * username: 'bob'", 90 | matcher.describe() 91 | ) 92 | -------------------------------------------------------------------------------- /tests/has_feature_tests.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from nose.tools import istest, assert_equal 4 | 5 | from precisely import has_feature, equal_to 6 | from precisely.results import matched, unmatched 7 | 8 | 9 | User = collections.namedtuple("User", ["username"]) 10 | 11 | 12 | @istest 13 | def matches_when_feature_has_correct_value(): 14 | matcher = has_feature("name", lambda user: user.username, equal_to("bob")) 15 | assert_equal(matched(), matcher.match(User("bob"))) 16 | 17 | 18 | @istest 19 | def mismatches_when_feature_extraction_fails(): 20 | # TODO: 21 | return 22 | matcher = has_feature("name", lambda user: user.username, equal_to("bob")) 23 | assert_equal( 24 | unmatched(""), 25 | matcher.match("bobbity") 26 | ) 27 | 28 | 29 | @istest 30 | def explanation_of_mismatch_contains_mismatch_of_feature(): 31 | matcher = has_feature("name", lambda user: user.username, equal_to("bob")) 32 | assert_equal( 33 | unmatched("name: was 'bobbity'"), 34 | matcher.match(User("bobbity")) 35 | ) 36 | 37 | 38 | @istest 39 | def submatcher_is_coerced_to_matcher(): 40 | matcher = has_feature("name", lambda user: user.username, "bob") 41 | assert_equal( 42 | unmatched("name: was 'bobbity'"), 43 | matcher.match(User("bobbity")) 44 | ) 45 | 46 | 47 | @istest 48 | def description_contains_description_of_property(): 49 | matcher = has_feature("name", lambda user: user.username, equal_to("bob")) 50 | assert_equal( 51 | "name: 'bob'", 52 | matcher.describe() 53 | ) 54 | 55 | -------------------------------------------------------------------------------- /tests/includes_tests.py: -------------------------------------------------------------------------------- 1 | from nose.tools import istest, assert_equal 2 | 3 | from precisely import equal_to, includes 4 | from precisely.results import matched, unmatched 5 | 6 | 7 | @istest 8 | def matches_when_all_submatchers_match_one_item_with_no_items_leftover(): 9 | matcher = includes(equal_to("apple"), equal_to("banana")) 10 | 11 | assert_equal(matched(), matcher.match(["apple", "banana"])) 12 | assert_equal(matched(), matcher.match(["apple", "banana", "coconut"])) 13 | 14 | 15 | @istest 16 | def mismatches_when_actual_is_not_iterable(): 17 | matcher = includes(equal_to("apple")) 18 | 19 | assert_equal( 20 | unmatched("was not iterable\nwas 0"), 21 | matcher.match(0) 22 | ) 23 | 24 | 25 | @istest 26 | def mismatches_when_item_is_missing(): 27 | matcher = includes(equal_to("apple"), equal_to("banana"), equal_to("coconut")) 28 | 29 | assert_equal( 30 | unmatched("was missing element:\n * 'banana'\nThese elements were in the iterable, but did not match the missing element:\n * 'coconut': was 'coconut'\n * 'apple': already matched"), 31 | matcher.match(["coconut", "apple"]) 32 | ) 33 | 34 | 35 | @istest 36 | def mismatches_when_duplicate_is_missing(): 37 | matcher = includes(equal_to("apple"), equal_to("apple")) 38 | 39 | assert_equal( 40 | unmatched("was missing element:\n * 'apple'\nThese elements were in the iterable, but did not match the missing element:\n * 'apple': already matched"), 41 | matcher.match(["apple"]) 42 | ) 43 | 44 | 45 | @istest 46 | def mismatches_when_item_is_expected_but_iterable_is_empty(): 47 | matcher = includes(equal_to("apple")) 48 | 49 | assert_equal( 50 | unmatched("iterable was empty"), 51 | matcher.match([]) 52 | ) 53 | 54 | 55 | @istest 56 | def when_no_elements_are_expected_then_empty_iterable_matches(): 57 | matcher = includes() 58 | 59 | assert_equal( 60 | matched(), 61 | matcher.match([]) 62 | ) 63 | 64 | 65 | @istest 66 | def matches_when_there_are_extra_items(): 67 | matcher = includes(equal_to("apple")) 68 | 69 | assert_equal(matched(), matcher.match(["coconut", "apple"])) 70 | 71 | 72 | @istest 73 | def description_contains_descriptions_of_submatchers(): 74 | matcher = includes(equal_to("apple"), equal_to("banana")) 75 | 76 | assert_equal( 77 | "iterable including elements:\n * 'apple'\n * 'banana'", 78 | matcher.describe() 79 | ) 80 | 81 | 82 | @istest 83 | def elements_are_coerced_to_matchers(): 84 | matcher = includes("apple", "banana") 85 | 86 | assert_equal( 87 | "iterable including elements:\n * 'apple'\n * 'banana'", 88 | matcher.describe() 89 | ) 90 | 91 | -------------------------------------------------------------------------------- /tests/is_instance_tests.py: -------------------------------------------------------------------------------- 1 | from nose.tools import istest, assert_equal 2 | 3 | from precisely import is_instance 4 | from precisely.results import matched, unmatched 5 | 6 | 7 | @istest 8 | def matches_when_value_is_instance_of_class(): 9 | assert_equal(matched(), is_instance(int).match(1)) 10 | 11 | 12 | @istest 13 | def explanation_of_mismatch_contains_actual_type(): 14 | assert_equal(unmatched("had type float"), is_instance(int).match(1.0)) 15 | 16 | 17 | @istest 18 | def description_includes_expected_type(): 19 | assert_equal("is instance of int", is_instance(int).describe()) 20 | -------------------------------------------------------------------------------- /tests/is_mapping_tests.py: -------------------------------------------------------------------------------- 1 | from nose.tools import istest, assert_equal 2 | 3 | from precisely import equal_to, is_mapping 4 | from precisely.results import matched, unmatched 5 | 6 | 7 | @istest 8 | def matches_when_keys_and_values_match(): 9 | matcher = is_mapping({"a": equal_to(1), "b": equal_to(2)}) 10 | assert_equal(matched(), matcher.match({"a": 1, "b": 2})) 11 | 12 | 13 | @istest 14 | def values_are_coerced_to_matchers(): 15 | matcher = is_mapping({"a": 1, "b": 2}) 16 | assert_equal(matched(), matcher.match({"a": 1, "b": 2})) 17 | 18 | 19 | @istest 20 | def does_not_match_when_value_does_not_match(): 21 | matcher = is_mapping({"a": equal_to(1), "b": equal_to(2)}) 22 | assert_equal(unmatched("value for key 'b' mismatched:\n * was 3"), matcher.match({"a": 1, "b": 3})) 23 | 24 | 25 | @istest 26 | def does_not_match_when_keys_are_missing(): 27 | matcher = is_mapping({"a": equal_to(1), "b": equal_to(2)}) 28 | assert_equal(unmatched("was missing key: 'b'"), matcher.match({"a": 1})) 29 | 30 | 31 | @istest 32 | def does_not_match_when_there_are_extra_keys(): 33 | matcher = is_mapping({"a": equal_to(1)}) 34 | assert_equal(unmatched("had extra keys:\n * 'b'\n * 'c'"), matcher.match({"a": 1, "b": 1, "c": 1})) 35 | 36 | 37 | @istest 38 | def description_describes_keys_and_value_matchers(): 39 | matcher = is_mapping({"a": equal_to(1), "b": equal_to(2)}) 40 | assert_equal("mapping with items:\n * 'a': 1\n * 'b': 2", matcher.describe()) 41 | -------------------------------------------------------------------------------- /tests/is_sequence_tests.py: -------------------------------------------------------------------------------- 1 | from nose.tools import istest, assert_equal 2 | 3 | from precisely import is_sequence, equal_to 4 | from precisely.results import matched, unmatched 5 | 6 | 7 | @istest 8 | def matches_when_all_submatchers_match_one_item_with_no_items_leftover(): 9 | matcher = is_sequence(equal_to("apple"), equal_to("banana")) 10 | 11 | assert_equal(matched(), matcher.match(["apple", "banana"])) 12 | 13 | 14 | @istest 15 | def mismatches_when_actual_is_not_iterable(): 16 | matcher = is_sequence(equal_to("apple")) 17 | 18 | assert_equal( 19 | unmatched("was not iterable\nwas 0"), 20 | matcher.match(0) 21 | ) 22 | 23 | 24 | @istest 25 | def mismatches_when_items_are_in_wrong_order(): 26 | matcher = is_sequence(equal_to("apple"), equal_to("banana")) 27 | 28 | assert_equal( 29 | unmatched("element at index 0 mismatched:\n * was 'banana'"), 30 | matcher.match(["banana", "apple"]) 31 | ) 32 | 33 | 34 | @istest 35 | def mismatches_when_item_is_missing(): 36 | matcher = is_sequence(equal_to("apple"), equal_to("banana"), equal_to("coconut")) 37 | 38 | assert_equal( 39 | unmatched("element at index 2 was missing"), 40 | matcher.match(["apple", "banana"]) 41 | ) 42 | 43 | 44 | @istest 45 | def mismatches_when_item_is_expected_but_iterable_is_empty(): 46 | matcher = is_sequence(equal_to("apple")) 47 | 48 | assert_equal( 49 | unmatched("iterable was empty"), 50 | matcher.match([]) 51 | ) 52 | 53 | 54 | @istest 55 | def when_empty_iterable_is_expected_then_empty_iterable_matches(): 56 | matcher = is_sequence() 57 | 58 | assert_equal( 59 | matched(), 60 | matcher.match([]) 61 | ) 62 | 63 | 64 | @istest 65 | def mismatches_when_contains_extra_item(): 66 | matcher = is_sequence(equal_to("apple")) 67 | 68 | assert_equal( 69 | unmatched("had extra elements:\n * 'coconut'"), 70 | matcher.match(["apple", "coconut"]) 71 | ) 72 | 73 | 74 | @istest 75 | def when_there_are_zero_submatchers_then_description_is_of_empty_iterable(): 76 | matcher = is_sequence() 77 | 78 | assert_equal( 79 | "empty iterable", 80 | matcher.describe() 81 | ) 82 | 83 | 84 | @istest 85 | def description_contains_descriptions_of_submatchers(): 86 | matcher = is_sequence(equal_to("apple"), equal_to("banana")) 87 | 88 | assert_equal( 89 | "iterable containing in order:\n 0: 'apple'\n 1: 'banana'", 90 | matcher.describe() 91 | ) 92 | 93 | 94 | @istest 95 | def elements_are_coerced_to_matchers(): 96 | matcher = is_sequence("apple", "banana") 97 | 98 | assert_equal( 99 | "iterable containing in order:\n 0: 'apple'\n 1: 'banana'", 100 | matcher.describe() 101 | ) 102 | 103 | -------------------------------------------------------------------------------- /tests/mapping_includes_tests.py: -------------------------------------------------------------------------------- 1 | from nose.tools import istest, assert_equal 2 | 3 | from precisely import equal_to, mapping_includes 4 | from precisely.results import matched, unmatched 5 | 6 | 7 | @istest 8 | def matches_when_keys_and_values_match(): 9 | matcher = mapping_includes({"a": equal_to(1), "b": equal_to(2)}) 10 | assert_equal(matched(), matcher.match({"a": 1, "b": 2})) 11 | 12 | 13 | @istest 14 | def values_are_coerced_to_matchers(): 15 | matcher = mapping_includes({"a": 1, "b": 2}) 16 | assert_equal(matched(), matcher.match({"a": 1, "b": 2})) 17 | 18 | 19 | @istest 20 | def does_not_match_when_value_does_not_match(): 21 | matcher = mapping_includes({"a": equal_to(1), "b": equal_to(2)}) 22 | assert_equal( 23 | unmatched("value for key 'b' mismatched:\n * was 3"), 24 | matcher.match({"a": 1, "b": 3, "c": 4}), 25 | ) 26 | 27 | 28 | @istest 29 | def does_not_match_when_keys_are_missing(): 30 | matcher = mapping_includes({"a": equal_to(1), "b": equal_to(2)}) 31 | assert_equal(unmatched("was missing key: 'b'"), matcher.match({"a": 1})) 32 | 33 | 34 | @istest 35 | def matches_when_there_are_extra_keys(): 36 | matcher = mapping_includes({"a": equal_to(1)}) 37 | assert_equal(matched(), matcher.match({"a": 1, "b": 1, "c": 1})) 38 | 39 | 40 | @istest 41 | def description_describes_keys_and_value_matchers(): 42 | matcher = mapping_includes({"a": equal_to(1), "b": equal_to(2)}) 43 | assert_equal("mapping including items:\n * 'a': 1\n * 'b': 2", matcher.describe()) 44 | -------------------------------------------------------------------------------- /tests/not_tests.py: -------------------------------------------------------------------------------- 1 | from nose.tools import istest, assert_equal 2 | 3 | from precisely import equal_to, not_ 4 | from precisely.results import matched, unmatched 5 | 6 | 7 | @istest 8 | def matches_when_negated_matcher_does_not_match(): 9 | assert_equal(matched(), not_(equal_to(1)).match(2)) 10 | 11 | 12 | @istest 13 | def does_not_match_when_negated_matcher_matches(): 14 | assert_equal(unmatched("matched: 1"), not_(equal_to(1)).match(1)) 15 | 16 | 17 | @istest 18 | def description_includes_description_of_negated_matcher(): 19 | assert_equal("not: 'hello'", not_(equal_to("hello")).describe()) 20 | -------------------------------------------------------------------------------- /tests/numeric_comparison_tests.py: -------------------------------------------------------------------------------- 1 | from nose.tools import istest, assert_equal 2 | 3 | from precisely import greater_than, greater_than_or_equal_to, less_than, less_than_or_equal_to 4 | from precisely.results import matched, unmatched 5 | 6 | 7 | @istest 8 | def greater_than_matches_when_actual_is_greater_than_value(): 9 | matcher = greater_than(42) 10 | assert_equal(matched(), matcher.match(43)) 11 | assert_equal(unmatched("was 42"), matcher.match(42)) 12 | assert_equal(unmatched("was 41"), matcher.match(41)) 13 | 14 | 15 | @istest 16 | def greater_than_description_describes_value(): 17 | matcher = greater_than(42) 18 | assert_equal("greater than 42", matcher.describe()) 19 | 20 | 21 | @istest 22 | def greater_than_or_equal_to_matches_when_actual_is_greater_than_or_equal_to_value(): 23 | matcher = greater_than_or_equal_to(42) 24 | assert_equal(matched(), matcher.match(43)) 25 | assert_equal(matched(), matcher.match(42)) 26 | assert_equal(unmatched("was 41"), matcher.match(41)) 27 | 28 | 29 | @istest 30 | def greater_than_or_equal_to_description_describes_value(): 31 | matcher = greater_than_or_equal_to(42) 32 | assert_equal("greater than or equal to 42", matcher.describe()) 33 | 34 | 35 | @istest 36 | def less_than_matches_when_actual_is_less_than_value(): 37 | matcher = less_than(42) 38 | assert_equal(matched(), matcher.match(41)) 39 | assert_equal(unmatched("was 42"), matcher.match(42)) 40 | assert_equal(unmatched("was 43"), matcher.match(43)) 41 | 42 | 43 | @istest 44 | def less_than_description_describes_value(): 45 | matcher = less_than(42) 46 | assert_equal("less than 42", matcher.describe()) 47 | 48 | 49 | @istest 50 | def less_than_or_equal_to_matches_when_actual_is_less_than_or_equal_to_value(): 51 | matcher = less_than_or_equal_to(42) 52 | assert_equal(matched(), matcher.match(41)) 53 | assert_equal(matched(), matcher.match(42)) 54 | assert_equal(unmatched("was 43"), matcher.match(43)) 55 | 56 | 57 | @istest 58 | def less_than_or_equal_to_description_describes_value(): 59 | matcher = less_than_or_equal_to(42) 60 | assert_equal("less than or equal to 42", matcher.describe()) 61 | -------------------------------------------------------------------------------- /tests/raises_tests.py: -------------------------------------------------------------------------------- 1 | from nose.tools import istest, assert_equal 2 | 3 | from precisely import is_instance, raises 4 | from precisely.results import matched, unmatched 5 | 6 | 7 | @istest 8 | def matches_when_expected_exception_is_raised(): 9 | def raise_key_error(): 10 | raise KeyError() 11 | 12 | matcher = raises(is_instance(KeyError)) 13 | assert_equal(matched(), matcher.match(raise_key_error)) 14 | 15 | 16 | @istest 17 | def mismatches_when_no_exception_is_raised(): 18 | matcher = raises(is_instance(KeyError)) 19 | assert_equal(unmatched("did not raise exception"), matcher.match(lambda: None)) 20 | 21 | 22 | @istest 23 | def mismatches_when_unexpected_exception_is_raised(): 24 | def raise_key_error(): 25 | raise KeyError() 26 | 27 | matcher = raises(is_instance(ValueError)) 28 | result = matcher.match(raise_key_error) 29 | assert not result.is_match 30 | assert _normalise_newlines(result.explanation).startswith( 31 | "exception did not match: had type KeyError\n\nTraceback (most recent call last):\n", 32 | ) 33 | 34 | 35 | @istest 36 | def mismatches_when_value_is_not_callable(): 37 | matcher = raises(is_instance(ValueError)) 38 | assert_equal(unmatched("was not callable"), matcher.match(42)) 39 | 40 | 41 | @istest 42 | def description_includes_description_of_exception(): 43 | matcher = raises(is_instance(ValueError)) 44 | assert_equal("a callable raising: is instance of ValueError", matcher.describe()) 45 | 46 | 47 | def _normalise_newlines(string): 48 | return string.replace("\r\n", "\n").replace("\r", "\n") 49 | -------------------------------------------------------------------------------- /tests/results_tests.py: -------------------------------------------------------------------------------- 1 | from nose.tools import istest, assert_equal 2 | 3 | from precisely.results import indented_list 4 | 5 | 6 | @istest 7 | def indented_list_indents_children(): 8 | assert_equal( 9 | "\n * apple\n * banana\n * coconut\n * durian", 10 | indented_list([ 11 | "apple" + indented_list(["banana", "coconut"]), 12 | "durian", 13 | ]) 14 | ) 15 | -------------------------------------------------------------------------------- /tests/starts_with_tests.py: -------------------------------------------------------------------------------- 1 | from nose.tools import istest, assert_equal 2 | 3 | from precisely import starts_with 4 | from precisely.results import matched, unmatched 5 | 6 | 7 | @istest 8 | def starts_with_matches_when_actual_string_starts_with_value_passed_to_matcher(): 9 | matcher = starts_with("ab") 10 | assert_equal(matched(), matcher.match("ab")) 11 | assert_equal(matched(), matcher.match("abc")) 12 | assert_equal(matched(), matcher.match("abcd")) 13 | assert_equal(unmatched("was 'a'"), matcher.match("a")) 14 | assert_equal(unmatched("was 'cab'"), matcher.match("cab")) 15 | 16 | 17 | @istest 18 | def starts_with_description_describes_value(): 19 | matcher = starts_with("ab") 20 | assert_equal("starts with 'ab'", matcher.describe()) 21 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py35,py36,py37,py38,py39,pypy,docs 3 | [testenv] 4 | changedir = {envtmpdir} 5 | deps=-r{toxinidir}/test-requirements.txt 6 | commands= 7 | nosetests {toxinidir}/tests 8 | pyflakes {toxinidir}/precisely {toxinidir}/tests 9 | [testenv:docs] 10 | deps= 11 | restructuredtext_lint==0.17.2 12 | pygments==2.1.3 13 | commands= 14 | rst-lint {toxinidir}/README.rst 15 | --------------------------------------------------------------------------------