├── tests ├── __init__.py ├── operators │ ├── __init__.py │ ├── conftest.py │ ├── pass_test.py │ ├── index_test.py │ ├── within_test.py │ ├── implements_test.py │ ├── test_match.py │ ├── length_test.py │ ├── start_end_test.py │ ├── type_test.py │ ├── present_test.py │ ├── none_test.py │ ├── attributes_test.py │ ├── properties_test.py │ ├── only_test.py │ ├── range_test.py │ ├── callable_test.py │ ├── raises_test.py │ ├── empty_test.py │ ├── keys_test.py │ ├── bool_test.py │ ├── contain_test.py │ └── been_called_test.py ├── runner_test.py ├── decorators_test.py ├── conftest.py ├── log_test.py ├── engine_test.py ├── api_test.py ├── exceptions_test.py ├── context_test.py ├── config_test.py ├── plugin_test.py ├── operator_test.py └── test_test.py ├── docs ├── history.rst ├── intro.rst ├── authors.rst ├── api.rst ├── references.rst ├── index.rst ├── contributing.rst ├── configuration.rst ├── license.rst ├── faq.rst ├── composition.rst ├── attributes-operators.rst ├── plugins.rst ├── style.rst ├── accessors-operators.rst ├── error-reporting.rst ├── integrations.rst └── getting-started.rst ├── requirements.txt ├── AUTHORS ├── MANIFEST.in ├── grappa ├── constants.py ├── reporters │ ├── message.py │ ├── reasons.py │ ├── information.py │ ├── expected.py │ ├── __init__.py │ ├── error.py │ ├── subject.py │ ├── diff.py │ ├── assertion.py │ ├── base.py │ └── code.py ├── empty.py ├── exceptions.py ├── log.py ├── operators │ ├── attributes.py │ ├── present.py │ ├── pass_test.py │ ├── none.py │ ├── equal.py │ ├── within.py │ ├── only.py │ ├── __init__.py │ ├── bool.py │ ├── callable.py │ ├── empty.py │ ├── raises.py │ ├── match.py │ ├── index.py │ ├── contain.py │ ├── length.py │ ├── been_called_with.py │ ├── property.py │ ├── keys.py │ ├── implements.py │ ├── been_called.py │ ├── type.py │ └── start_end.py ├── __init__.py ├── base.py ├── test_proxy.py ├── plugin.py ├── api.py ├── reporter.py ├── operator_dsl.py ├── config.py ├── template.py ├── context.py ├── runner.py ├── engine.py ├── assertion.py ├── decorators.py └── resolver.py ├── tox.ini ├── requirements-dev.txt ├── .editorconfig ├── .travis.yml ├── LICENSE ├── .gitignore ├── Makefile ├── .github └── workflows │ └── python-package.yml └── setup.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/operators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/runner_test.py: -------------------------------------------------------------------------------- 1 | # TODO 2 | -------------------------------------------------------------------------------- /tests/decorators_test.py: -------------------------------------------------------------------------------- 1 | # TODO 2 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../History.rst 2 | -------------------------------------------------------------------------------- /docs/intro.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colorama>=0.3.9,<2 2 | six~=1.14 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | * Tomás Aparicio - https://github.com/h2non 2 | -------------------------------------------------------------------------------- /tests/operators/conftest.py: -------------------------------------------------------------------------------- 1 | from .. import conftest # noqa 2 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | .. include:: ../AUTHORS 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE History.rst requirements-dev.txt requirements.txt 2 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API 4 | === 5 | 6 | .. automodule:: grappa 7 | :members: 8 | :undoc-members: 9 | :show-inheritance: 10 | -------------------------------------------------------------------------------- /grappa/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | 4 | # True if running Python 2.x 5 | IS_PY2 = sys.version_info[0] == 2 6 | 7 | # String types per Python version 8 | STR_TYPES = (str, unicode) if IS_PY2 else (bytes, str) # noqa 9 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = {py34,py35,py36} 3 | 4 | [testenv] 5 | setenv = 6 | PYTHONPATH = {toxinidir} 7 | deps = 8 | -r{toxinidir}/requirements-dev.txt 9 | commands = 10 | py.test . --cov paco --cov-report term-missing --flakes 11 | -------------------------------------------------------------------------------- /grappa/reporters/message.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .base import BaseReporter 3 | 4 | 5 | class MessageReporter(BaseReporter): 6 | 7 | title = 'Message' 8 | 9 | def run(self, error): 10 | return self.ctx.message 11 | -------------------------------------------------------------------------------- /grappa/reporters/reasons.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .base import BaseReporter 3 | 4 | 5 | class ReasonsReporter(BaseReporter): 6 | 7 | title = 'Reasons' 8 | 9 | def run(self, error): 10 | return getattr(error, 'reasons', None) 11 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | coveralls~=1.1 2 | flake8~=3.7.9 3 | pytest~=3.0.3 4 | pytest-mock~=2.0.0 5 | pytest-cov~=2.4.0 6 | pytest-flakes~=1.0.1 7 | Sphinx~=1.5.3 8 | sphinx-rtd-theme~=0.1.9 9 | python-coveralls~=2.9.0 10 | bumpversion~=0.5.3 11 | mock~=2.0.0 12 | -------------------------------------------------------------------------------- /grappa/reporters/information.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .base import BaseReporter 3 | 4 | 5 | class InformationReporter(BaseReporter): 6 | 7 | title = 'Information' 8 | 9 | def run(self, error): 10 | return self.from_operator('information', None) 11 | -------------------------------------------------------------------------------- /docs/references.rst: -------------------------------------------------------------------------------- 1 | References 2 | ---------- 3 | 4 | ``grappa`` was inspired by other libraries, such as: 5 | 6 | - **chai.js** - http://chaijs.com 7 | - **should.js** - https://shouldjs.github.io 8 | - **RSpec** - http://rspec.info 9 | - **Jasmine** - https://jasmine.github.io/ 10 | -------------------------------------------------------------------------------- /grappa/reporters/expected.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .subject import SubjectMessageReporter 3 | 4 | 5 | class ExpectedMessageReporter(SubjectMessageReporter): 6 | 7 | title = 'What we expected' 8 | 9 | # Attribute to lookup in the operator instance for custom message template 10 | attribute = 'expected' 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{py,rst,txt}] 4 | indent_style = space 5 | indent_size = 4 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | end_of_line = LF 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | end_of_line = LF 14 | 15 | [Makefile] 16 | indent_style = tab 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /tests/operators/pass_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_pass_function(should): 5 | def test(subject): 6 | return len(subject) == 3, [] 7 | 8 | 'foo' | should.pass_test(test) 9 | 'fo' | should.do_not.pass_test(test) 10 | 11 | with pytest.raises(AssertionError): 12 | 'foo' | should.do_not.pass_test(test) 13 | -------------------------------------------------------------------------------- /grappa/empty.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Empty(object): 5 | """ 6 | Empty object represents emptyness state in `grappa`. 7 | """ 8 | 9 | def __repr__(self): 10 | return 'Empty' 11 | 12 | def __len__(self): 13 | return 0 14 | 15 | 16 | # Object reference representing emptpyness 17 | empty = Empty() 18 | -------------------------------------------------------------------------------- /tests/operators/index_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_index(should): 5 | [1, 2, 3] | should.have.index(2) 6 | (1, 2, 3) | should.have.index(1) 7 | (1, 2, 3) | should.have.index(2).to.be.equal(3) 8 | 9 | [1] | should.have.length.of(1).to.have.index.at(0) 10 | 11 | with pytest.raises(AssertionError): 12 | [1, 2, 3] | should.have.index(4) 13 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from grappa.context import Context 3 | from grappa import should as _should, expect as _expect 4 | 5 | 6 | @pytest.fixture(scope='module') 7 | def ctx(): 8 | return Context() 9 | 10 | 11 | @pytest.fixture(scope='module') 12 | def should(): 13 | return _should 14 | 15 | 16 | @pytest.fixture(scope='module') 17 | def expect(): 18 | return _expect 19 | -------------------------------------------------------------------------------- /tests/log_test.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from grappa import log 3 | 4 | 5 | def test_log_module(should): 6 | log | should.be.a('module') 7 | 8 | log | should.have.property('handler') 9 | log | should.have.property('log') > should.be.instance.of(logging.Logger) 10 | 11 | (log 12 | | should.have.property('formatter') 13 | > should.be.instance.of(logging.Formatter)) 14 | -------------------------------------------------------------------------------- /tests/operators/within_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_should_within(should): 5 | 10 | should.be.between(5, 10) 6 | 101 | should.be.within(100, 102) 7 | 8 | # negation 9 | 11 | should.not_be.between(5, 9) 10 | 11 | with pytest.raises(AssertionError): 12 | 10 | should.not_be.between.to(5, 10) 13 | 14 | with pytest.raises(AssertionError): 15 | None | should.be.between.numbers(10, 10) 16 | -------------------------------------------------------------------------------- /tests/engine_test.py: -------------------------------------------------------------------------------- 1 | from grappa.engine import Engine, isoperator 2 | 3 | 4 | def test_engine(should): 5 | Engine | should.be.a('class') 6 | Engine | should.have.property('register') > should.be.a('function') 7 | 8 | 9 | class FakeOperator(object): 10 | operators = tuple() 11 | 12 | kind = 'accessor' 13 | 14 | def run(self): 15 | pass 16 | 17 | 18 | def test_isoperator(should): 19 | FakeOperator() | should.pass_function(isoperator) 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | - "3.5" 6 | - "3.6" 7 | - "3.7" 8 | - "3.8" 9 | - "3.9" 10 | - pypy3 11 | - nightly 12 | 13 | jobs: 14 | allow_failures: 15 | - python: nightly 16 | include: 17 | - python: pypy 18 | env: CRYPTOGRAPHY_ALLOW_OPENSSL_102=true 19 | 20 | sudo: false 21 | 22 | install: 23 | - pip install -r requirements-dev.txt 24 | - pip install -r requirements.txt 25 | 26 | script: 27 | - make lint 28 | - make test 29 | - make coverage 30 | 31 | after_success: 32 | coveralls 33 | -------------------------------------------------------------------------------- /tests/operators/implements_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_implements(should): 5 | class foo(object): 6 | foo = None 7 | 8 | def bar(self): 9 | pass 10 | 11 | def baz(self): 12 | pass 13 | 14 | foo | should.implements.methods('bar', 'baz') 15 | foo | should.do_not.implements.methods('foo') 16 | 17 | with pytest.raises(AssertionError): 18 | foo | should.implement.methods('foo', 'faa') 19 | 20 | with pytest.raises(AssertionError): 21 | foo | should.implements.methods(2, False) 22 | -------------------------------------------------------------------------------- /tests/api_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import grappa 3 | from grappa import api 4 | 5 | 6 | def test_api(should): 7 | for symbol in api.__all__: 8 | grappa | should.have.property(symbol) 9 | 10 | 11 | def test_should_api_constructor(should): 12 | should('foo').to.be.equal('foo') 13 | 14 | with pytest.raises(AssertionError): 15 | should('foo').to.be.equal('bar') 16 | 17 | 18 | def test_expect_api_constructor(expect): 19 | expect('foo').to.be.equal('foo') 20 | 21 | with pytest.raises(AssertionError): 22 | expect('foo').to.be.equal('bar') 23 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Contents 2 | -------- 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | intro 8 | getting-started 9 | style 10 | error-reporting 11 | attributes-operators 12 | accessors-operators 13 | matchers-operators 14 | composition 15 | configuration 16 | plugins 17 | integrations 18 | faq 19 | api 20 | contributing 21 | authors 22 | references 23 | license 24 | history 25 | 26 | 27 | .. include:: intro.rst 28 | 29 | 30 | Indices and tables 31 | ================== 32 | 33 | * :ref:`genindex` 34 | * :ref:`modindex` 35 | * :ref:`search` 36 | -------------------------------------------------------------------------------- /tests/operators/test_match.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pytest 3 | 4 | 5 | def test_expect_match(should): 6 | 'Hello foo bar' | should.match(r'Hello \w+ bar') 7 | 'Hello foo BAR' | should.match.regexp(r'Hello \w+ bar', re.I) 8 | 'Hello foo BAR' | should.match.regexp('BAR', re.I) 9 | 10 | with pytest.raises(AssertionError): 11 | should('foo bar').match('baz') 12 | 13 | with pytest.raises(AssertionError): 14 | 'foo bar' | should.match.value('baz') 15 | 16 | with pytest.raises(AssertionError): 17 | 'Hello foo BAR' | should.match.regexp(r'Hello \w+ bar') 18 | -------------------------------------------------------------------------------- /tests/exceptions_test.py: -------------------------------------------------------------------------------- 1 | from grappa.exceptions import GrappaAssertionError 2 | 3 | 4 | def test_exceptions(should): 5 | GrappaAssertionError | should.be.a('class') 6 | 7 | # Test assertion instance 8 | GrappaAssertionError() | should.be.instance.of(AssertionError) 9 | 10 | # Test assertion properties 11 | err = GrappaAssertionError(error='foo', reasons=['bar'], operator=True) 12 | err | should.have.property('error') > should.be.equal.to('foo') 13 | err | should.have.property('reasons') > should.be.equal.to(['bar']) 14 | err | should.have.property('operator') > should.be.true 15 | -------------------------------------------------------------------------------- /grappa/reporters/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | assertion, code, diff, error, expected, 3 | information, message, reasons, subject 4 | ) 5 | 6 | # Symbols to export 7 | __all__ = ('reporters',) 8 | 9 | # Stores error message reporters 10 | # Reporters will be executed in definition order. 11 | reporters = [ 12 | assertion.AssertionReporter, 13 | message.MessageReporter, 14 | error.UnexpectedError, 15 | reasons.ReasonsReporter, 16 | expected.ExpectedMessageReporter, 17 | subject.SubjectMessageReporter, 18 | information.InformationReporter, 19 | diff.DiffReporter, 20 | code.CodeReporter, 21 | ] 22 | -------------------------------------------------------------------------------- /tests/operators/length_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_expect_length(should): 5 | [] | should.have.length.of(0) 6 | 'foo' | should.have.length.of(3) | should.have.length(3) 7 | 'foo' | should.not_have.length.of(4) 8 | 'foo' | should.not_have.length.of(0) | should.not_have.length.of(2) 9 | 10 | with pytest.raises(AssertionError): 11 | 'foo' | should.have.length(0) 12 | 13 | with pytest.raises(AssertionError): 14 | 'foo' | should.have.length.of(3) | should.have.length(5) 15 | 16 | with pytest.raises(AssertionError): 17 | None | should.have.length(0) 18 | 19 | with pytest.raises(AssertionError): 20 | 'foo' | should.not_have.length.of(3) 21 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | ``grappa`` is actively developed and improved. As in any other open source project, community help is always welcome. 5 | 6 | These are some of the tasks where community help is highly appreciated: 7 | 8 | - Add more assertion operators. 9 | - Improve error reporting with better messages. 10 | - Improve documentation and examples. 11 | - Improve test coverage. 12 | - Implement pending features (see `Github issues`_). 13 | - Create or improve plugins (http, server...) 14 | - Write a post introducing ``grappa``, along with real use cases. 15 | - Potential core refactors and improvements. 16 | 17 | 18 | .. _`Github issues`: https://github.com/grappa-py/grappa/labels/enhancement 19 | -------------------------------------------------------------------------------- /grappa/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class GrappaAssertionError(AssertionError): 5 | """ 6 | GrappaAssertionError represents an internal grappa assertion error 7 | and it is used internally to encapsulate and share state on 8 | assertion errors. 9 | 10 | Attributes: 11 | error (Exception): stores original error exception. 12 | reasons (list|tuple[str]): stores error reason details. 13 | operator (Operator): original operator instance that failed. 14 | """ 15 | 16 | def __init__(self, error=None, reasons=None, operator=None): 17 | self.error = error 18 | self.reasons = reasons or getattr(error, 'reasons', None) 19 | self.operator = operator 20 | -------------------------------------------------------------------------------- /tests/operators/start_end_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from collections import OrderedDict 3 | 4 | 5 | def test_start_end_with(should): 6 | # Strings 7 | 'foo' | should.end_with.letter('o') 8 | 'bar' | should.start_with.character('b') 9 | 10 | # Iterables 11 | [1, 2, 3] | should.start_with.number(1) 12 | [1, 2, 3] | should.end_with.number(2, 3) 13 | iter([1, 2, 3]) | should.start_with.numbers(1, 2) 14 | 15 | # Mappings 16 | OrderedDict([('foo', 0), ('bar', 1)]) | should.start_with('foo', 'bar') 17 | 18 | with pytest.raises(AssertionError): 19 | [1, 2, 3] | should.start_with.numbers(2, 3) 20 | 21 | with pytest.raises(AssertionError): 22 | 'foo' | should.start_with.letter('o') 23 | -------------------------------------------------------------------------------- /tests/operators/type_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_should_type(should): 5 | 'foo' | should.be.a(str) 6 | 4 | should.be.an(int) 7 | {} | should.be.an(dict) 8 | tuple() | should.be.an(tuple) 9 | tuple() | should.be.instance.of('tuple') 10 | (lambda: True) | should.be.instance.of('lambda') 11 | 12 | def foo(): yield 1 13 | foo() | should.be.a('generator') 14 | 15 | class bar(object): 16 | def baz(self): 17 | pass 18 | 19 | bar | should.be.a('class') 20 | bar().baz | should.be.a('method') 21 | 22 | with pytest.raises(AssertionError): 23 | 'foo' | should.be.an(int) 24 | 25 | with pytest.raises(AssertionError): 26 | [1, 2, 3] | should.be.a('tuple') 27 | -------------------------------------------------------------------------------- /grappa/log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | import logging 4 | from colorama import Fore, Style 5 | 6 | # Create logger instance for grappa 7 | log = logging.getLogger('grappa') 8 | 9 | # Custom log message formatter with colored output 10 | formatter = logging.Formatter( 11 | u'{}=>{} {}[grappa]{} {}%(asctime)s{} | %(message)s'.format( 12 | Fore.GREEN, 13 | Style.RESET_ALL, 14 | Fore.MAGENTA, 15 | Style.RESET_ALL, 16 | Fore.CYAN, 17 | Style.RESET_ALL, 18 | ) 19 | ) 20 | 21 | # Create log handler with custom text formatter 22 | handler = logging.StreamHandler(sys.stdout) 23 | handler.setFormatter(formatter) 24 | log.addHandler(handler) 25 | 26 | # Set default logging level 27 | log.setLevel(logging.CRITICAL) 28 | -------------------------------------------------------------------------------- /grappa/reporters/error.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .base import BaseReporter 3 | 4 | 5 | class UnexpectedError(BaseReporter): 6 | 7 | title = 'However, an unexpected exception was raised' 8 | 9 | default_reasons = [ 10 | 'The assertion raised an unexpected exception, but it should not.', 11 | 'This behavior might be an bug within the testing library or ' 12 | 'in a third-party test operator.' 13 | ] 14 | 15 | def run(self, error): 16 | err = getattr(error, 'error', None) 17 | if not err: 18 | return None 19 | 20 | # Load error reasons 21 | error.reasons = getattr(error, 'reasons', self.default_reasons) 22 | 23 | # Normalize error message 24 | return self.indentify(str(err)) 25 | -------------------------------------------------------------------------------- /grappa/operators/attributes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..decorators import attribute 3 | 4 | 5 | @attribute( 6 | operators=( 7 | 'to', 'has', 'have', 'satisfy', 'that', 'that_is', 8 | 'satisfies', 'include', 'do', '_is', 'which', 'which_is' 9 | ) 10 | ) 11 | def be(ctx): 12 | """ 13 | Semantic attributes providing chainable declarative DSL 14 | for assertions. 15 | """ 16 | pass 17 | 18 | 19 | @attribute(operators=( 20 | 'not_to', 'to_not', 'does_not', 'do_not', '_not', 'not_satisfy', 21 | 'not_have', 'not_has', 'have_not', 'has_not', 'dont', 'is_not', 22 | 'which_not', 'that_not' 23 | )) 24 | def not_be(ctx): 25 | """ 26 | Semantic negation attributes providing chainable declarative DSL 27 | for assertions. 28 | """ 29 | ctx.negate = True 30 | -------------------------------------------------------------------------------- /tests/context_test.py: -------------------------------------------------------------------------------- 1 | from grappa.context import Context 2 | 3 | 4 | def test_context(should): 5 | ctx = Context() 6 | 7 | # Setter/getter magic methods 8 | ctx | should.have.property('has') 9 | ctx | should.have.property('__setattr__') 10 | ctx | should.have.property('__getattr__') 11 | 12 | # Defaults options 13 | ctx | should.have.property('negate') > should.be.false 14 | 15 | # Test define properties 16 | ctx.foo = 'bar' 17 | ctx.bar = True 18 | ctx | should.have.property('foo') > should.be.equal.to('bar') 19 | ctx | should.have.property('bar') > should.be.true 20 | 21 | ctx.has('foo') | should.be.true 22 | ctx.has('baz') | should.be.false 23 | 24 | ctx.__repr__() | should.contain("'negate': False") 25 | ctx.__repr__() | should.contain("'foo': 'bar'") 26 | ctx.__repr__() | should.contain("'bar': True") 27 | -------------------------------------------------------------------------------- /docs/configuration.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | ``grappa`` allows a small level of configuration to customize behavior and 5 | error reporting. 6 | 7 | Options 8 | ------- 9 | 10 | - *use_colors* ``bool`` - Defaults to `True` - Enables/disables showing ANSI colored text in error exception messages. 11 | - *show_code* ``bool`` - Defaults to `True` - Enables/disables showing code fragment with the line that originally caused the error in the exception message. 12 | - *debug* ``bool`` - Defaults to `False` - Enables internal debug mode. This would output a lot of stuff into ``stdout``. Only intended to be used for ``grappa`` developers. 13 | 14 | New configuration option can be added in the future 15 | 16 | Example 17 | ------- 18 | 19 | .. code-block:: python 20 | 21 | import grappa 22 | 23 | # Disables code output in error messages 24 | grappa.config.show_code = False 25 | # Enables colored error messages 26 | grappa.config.use_colors = True 27 | -------------------------------------------------------------------------------- /grappa/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | """ 3 | `grappa` provides two different testing styles: `should` and `expect`. 4 | 5 | should 6 | ------ 7 | 8 | Example using ``should`` style:: 9 | 10 | from grappa import should 11 | 12 | should('foo').be.equal.to('foo') 13 | 'foo' | should.be.equal.to('foo') 14 | 15 | expect 16 | ------ 17 | 18 | Example using ``expect`` style:: 19 | 20 | from grappa import expect 21 | 22 | expect([1, 2, 3]).to.contain([2, 3]) 23 | [1, 2, 3] | expect.to.contain([2, 3]) 24 | 25 | 26 | For assertion operators and aliases, see `operators documentation`_. 27 | 28 | .. _`operators documentation`: operators.html 29 | 30 | Reference 31 | --------- 32 | """ 33 | 34 | # Export public API module members 35 | from .api import * # noqa 36 | from .api import __all__ # noqa 37 | 38 | # Package metadata 39 | __author__ = 'Tomas Aparicio' 40 | __license__ = 'MIT' 41 | 42 | # Current package version 43 | __version__ = '1.0.1' 44 | -------------------------------------------------------------------------------- /grappa/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import functools 3 | 4 | 5 | # List of unsupported binary operators 6 | unsupported_operators = ( 7 | # method # operator symbol 8 | ('rshit', '>>'), 9 | ('lshift', '<<'), 10 | ('add', '+'), 11 | ('and', '&'), 12 | ) 13 | 14 | 15 | def not_implemented(symbol, self, value): 16 | """ 17 | Raises an `NotImplementedError` exception when called. 18 | """ 19 | raise NotImplementedError('"{}" operator is not overloaded'.format( 20 | symbol 21 | )) 22 | 23 | 24 | class BaseTest(object): 25 | """ 26 | BaseTest implements magic method that explicitly raises an exception 27 | if consumed with unsupported binary operators. 28 | 29 | Note: methods are defined dynamically. See below. 30 | """ 31 | 32 | 33 | # Dynamically set class methods 34 | for name, symbol in unsupported_operators: 35 | setattr(BaseTest, '__{}__'.format(name), 36 | functools.partial(not_implemented, symbol)) 37 | -------------------------------------------------------------------------------- /tests/config_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import grappa 3 | from grappa.config import Config 4 | 5 | 6 | def test_config(should): 7 | grappa | should.have.property('config') > should.be.instance.of(Config) 8 | 9 | # Create new config 10 | conf = Config() 11 | 12 | # Clone 13 | assert conf.__dict__['opts'] is not grappa.config.__dict__['opts'] 14 | 15 | # Setter/getter magic methods 16 | conf | should.have.property('__setattr__') 17 | conf | should.have.property('__getattr__') 18 | 19 | # Defaults options 20 | (conf 21 | | should.have.property('defaults') 22 | > should.be.equal.to(grappa.config.defaults)) 23 | 24 | # Test define properties 25 | conf.debug = False 26 | conf.show_code = True 27 | conf | should.have.property('debug') > should.be.false 28 | conf | should.have.property('show_code') > should.be.true 29 | 30 | # Invalid config option should raise an error 31 | with pytest.raises(ValueError): 32 | conf.foo = 'bar' 33 | -------------------------------------------------------------------------------- /tests/operators/present_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_should_present(should): 5 | 1 | should.be.present 6 | 'foo' | should.be.present 7 | [1, 2, 3] | should.be.present 8 | (1, 2, 3) | should.be.present 9 | iter([1]) | should.be.present 10 | {'foo': True} | should.be.present 11 | 12 | '' | should.not_be.present 13 | 0 | should.not_be.present 14 | None | should.not_be.present 15 | [] | should.not_be.present 16 | tuple() | should.not_be.present 17 | dict() | should.not_be.present 18 | 19 | with pytest.raises(AssertionError): 20 | 'foo' | should.do_not.exists 21 | 22 | with pytest.raises(AssertionError): 23 | None | should.be.present 24 | 25 | with pytest.raises(AssertionError): 26 | None | should.exists 27 | 28 | with pytest.raises(AssertionError): 29 | 0 | should.exists 30 | 31 | with pytest.raises(AssertionError): 32 | [] | should.exists 33 | 34 | with pytest.raises(AssertionError): 35 | tuple() | should.exists 36 | -------------------------------------------------------------------------------- /tests/plugin_test.py: -------------------------------------------------------------------------------- 1 | import grappa 2 | from grappa.engine import Engine 3 | 4 | 5 | def test_plugin_api(should): 6 | grappa | should.be.a('module') 7 | grappa | should.have.property('use') > should.be.a('function') 8 | 9 | 10 | def test_register_plugin_function(should): 11 | state = {'called': False} 12 | 13 | def plugin_stub(engine): 14 | engine | should.be.equal.to(Engine) 15 | engine | should.have.property('register') > should.be.a('function') 16 | state['called'] = True 17 | 18 | grappa.use(plugin_stub) 19 | state | should.be.have.key('called') > should.be.true 20 | 21 | 22 | def test_register_plugin_method(should): 23 | state = {'called': False} 24 | 25 | class plugin_stub(object): 26 | @staticmethod 27 | def register(engine): 28 | engine | should.be.equal.to(Engine) 29 | engine | should.have.property('register') > should.be.a('function') 30 | state['called'] = True 31 | 32 | grappa.use(plugin_stub) 33 | state | should.be.have.key('called') > should.be.true 34 | -------------------------------------------------------------------------------- /grappa/test_proxy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import inspect 3 | from .test import test 4 | 5 | 6 | class TestProxy(object): 7 | """ 8 | TestProxy acts like a delegator intermediate proxy between public 9 | API calls and `Test` instances defining the test style. 10 | 11 | Arguments: 12 | style (str): test style between `should` or `expect`. 13 | """ 14 | 15 | def __init__(self, style): 16 | self._style = style 17 | 18 | def __getattr__(self, name): 19 | """ 20 | Overloads object attribute accessory proxy to the currently used 21 | operator and parent test instance. 22 | """ 23 | _test = getattr(test, name) 24 | 25 | if inspect.ismethod(_test): 26 | return _test 27 | 28 | _test._ctx.style = self._style 29 | return _test 30 | 31 | def __call__(self, *args, **kw): 32 | """ 33 | Makes proxy object itself callable, delegating the invokation 34 | to a new test instance. 35 | """ 36 | _test = test(*args, **kw) 37 | _test._ctx.style = self._style 38 | return _test 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tomás Aparicio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/operators/none_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from grappa.operators.none import NoneOperator 3 | 4 | 5 | def test_should_none(should): 6 | None | should.be.none 7 | 'foo' | should.not_be.none 8 | 9 | with pytest.raises(RuntimeError): 10 | 'foo' | should.not_be.none.to.be.equal(None) 11 | 12 | with pytest.raises(AssertionError): 13 | False | should.be.none 14 | 15 | with pytest.raises(AssertionError): 16 | None | should.not_be.none 17 | 18 | with pytest.raises(AssertionError): 19 | 'foo' | should.be.none 20 | 21 | with pytest.raises(AssertionError): 22 | [1, 2, 3] | should.be.none 23 | 24 | 25 | def test_expect_none(expect): 26 | None | expect.to.be.none 27 | 28 | with pytest.raises(AssertionError): 29 | False | expect.to.be.none 30 | 31 | with pytest.raises(AssertionError): 32 | 'foo' | expect.to.be.none 33 | 34 | with pytest.raises(AssertionError): 35 | [1, 2, 3] | expect.to.be.none 36 | 37 | 38 | def test_none_operator(ctx): 39 | assert NoneOperator(ctx).match(None) == (True, []) 40 | assert NoneOperator(ctx).match(1) == (False, []) 41 | -------------------------------------------------------------------------------- /grappa/plugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import inspect 3 | 4 | from .log import log 5 | from .engine import Engine 6 | 7 | 8 | def use(plugin): 9 | """ 10 | Register plugin in grappa. 11 | 12 | `plugin` argument can be a function or a object that implement `register` 13 | method, which should accept one argument: `grappa.Engine` instance. 14 | 15 | Arguments: 16 | plugin (function|module): grappa plugin object to register. 17 | 18 | Raises: 19 | ValueError: if `plugin` is not a valid interface. 20 | 21 | Example:: 22 | 23 | import grappa 24 | 25 | class MyOperator(grappa.Operator): 26 | pass 27 | 28 | def my_plugin(engine): 29 | engine.register(MyOperator) 30 | 31 | grappa.use(my_plugin) 32 | """ 33 | log.debug('register new plugin: {}'.format(plugin)) 34 | 35 | if inspect.isfunction(plugin): 36 | return plugin(Engine) 37 | 38 | if plugin and hasattr(plugin, 'register'): 39 | return plugin.register(Engine) 40 | 41 | raise ValueError('invalid plugin: must be a function or ' 42 | 'implement register() method') 43 | -------------------------------------------------------------------------------- /grappa/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Import test module 4 | from .test import Test 5 | # Import test proxy used for style delegation 6 | from .test_proxy import TestProxy 7 | # Expose plugin module as part of the public API 8 | from .plugin import use 9 | # Expose config module as part of the public API 10 | from .config import config 11 | # Expose decorator for operators factory 12 | from .decorators import operator, register, attribute 13 | # Expose Operatocdr base class 14 | from .operator import Operator 15 | # Required to load built-in operators 16 | from . import operators 17 | 18 | # Initialize colorama stdout/stderr interceptor 19 | import colorama 20 | colorama.init() 21 | 22 | # IMPORTANT! Autoload built-in operators 23 | operators.load() 24 | 25 | # Global test instance for "should" testing declaration style 26 | should = TestProxy('should') 27 | 28 | # Global test instance for "expect" testing declaration style 29 | expect = TestProxy('expect') 30 | 31 | # Module symbols to export as public API 32 | __all__ = ( 33 | 'should', 34 | 'expect', 35 | 'Test', 36 | 'use', 37 | 'config', 38 | 'Operator', 39 | 'attribute', 40 | 'operator', 41 | 'register' 42 | ) 43 | -------------------------------------------------------------------------------- /grappa/reporter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .reporters import reporters 3 | from .template import ErrorTemplate 4 | 5 | 6 | class ErrorReporter(object): 7 | """ 8 | ErrorReporter renders a given assertion error based on the 9 | built-in error reporters. 10 | """ 11 | 12 | def __init__(self, ctx): 13 | self.ctx = ctx 14 | 15 | def render_reporters(self, template, error): 16 | for Reporter in reporters: 17 | report = Reporter(self.ctx, error).run(error) 18 | template.block(Reporter.title, report) 19 | 20 | def run(self, error): 21 | # Create error template generator 22 | template = ErrorTemplate() 23 | 24 | # Trigger registered reporters 25 | try: 26 | self.render_reporters(template, error) 27 | except Exception as err: 28 | err.__legit__ = True 29 | return err 30 | 31 | # Create assertion error 32 | err = AssertionError(template.render()) 33 | 34 | # Flag error as grappa generated error 35 | err.__grappa__ = True 36 | err.__cause__ = getattr(error, 'error', error) 37 | err.context = self.ctx 38 | 39 | return err 40 | -------------------------------------------------------------------------------- /tests/operators/attributes_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from grappa.test import Test as GrappaTest 3 | from grappa import should, expect 4 | 5 | 6 | @pytest.mark.parametrize('operator', ( 7 | 'to', 'has', 'have', 'include', 'do', 'that', 'which', '_is' 8 | )) 9 | def test_assertion_attributes(operator): 10 | assert isinstance(getattr(should, operator), GrappaTest) 11 | assert getattr(should, operator)._ctx.negate is False 12 | assert isinstance(getattr(expect, operator), GrappaTest) 13 | assert getattr(expect, operator)._ctx.negate is False 14 | 15 | 16 | @pytest.mark.parametrize('operator', ( 17 | 'not_to', 'to_not', 'does_not', 'do_not', '_not', 18 | 'not_have', 'not_has', 'have_not', 'has_not', 'dont' 19 | )) 20 | def test_negation_attributes(operator): 21 | assert isinstance(getattr(should, operator), GrappaTest) 22 | assert getattr(should, operator)._ctx.negate 23 | assert isinstance(getattr(expect, operator), GrappaTest) 24 | assert getattr(expect, operator)._ctx.negate 25 | 26 | 27 | def test_negation_called_before_non_negation_attributes(): 28 | expect(False).to_not.have.true 29 | expect(False).not_to.be.true 30 | 31 | False | should.to_not.have.true 32 | False | should.not_to.be.true 33 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | .. code-block:: bash 5 | 6 | MIT License 7 | 8 | Copyright (c) 2017 Tomás Aparicio 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | -------------------------------------------------------------------------------- /tests/operators/properties_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_expect_properties(should): 5 | class foo(object): 6 | foo = 'bar' 7 | 8 | class baz(object): 9 | foo = 'bar' 10 | 11 | def bar(self): 12 | pass 13 | 14 | foo() | should.have.property('foo') 15 | foo() | should.have.property('foo') > should.be.equal.to('bar') 16 | 17 | should(foo()).have.property('foo') > should.be.equal.to('bar') 18 | should(foo()).have.property('foo').which.should.be.equal.to('bar') 19 | 20 | should(foo()).have.property('bar').which.should.be.a('method') 21 | should(foo()).have.properties('bar', 'foo') 22 | 23 | (foo() 24 | | should.have.property('foo') 25 | > should.be.a('string') 26 | > should.have.length.of(3) 27 | > should.be.equal.to('bar')) 28 | 29 | (foo() 30 | | should.have.property('baz') 31 | > should.be.an('object') 32 | > should.have.property('foo') 33 | > should.be.equal.to('bar')) 34 | 35 | with pytest.raises(AssertionError): 36 | should(foo()).have.property('boo') 37 | 38 | with pytest.raises(AssertionError): 39 | should(foo()).have.property('boo') 40 | 41 | with pytest.raises(AssertionError): 42 | should(foo()).have.property('foo') > should.be.equal.to('foo') 43 | 44 | with pytest.raises(AssertionError): 45 | should(foo()).have.property('foo').which.should.be.equal.to('pepe') 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | *.tar.gz 27 | .DS_Store 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # IPython Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # dotenv 81 | .env 82 | 83 | # virtualenv 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | -------------------------------------------------------------------------------- /tests/operator_test.py: -------------------------------------------------------------------------------- 1 | from grappa.operator import OperatorTypes, Operator 2 | 3 | 4 | def test_operator_types(should): 5 | OperatorTypes | should.be.a('class') 6 | 7 | (OperatorTypes 8 | | should.have.property('ATTRIBUTE') > should.be.equal.to('attribute')) 9 | 10 | (OperatorTypes 11 | | should.have.property('ACCESSOR') > should.be.equal.to('accessor')) 12 | 13 | (OperatorTypes 14 | | should.have.property('MATCHER') > should.be.equal.to('matcher')) 15 | 16 | 17 | def test_operator(should): 18 | Operator | should.be.a('class') 19 | 20 | Operator | should.have.property('run') 21 | Operator | should.have.property('Type') > should.be.equal.to(OperatorTypes) 22 | 23 | (Operator 24 | | should.have.property('Dsl') 25 | > should.be.a('module') 26 | > should.have.attributes('Message', 'Help', 27 | 'Reference', 'Description')) 28 | 29 | Operator | should.have.property('kind') > should.be.equal.to('matcher') 30 | Operator | should.have.property('operators') > should.have.length.of(0) 31 | Operator | should.have.property('aliases') > should.have.length.of(0) 32 | Operator | should.have.property('suboperators') > should.have.length.of(0) 33 | Operator | should.have.property('value') 34 | Operator | should.have.property('expected') 35 | Operator | should.have.property('operator_name') 36 | Operator | should.have.property('subject_message') 37 | Operator | should.have.property('expected_message') 38 | -------------------------------------------------------------------------------- /tests/operators/only_test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import pytest 3 | try: 4 | from unittest import mock 5 | except ImportError: 6 | import mock 7 | 8 | 9 | def test_should_contain_only(should): 10 | [1, 2] | should.contain.only(1, 2) 11 | # dicts/list + different order in expected values 12 | [{'foo': 'bar'}, 2] | should.contain.only(2, {'foo': 'bar'}) 13 | ('foo', 'bar', 123) | should.contain.only('bar', 123, 'foo') 14 | 'hello' | should.contain.only('hello') # string case 15 | # chainability 16 | ([], 'ab', 42) | should.contain.only(42, 'ab', []) | should.have.length(3) 17 | 18 | with pytest.raises(AssertionError): 19 | ['a', '∆˚'] | should.contain.only('a') # missing items 20 | 21 | with pytest.raises(AssertionError): 22 | 'abc' | should.contain.only('def') # string case unequal 23 | 24 | with pytest.raises(AssertionError): 25 | [321, '∆˚'] | should.contain.only('∆˚', 'b') # different item 26 | 27 | with pytest.raises(AssertionError): 28 | # too many items 29 | [ 30 | {'foo': 'bar'}, 31 | {'meaning': 42} 32 | ] | should.contain.only([], {'foo': 'bar'}, {'meaning': 42}) 33 | 34 | 35 | def test_should_contain_only_string_case(should): 36 | """Should log a warning when using the only operator for strings.""" 37 | with mock.patch('grappa.operators.only.log') as log: 38 | 'lala' | should.contain.only('lala') 39 | log.warn.assert_called_once_with( 40 | 'String comparison using "only" is not advised' 41 | ) 42 | -------------------------------------------------------------------------------- /grappa/operator_dsl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from colorama import Fore, Style 3 | 4 | 5 | class Message(object): 6 | 7 | def __init__(self, allowance=None, negation=None): 8 | self.allowance = allowance 9 | self.negation = negation or allowance 10 | 11 | def render(self, negation=False): 12 | return self.negation if negation else self.allowance 13 | 14 | 15 | class Description(object): 16 | 17 | ARROW = '>' 18 | 19 | def __init__(self, *text): 20 | self.value = text 21 | 22 | def render(self, separator): 23 | lines = '\n{}'.format(separator).join(self.value) 24 | return Description.ARROW + ' ' + lines 25 | 26 | 27 | class Reference(object): 28 | 29 | TOKEN = '=>' 30 | TEXT = 'Reference' 31 | 32 | def __init__(self, url=None): 33 | self.url = url 34 | 35 | def render(self, separator): 36 | return separator + '{}{}{} {}{}: {}{}'.format( 37 | Fore.CYAN, 38 | Reference.TOKEN, 39 | Style.RESET_ALL, 40 | Fore.GREEN, 41 | Reference.TEXT, 42 | self.url, 43 | Style.RESET_ALL, 44 | ) 45 | 46 | 47 | class Help(object): 48 | 49 | def __init__(self, description=None, *references): 50 | self.description = description 51 | self.references = references 52 | 53 | def render(self, separator): 54 | description = self.description.render(separator) 55 | references = [ref.render(separator) for ref in self.references] 56 | return '\n'.join([description] + references) 57 | -------------------------------------------------------------------------------- /grappa/operators/present.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..operator import Operator 3 | 4 | 5 | class PresentOperator(Operator): 6 | """ 7 | Asserts if a given subject is not ``None`` or a negative value 8 | if evaluated via logical unary operator. 9 | 10 | This operator is the opposite of ``empty``. 11 | 12 | Example:: 13 | 14 | # Should style 15 | 'foo' | should.be.present 16 | 17 | # Should style - negation form 18 | '' | should.not_be.present 19 | 20 | # Expect style 21 | 'foo' | expect.to.be.present 22 | 23 | # Expect style - negation form 24 | False | expect.to_not.be.present 25 | """ 26 | 27 | # Is the operator a keyword 28 | kind = Operator.Type.ACCESSOR 29 | 30 | # Disable diff report 31 | show_diff = False 32 | 33 | # Operator keywords 34 | operators = ('present', 'exists') 35 | 36 | # Expected 37 | expected = 'a value which is not None and has data' 38 | 39 | # Expected template message 40 | expected_message = Operator.Dsl.Message( 41 | 'a value that is not None and its length is not "0"', 42 | 'a value that is None or its length is "0"' 43 | ) 44 | 45 | # Assertion information 46 | information = ( 47 | Operator.Dsl.Help( 48 | Operator.Dsl.Description( 49 | 'An present object is the opposite of an empty object.' 50 | ) 51 | ), 52 | ) 53 | 54 | def match(self, subject): 55 | if subject is None: 56 | return False, ['subject is "None"'] 57 | 58 | return bool(subject is not None and subject), [] 59 | -------------------------------------------------------------------------------- /tests/test_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_context_manager(should): 5 | 'foo' | should.to.be.equal('foo') 6 | 7 | with should('foo'): 8 | should.be.equal('foo') 9 | should.have.length.of(3) 10 | should.have.type('string') 11 | 12 | with should('foo'): 13 | should.be.a('string').which.has.length.of(3) 14 | should.be.equal('foo') 15 | 16 | should('foo').be.equal('foo') 17 | 'foo' | should.be.equal('foo') 18 | 19 | with pytest.raises(AssertionError): 20 | with should('foo'): 21 | should.be.equal('foo') 22 | should.have.length.of(5) 23 | 24 | 25 | def test_all_assertions(should): 26 | 'foo' | should.all(should.be.a('string'), should.have.length.to(3)) 27 | 28 | 'foo' | should.any(should.be.a('number'), should.have.length.to(3)) 29 | 30 | with pytest.raises(AssertionError): 31 | 'foo' | should.all(should.be.a('string'), should.have.length.to(5)) 32 | 33 | with pytest.raises(AssertionError): 34 | 'foo' | should.any(should.be.a('number'), should.have.length.to(5)) 35 | 36 | 37 | def test_assertion_chaining(should): 38 | 'foo' | should.have.length.between.range(2, 5) 39 | 40 | ('foo' 41 | | should.have.length.lower.than(4) 42 | | should.have.length.higher.than(2)) 43 | 44 | with pytest.raises(AssertionError): 45 | 'foo' | should.have.length.lower.than(4) | should.have.length.higher(5) 46 | 47 | 48 | def test_custom_message_error(should): 49 | with pytest.raises(AssertionError) as err: 50 | 'foo' | should.be.equal('bar', msg='invalid string') 51 | 52 | str(err.value) | should.contain('invalid string') 53 | -------------------------------------------------------------------------------- /grappa/operators/pass_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import inspect 3 | from ..operator import Operator 4 | 5 | 6 | class PassTestOperator(Operator): 7 | """ 8 | Asserts if a given function passes the test. 9 | 10 | Example:: 11 | 12 | # Should style 13 | 'foo' | should.pass_test(lambda x: len(x) > 2) 14 | [1, 2, 3] | should.pass_function(lambda x: 2 in x) 15 | 16 | # Should style - negation form 17 | 'foo' | should.do_not.pass_test(lambda x: len(x) > 3) 18 | [1, 2, 3] | should.do_not.pass_function(lambda x: 5 in x) 19 | 20 | # Expect style 21 | 'foo' | expect.to.pass_test(lambda x: len(x) > 2) 22 | [1, 2, 3] | expect.to.pass_function(lambda x: 2 in x) 23 | 24 | # Expect style - negation form 25 | 'foo' | expect.to_not.pass_test(lambda x: len(x) > 3) 26 | [1, 2, 3] | expect.to_not.pass_function(lambda x: 5 in x) 27 | """ 28 | 29 | # Is the operator a keyword 30 | kind = Operator.Type.MATCHER 31 | 32 | # Operator keywords 33 | operators = ('pass_test', 'pass_function') 34 | 35 | # Expected message template 36 | expected_message = Operator.Dsl.Message( 37 | 'a value that passes the test function', 38 | 'a value that does not pass the test function', 39 | ) 40 | 41 | # Subject template message 42 | subject_message = Operator.Dsl.Message( 43 | 'an value of type "{type}" with value "{value}"', 44 | ) 45 | 46 | def match(self, subject, fn): 47 | if not any([inspect.isfunction(fn) or inspect.ismethod(fn)]): 48 | return False, ['matcher argument must be a function or method'] 49 | return fn(subject) 50 | -------------------------------------------------------------------------------- /tests/operators/range_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_expect_below(should): 5 | 10 | should.be.below(100) 6 | 1 | should.be.below.of(2) 7 | 10 | should.be.lower.than(100) 8 | 9 | # negation 10 | 10 | should.not_be.below.of(2) 11 | 10 | should.not_be.lower.than(2) 12 | 13 | with pytest.raises(AssertionError): 14 | 10 | should.be.below.of(2) 15 | 16 | with pytest.raises(AssertionError): 17 | None | should.be.below.of(10) 18 | 19 | 20 | def test_expect_above(should): 21 | 10 | should.be.above(9) 22 | 3 | should.be.above.of(2) 23 | 24 | # negation 25 | 10 | should.not_be.above.of(11) 26 | 27 | with pytest.raises(AssertionError): 28 | 10 | should.be.above.of(100) 29 | 30 | with pytest.raises(AssertionError): 31 | None | should.be.above.of(10) 32 | 33 | 34 | def test_expect_below_or_equal(should): 35 | 10 | should.be.below_or_equal(10) 36 | 5 | should.be.below_or_equal(10) 37 | 38 | # negation 39 | 10 | should.not_be.below_or_equal.to(5) 40 | 41 | with pytest.raises(AssertionError): 42 | 10 | should.not_be.below_or_equal.to(10) 43 | 44 | with pytest.raises(AssertionError): 45 | None | should.be.below_or_equal.of(10) 46 | 47 | 48 | def test_expect_above_or_equal(should): 49 | 10 | should.be.above_or_equal(10) 50 | 101 | should.be.above_or_equal(100) 51 | 3 | should.be.above_or_equal.to(3) 52 | 53 | # negation 54 | 10 | should.not_be.above_or_equal.to(100) 55 | 56 | with pytest.raises(AssertionError): 57 | 10 | should.not_be.above_or_equal.to(10) 58 | 59 | with pytest.raises(AssertionError): 60 | None | should.be.above_or_equal.of(10) 61 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OK_COLOR=\033[32;01m 2 | NO_COLOR=\033[0m 3 | 4 | all: test 5 | 6 | export PYTHONPATH:=${PWD} 7 | version=`python -c 'import grappa; print(grappa.__version__)'` 8 | filename=grappa-`python -c 'import grappa; print(grappa.__version__)'`.tar.gz 9 | 10 | apidocs: 11 | @sphinx-apidoc -f --follow-links -H "API documentation" -o docs/source grappa 12 | 13 | htmldocs: 14 | @rm -rf docs/_build 15 | $(MAKE) -C docs html 16 | 17 | lint: 18 | @printf "$(OK_COLOR)==> Linting code...$(NO_COLOR)\n" 19 | @flake8 . 20 | 21 | test: lint 22 | @printf "$(OK_COLOR)==> Runnings tests...$(NO_COLOR)\n" 23 | @pytest -s -v --tb=native --capture=sys --cov grappa --cov-report term-missing tests 24 | 25 | coverage: 26 | @coverage run --source grappa -m py.test 27 | @coverage report 28 | 29 | bump: 30 | @bumpversion --commit --tag --current-version $(version) patch grappa/__init__.py 31 | 32 | history: 33 | @git changelog --tag $(version) 34 | 35 | tag: 36 | @printf "$(OK_COLOR)==> Creating tag $(version)...$(NO_COLOR)\n" 37 | @git tag -a "v$(version)" -m "Version $(version)" 38 | @printf "$(OK_COLOR)==> Pushing tag $(version) to origin...$(NO_COLOR)\n" 39 | @git push origin "v$(version)" 40 | 41 | clean: 42 | @printf "$(OK_COLOR)==> Cleaning up files that are already in .gitignore...$(NO_COLOR)\n" 43 | @for pattern in `cat .gitignore`; do find . -name "$$pattern" -delete; done 44 | 45 | release: clean publish 46 | @printf "$(OK_COLOR)==> Exporting to $(filename)...$(NO_COLOR)\n" 47 | @tar czf $(filename) grappa setup.py README.rst LICENSE 48 | 49 | publish: 50 | @echo "$(OK_COLOR)==> Releasing package $(version)...$(NO_COLOR)" 51 | @python setup.py sdist bdist_wheel 52 | @twine upload dist/* 53 | @rm -fr build dist .egg pook.egg-info 54 | -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | Frequently Asked Questions 2 | ========================== 3 | 4 | Can I use ``grappa`` with any testing framework? 5 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 6 | 7 | Yes, you can. ``grappa`` is testing framework agnostic. 8 | In fact, you don't even need to use a testing framework at all to use it. 9 | 10 | 11 | Can I extend ``grappa`` with other assertion operators? 12 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 13 | 14 | Of course you can. ``grappa`` is an extensible modular testing library. 15 | 16 | See `creating your custom operator`_ documentation section. 17 | 18 | 19 | Why use ``grappa`` instead of traditional Python assertions? 20 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 21 | 22 | Some people would argue that ``grappa`` is more verbose. 23 | It certainly is, but comes with several benefits that you won't have without it, such as: 24 | 25 | - Expressivity: code intentions are more clear, easy to read and understand. 26 | - Readability: besides trivial assertions, it help you writing complex assertions is a human-friendly way. 27 | - Underline complex assertions with an expressive, human-friendly DSL. 28 | - Great error report system with detailed failure reasons, subject/expectation comparison diff, embedded code. 29 | - Lazy evaluation allows ``grappa`` understand what you're expressing in code to perform more fine-grane validations. 30 | - ``grappa`` does type validation under the hood to ensure your providing a valid type. 31 | - ``grappa`` is an extensible assertion layer that can be used in a lot of ways, such as for HTTP protocol testing. 32 | - First-class support for Python data structures assertions and access. 33 | 34 | 35 | .. _`creating your custom operator`: http://grappa.readthedocs.io/en/latest/plugins.html#creating-operators -------------------------------------------------------------------------------- /grappa/operators/none.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..operator import Operator 3 | 4 | 5 | class NoneOperator(Operator): 6 | """ 7 | Asserts if a given subject is ``None``. 8 | 9 | Example:: 10 | 11 | # Should style 12 | None | should.be.none 13 | 14 | # Should style - negation form 15 | 'foo' | should.not_be.none 16 | 17 | # Expect style 18 | None | expect.to.be.none 19 | 20 | # Expect style - negation form 21 | 'foo' | expect.to_not.be.none 22 | """ 23 | 24 | # Is the operator a keyword 25 | kind = Operator.Type.ACCESSOR 26 | 27 | # Disable diff report 28 | show_diff = False 29 | 30 | # Operator keywords 31 | operators = ('none',) 32 | 33 | # Disable chanined calls 34 | chainable = False 35 | 36 | # Expected message templates 37 | expected_message = Operator.Dsl.Message( 38 | 'a value that is exactly equal to "None"', 39 | 'a value that is not equal to "None"', 40 | ) 41 | 42 | # Subject template message 43 | subject_message = Operator.Dsl.Message( 44 | 'an object with type "{type}" with value "{value}"', 45 | ) 46 | 47 | # Assertion information 48 | information = ( 49 | Operator.Dsl.Help( 50 | Operator.Dsl.Description( 51 | '"None" is a built-in constant in Python that represents the', 52 | 'absence of a value, as when default arguments are not passed', 53 | 'to a function. The sole value of the type NoneType.', 54 | ), 55 | Operator.Dsl.Reference( 56 | 'https://docs.python.org/3/library/constants.html#None' 57 | ), 58 | ), 59 | ) 60 | 61 | def match(self, subject): 62 | return subject is None, [] 63 | -------------------------------------------------------------------------------- /grappa/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import functools 4 | 5 | from .log import log 6 | 7 | 8 | def validate(method): 9 | """ 10 | Config option name value validator decorator. 11 | """ 12 | # Name error template 13 | name_error = 'configuration option "{}" is not supported' 14 | 15 | @functools.wraps(method) 16 | def validator(self, name, *args): 17 | if name not in self.allowed_opts: 18 | raise ValueError(name_error.format(name)) 19 | return method(self, name, *args) 20 | return validator 21 | 22 | 23 | class Config(object): 24 | """ 25 | Config provides a simple key-value Pythonic data store for grappa 26 | runtime configuration settings. 27 | """ 28 | 29 | # Default options store 30 | defaults = { 31 | # Debug enables internal logging. Defaults to False. 32 | 'debug': False, 33 | # show_code enables/disables code output in errors. 34 | 'show_code': True, 35 | # colors enables/disables colored ANSI sequences for the terminal 36 | 'use_colors': True 37 | } 38 | 39 | # List of supported configuration names 40 | allowed_opts = defaults.keys() 41 | 42 | def __init__(self): 43 | self.__dict__['opts'] = self.defaults.copy() 44 | 45 | @validate 46 | def __getattr__(self, name): 47 | return self.opts.get(name) 48 | 49 | @validate 50 | def __setattr__(self, name, value): 51 | self.opts[name] = value 52 | 53 | # Configure logger level 54 | if name == 'debug': 55 | log.setLevel(logging.DEBUG if value is True 56 | else logging.CRITICAL) 57 | 58 | log.debug('[config] set option {}="{}"'.format(name, value)) 59 | 60 | 61 | # Global config store 62 | config = Config() 63 | -------------------------------------------------------------------------------- /grappa/operators/equal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..operator import Operator 3 | 4 | 5 | class EqualOperator(Operator): 6 | """ 7 | Performs a strict equality comparison between ``x`` and ``y`` values. 8 | 9 | Uses ``==`` built-in binary operator for the comparison. 10 | 11 | Example:: 12 | 13 | # Should style 14 | 'foo' | should.be.equal('foo') 15 | 'foo' | should.be.equal.to('foo') 16 | 'foo' | should.be.equal.to.value('foo') 17 | 18 | # Should style - negation form 19 | 'foo' | should.not_be.equal('foo') 20 | 'foo' | should.not_be.equal.to('foo') 21 | 'foo' | should.not_be.equal.to.value('foo') 22 | 23 | # Expect style 24 | 'foo' | expect.to.equal('foo') 25 | 'foo' | expect.to.equal.to('foo') 26 | 'foo' | expect.to.equal.to.value('foo') 27 | 28 | # Expect style - negation form 29 | 'foo' | expect.to_not.equal('foo') 30 | 'foo' | expect.to_not.equal.to('foo') 31 | 'foo' | expect.to_not.equal.to.value('foo') 32 | """ 33 | 34 | # Is the operator a keyword 35 | kind = Operator.Type.MATCHER 36 | 37 | # Enable diff report 38 | show_diff = True 39 | 40 | # Operator keywords 41 | operators = ('equal', 'same') 42 | 43 | # Operator chain aliases 44 | aliases = ('value', 'data', 'to', 'of', 'as') 45 | 46 | # Error message templates 47 | expected_message = Operator.Dsl.Message( 48 | 'a value that is equal to "{value}"', 49 | 'a value that is not equal to "{value}"', 50 | ) 51 | 52 | # Subject message template 53 | subject_message = Operator.Dsl.Message( 54 | 'a value of type "{type}" with data "{value}"', 55 | ) 56 | 57 | def match(self, subject, expected): 58 | return subject == expected, [] 59 | -------------------------------------------------------------------------------- /grappa/operators/within.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..operator import Operator 3 | 4 | 5 | class WithinOperator(Operator): 6 | """ 7 | Asserts that a number is within a range. 8 | 9 | Example:: 10 | 11 | # Should style 12 | 4 | should.be.within(2, 5) 13 | 5 | should.be.between(2, 5) 14 | 4.5 | should.be.within(4, 5) 15 | 16 | # Should style - negation form 17 | 4 | should.not_be.within(2, 5) 18 | 5 | should.not_be.between(2, 5) 19 | 4.5 | should.not_be.within(4, 5) 20 | 21 | # Expect style 22 | 4 | expect.to.be.within(2, 5) 23 | 5 | expect.to.be.between(2, 5) 24 | 4.5 | expect.to.be.within(4, 5) 25 | 26 | # Expect style - negation form 27 | 4 | expect.to_not.be.within(2, 5) 28 | 5 | expect.to_not.be.between(2, 5) 29 | 4.5 | expect.to_not.be.within(4, 5) 30 | """ 31 | 32 | # Is the operator a keyword 33 | kind = Operator.Type.MATCHER 34 | 35 | # Disable diff report 36 | show_diff = False 37 | 38 | # Operator keywords 39 | operators = ('within', 'between') 40 | 41 | # Operator keywords 42 | aliases = ('to', 'numbers', 'range') 43 | 44 | # Expected template message 45 | expected_message = Operator.Dsl.Message( 46 | 'a number that is within range {value}', 47 | 'a number that is not within range {value}' 48 | ) 49 | 50 | # Subject template message 51 | subject_message = Operator.Dsl.Message( 52 | 'an object of type "{type}" with value "{value}"', 53 | ) 54 | 55 | def match(self, subject, start, end): 56 | if self.ctx.length: 57 | subject = len(subject) 58 | 59 | if not isinstance(subject, (int, float, complex)): 60 | return False, ['subject is not a numeric type'] 61 | 62 | return start <= subject <= end, [] 63 | -------------------------------------------------------------------------------- /grappa/reporters/subject.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..empty import empty 3 | from .base import BaseReporter 4 | from ..operator_dsl import Message 5 | 6 | 7 | class SubjectMessageReporter(BaseReporter): 8 | 9 | # Report section title 10 | title = 'What we got instead' 11 | 12 | # Attribute to lookup in the operator instance for custom message template 13 | attribute = 'subject' 14 | 15 | def run(self, error): 16 | if not hasattr(error, 'operator'): 17 | return None 18 | 19 | # Custom value human-friendly message 20 | value = self.from_operator( 21 | self.attribute, getattr(self.ctx, self.attribute, empty) 22 | ) 23 | 24 | # If value empty, return its value accordingly 25 | if value is empty: 26 | return None 27 | if value is None: 28 | return None if self.attribute == 'expected' else 'None' 29 | 30 | # Get first argument, if needed 31 | if self.attribute == 'expected' and isinstance(value, (list, tuple)): 32 | value = value[0] if len(value) == 1 else value 33 | 34 | # Get expectation message, if present in the operator 35 | attribute = '{}_message'.format(self.attribute) 36 | text_message = self.from_operator(attribute, None) 37 | if text_message: 38 | # Check if message is present and is a negation expression 39 | if isinstance(text_message, Message): 40 | attr = 'negation' if self.ctx.negate else 'allowance' 41 | text_message = getattr(text_message, attr, '') 42 | 43 | # Render template 44 | text_message = self.render_tmpl( 45 | self.indentify(text_message), 46 | value 47 | ) 48 | 49 | # Return template text message 50 | return text_message or self.normalize(value) 51 | -------------------------------------------------------------------------------- /grappa/operators/only.py: -------------------------------------------------------------------------------- 1 | import six 2 | from grappa.log import log 3 | from .contain import ContainOperator 4 | from ..operator import Operator 5 | 6 | 7 | class ContainOnlyOperator(ContainOperator): 8 | """ 9 | Asserts if only the given value or values can be found 10 | in a another object. 11 | 12 | Example:: 13 | 14 | # Should style 15 | ['foo', 'bar'] | should.contain.only('foo', 'bar') 16 | [{'foo': True}, 'bar'] | should.contain.only({'foo': True}, 'bar') 17 | 18 | # Should style - negation form 19 | ['foo', 'bar'] | should.do_not.contain.only('baz') 20 | ('foo', 'bar') | should.do_not.contain.only('foo', 'bar', 'extra') 21 | 22 | # Expect style 23 | ['foo', 'bar'] | expect.to.contain.only('foo', 'bar') 24 | [{'foo': True}, 'bar'] | expect.to.contain.only('bar', {'foo': True}) 25 | 26 | # Expect style - negation form 27 | ['foo', 'bar'] | expect.to_not.contain.only('bar') 28 | ['foo', 'bar'] | expect.to_not.contain.only('baz', 'foo') 29 | ['foo', 'bar'] | expect.to_not.contain.only('bar', 'foo', 'extra') 30 | """ 31 | operators = ('only', 'just',) 32 | expected_message = Operator.Dsl.Message( 33 | 'a value that contains only "{value}"', 34 | 'a value that does not contain only "{value}"', 35 | ) 36 | 37 | def match(self, subject, *expected): 38 | contains_all, reasons = super(ContainOnlyOperator, self)\ 39 | .match(subject, *expected) 40 | # Add check that no other item is in expected 41 | if contains_all is True: 42 | # Handle string case 43 | if isinstance(subject, six.string_types): 44 | log.warn('String comparison using "only" is not advised') 45 | return (subject == expected[0]), [] 46 | return super(ContainOnlyOperator, self).match(expected, *subject) 47 | return contains_all, reasons 48 | -------------------------------------------------------------------------------- /grappa/reporters/diff.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import difflib 4 | 5 | from .base import BaseReporter 6 | 7 | 8 | class DiffReporter(BaseReporter): 9 | """ 10 | Outputs the comparison differences result between the 11 | subject/expected objects. 12 | """ 13 | 14 | title = 'Difference comparison' 15 | 16 | def run(self, error): 17 | # Ensure operator enables diff reporter, otherwise just exit 18 | show_diff = any([ 19 | self.ctx.show_diff, 20 | self.from_operator('show_diff', False) 21 | ]) 22 | if not show_diff: 23 | return 24 | 25 | # Match if the given operator implements a custom differ 26 | differ = self.from_operator('differ', None) 27 | if differ: 28 | return error.operator.differ() 29 | 30 | # Obtain subject value 31 | subject = str( 32 | self.from_operator( 33 | 'diff_subject', 34 | self.from_operator('subject', self.ctx.subject))) 35 | 36 | # Split subject by lines 37 | subject = subject.splitlines(1) 38 | 39 | # Obtain expected value 40 | expected = self.from_operator( 41 | 'diff_expected', 42 | self.from_operator('expected', self.ctx.expected)) 43 | 44 | # Get expected value, if needed 45 | if isinstance(expected, tuple) and len(expected) == 1: 46 | expected = expected[0] 47 | 48 | # Expected value 49 | expected = str(expected).splitlines(1) 50 | 51 | # Diff subject and expected values 52 | data = list(difflib.ndiff(subject, expected)) 53 | 54 | # Remove trailing line feed returned by ndiff, if present 55 | data[-1] = data[-1].replace(os.linesep, '') 56 | 57 | # Normalize line separator with proper indent level 58 | data = [i.replace(os.linesep, '') for i in data] 59 | 60 | return data 61 | -------------------------------------------------------------------------------- /docs/composition.rst: -------------------------------------------------------------------------------- 1 | Operators composition 2 | ===================== 3 | 4 | ``grappa`` overloads ``|`` and ``>`` operators for expressive assertions composition and chaining. 5 | 6 | 7 | Pipe operator 8 | ------------- 9 | 10 | Use ``|`` operator for composing assertions that matches the following conditions: 11 | 12 | - For assertion expression initialization. 13 | - A typical `AND` logical composition. 14 | - Assertions that DO tests the same subject. 15 | - Assertions that uses operators that DOES NOT yield a new test subject. 16 | 17 | .. code-block:: python 18 | 19 | 'foo' | should.have.length.of(3) | should.contain('o') 20 | [1, 2, 3] | should.be.a(list) | should.have.length.of(3) | should.contain(2) 21 | 22 | 23 | Arrow operator 24 | -------------- 25 | 26 | Use ``>`` operator for composing assertions that matches the following conditions: 27 | 28 | - For non-initialization assertion expressions. 29 | - A typical `AND` logical composition. 30 | - Assertions that DOES NOT test the same subject. 31 | - Assertions that uses operators that YIELDS a new test subject in the assertion chain. 32 | 33 | .. code-block:: python 34 | 35 | [1, 2, 3] | should.be.a(list) > should.have.index.at(2) | should.be.equal.to(3) 36 | 37 | 38 | Conditional operators 39 | --------------------- 40 | 41 | ``all`` assertion composition, equivalent to ``and`` operator. 42 | 43 | You can define ``N`` number of composed assertions. 44 | 45 | .. code-block:: python 46 | 47 | {'foo': True} | should.all(should.have.key('foo'), should.have.length.of(1)) 48 | 49 | {'foo': True} | expect.all(expect.to.have.key('foo'), expect.to.have.length.of(1)) 50 | 51 | 52 | ``any`` assertion composition, equivalent to ``or`` operator. 53 | You can define ``N`` number of composed assertions. 54 | 55 | .. code-block:: python 56 | 57 | {'foo': True} | should.any(should.have.key('bar'), should.have.length.of(1)) 58 | 59 | {'foo': True} | expect.any(expect.to.have.key('bar'), expect.to.have.length.of(1)) 60 | -------------------------------------------------------------------------------- /grappa/reporters/assertion.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..empty import empty 3 | from .base import BaseReporter 4 | 5 | 6 | class AssertionReporter(BaseReporter): 7 | 8 | title = 'The following assertion was not satisfied' 9 | 10 | template = 'subject "{}" {} {}' 11 | 12 | def get_expected(self, operator=None, defaults=None): 13 | # Expected value 14 | expected = self.from_operator('expected', defaults, operator=operator) 15 | 16 | if isinstance(expected, tuple): 17 | if len(expected) == 0: 18 | expected = empty 19 | if len(expected) == 1: 20 | expected = expected[0] 21 | 22 | # Add expected value template, if needed 23 | if expected is not empty: 24 | if isinstance(expected, (tuple, list)): 25 | expected = ', '.join(str(i) for i in expected) 26 | 27 | if isinstance(expected, (int, float)): 28 | return '{}'.format(expected) 29 | 30 | return '"{}"'.format(self.normalize(expected, use_raw=False)) 31 | 32 | def run(self, error): 33 | # Assertion expression value 34 | subject = self.normalize( 35 | self.from_operator('subject', self.ctx.subject), use_raw=False) 36 | 37 | # List of used keyword operators 38 | keywords = [] 39 | for keyword in self.ctx.keywords: 40 | if type(keyword) is dict and 'operator' in keyword: 41 | expected = self.get_expected( 42 | keyword['operator'], keyword['call']) 43 | keywords.append(expected or '"Empty"') 44 | else: 45 | keywords.append(keyword) 46 | 47 | # Compose assertion sentence 48 | operators = ' '.join(keywords).replace('_', ' ') 49 | 50 | # Assertion expression value 51 | assertion = self.template.format(subject, self.ctx.style, operators) 52 | 53 | # Return assertion formatted sentence 54 | return assertion 55 | -------------------------------------------------------------------------------- /grappa/operators/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..engine import Engine 3 | 4 | # Module symbols to export 5 | __all__ = ('operators', 'load') 6 | 7 | 8 | # List of built-in operators 9 | operators = ( 10 | # Module name # Operator class to import 11 | ('attributes', ), 12 | ('type', 'TypeOperator'), 13 | ('none', 'NoneOperator'), 14 | ('keys', 'KeysOperator'), 15 | ('index', 'IndexOperator'), 16 | ('match', 'MatchOperator'), 17 | ('length', 'LengthOperator'), 18 | ('empty', 'EmptyOperator'), 19 | ('equal', 'EqualOperator'), 20 | ('within', 'WithinOperator'), 21 | ('present', 'PresentOperator'), 22 | ('contain', 'ContainOperator'), 23 | ('only', 'ContainOnlyOperator'), 24 | ('callable', 'CallableOperator'), 25 | ('property', 'PropertyOperator'), 26 | ('pass_test', 'PassTestOperator'), 27 | ('implements', 'ImplementsOperator'), 28 | ('raises', 'RaisesOperator'), 29 | 30 | ('been_called', 'BeenCalledOperator', 31 | 'BeenCalledTimesOperator', 32 | 'BeenCalledOnceOperator'), 33 | ('been_called_with', 'BeenCalledWithOperator', 34 | 'BeenCalledOnceWithOperator'), 35 | 36 | ('bool', 'TrueOperator', 'FalseOperator'), 37 | ('start_end', 'StartWithOperator', 'EndWithOperator'), 38 | 39 | ('range', 'BelowOperator', 'AboveOperator', 40 | 'AboveOrEqualOperator', 'BelowOrEqualOperator'), 41 | ) 42 | 43 | 44 | def load(): 45 | """ 46 | Loads the built-in operators into the global test engine. 47 | """ 48 | for operator in operators: 49 | module, symbols = operator[0], operator[1:] 50 | path = 'grappa.operators.{}'.format(module) 51 | 52 | # Dynamically import modules 53 | operator = __import__(path, None, None, symbols) 54 | 55 | # Register operators in the test engine 56 | for symbol in symbols: 57 | Engine.register(getattr(operator, symbol)) 58 | -------------------------------------------------------------------------------- /grappa/template.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import functools 3 | from colorama import Fore, Style 4 | 5 | from .config import config 6 | 7 | 8 | class ErrorTemplate(object): 9 | """ 10 | ErrorTemplate is used to compose and render a human-friendly 11 | descriptive error message. 12 | """ 13 | 14 | separator = ' ' * 6 15 | 16 | section_separator = '\n\n ' 17 | 18 | header = 'Oops! Something went wrong!' 19 | 20 | def __init__(self): 21 | self.sections = [] 22 | 23 | @property 24 | def color(self): 25 | return Style.BRIGHT + Fore.GREEN if config.use_colors else '' 26 | 27 | @property 28 | def reset(self): 29 | return Style.RESET_ALL if config.use_colors else '' 30 | 31 | def map_iterable(self, content): 32 | margin = ' ' * 4 33 | 34 | if type(content[0]) is str: 35 | return ('\n' + margin).join( 36 | '> ' + item for item in content if item 37 | ) 38 | 39 | return ('\n\n' + margin).join( 40 | item.render(ErrorTemplate.separator) 41 | for item in content 42 | if hasattr(item, 'render') 43 | ) 44 | 45 | def block(self, title, content): 46 | # If no content, just exit 47 | if not content: 48 | return self 49 | 50 | # If iterable type, aggregate elements and format them 51 | if isinstance(content, (tuple, list)): 52 | content = self.map_iterable(content) 53 | 54 | # Register section object 55 | self.sections.append({ 56 | 'title': title, 57 | 'content': content 58 | }) 59 | 60 | return self 61 | 62 | def render(self): 63 | def add_section(buf, section): 64 | buf.append('{color}{title}{reset}\n {content}'.format( 65 | color=self.color, 66 | title=section['title'], 67 | reset=self.reset, 68 | content=section['content'] 69 | )) 70 | return buf 71 | 72 | sections = functools.reduce(add_section, self.sections, [self.header]) 73 | return self.section_separator.join(sections) 74 | -------------------------------------------------------------------------------- /tests/operators/callable_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from grappa.operators.callable import CallableOperator 3 | 4 | 5 | def test_should_callable(should): 6 | test_should_callable | should.be.callable 7 | (lambda x: x) | should.be.callable 8 | CallableOperator | should.be.callable 9 | CallableOperator.match | should.be.callable 10 | 11 | with pytest.raises(AssertionError): 12 | tuple() | should.be.callable 13 | 14 | with pytest.raises(AssertionError): 15 | 0 | should.be.callable 16 | 17 | 18 | def test_expect_callable(expect): 19 | test_expect_callable | expect.to.be.callable 20 | (lambda x: x) | expect.to.be.callable 21 | CallableOperator | expect.to.be.callable 22 | CallableOperator.match | expect.to.be.callable 23 | 24 | with pytest.raises(AssertionError): 25 | tuple() | expect.to.be.callable 26 | 27 | with pytest.raises(AssertionError): 28 | 0 | expect.to.be.callable 29 | 30 | 31 | def test_callable_operator(ctx): 32 | assert CallableOperator(ctx).match(lambda x: x) == (True, []) 33 | assert CallableOperator(ctx).match(CallableOperator) == (True, []) 34 | assert CallableOperator(ctx).match(CallableOperator.match) == (True, []) 35 | 36 | assert CallableOperator(ctx).match(0) == (False, []) 37 | assert CallableOperator(ctx).match('foo') == (False, []) 38 | assert CallableOperator(ctx).match(iter([1, 2, 3])) == (False, []) 39 | assert CallableOperator(ctx).match(None) == ( 40 | False, ['a callable value cannot be "None"']) 41 | 42 | 43 | def test_callable_operator_properties(should): 44 | (CallableOperator 45 | | should.have.property('kind') 46 | > should.be.equal.to('accessor')) 47 | 48 | (CallableOperator 49 | | should.have.property('operators') 50 | > should.have.length.of(1) 51 | > should.be.equal.to(('callable',))) 52 | 53 | CallableOperator | should.have.property('aliases') > should.be.empty 54 | 55 | CallableOperator | should.have.property('expected_message') 56 | CallableOperator | should.have.property('subject_message') 57 | 58 | (CallableOperator | should.have.property('information') 59 | > should.be.a('tuple') 60 | > should.have.length.of(2)) 61 | -------------------------------------------------------------------------------- /grappa/operators/bool.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..operator import Operator 3 | 4 | 5 | class TrueOperator(Operator): 6 | """ 7 | Asserts if a given subject is `True` value. 8 | 9 | Example:: 10 | 11 | # Should style 12 | True | should.be.true 13 | 14 | # Should style - negation form 15 | True | should.not_be.true 16 | 17 | # Expect style 18 | False | expect.to.be.true 19 | 20 | # Expect style - negation form 21 | False | expect.to_not.be.true 22 | """ 23 | 24 | # Is the operator a keyword 25 | kind = Operator.Type.ACCESSOR 26 | 27 | # Disable diff report 28 | show_diff = False 29 | 30 | # Operator keywords 31 | operators = ('true',) 32 | 33 | # Error message templates 34 | expected_message = Operator.Dsl.Message( 35 | 'a value that is "True"', 36 | 'a value that is not "True"', 37 | ) 38 | 39 | # Subject message template 40 | subject_message = Operator.Dsl.Message( 41 | 'a value of type "{type}" with content "{value}"', 42 | ) 43 | 44 | def match(self, subject): 45 | if not self.ctx.negate and not isinstance(subject, bool): 46 | return False, ['subject is not a bool type'] 47 | 48 | return subject is True, [] 49 | 50 | 51 | class FalseOperator(TrueOperator): 52 | """ 53 | Asserts if a given subject is `False` value. 54 | 55 | Example:: 56 | 57 | # Should style 58 | True | should.be.false 59 | 60 | # Should style - negation form 61 | True | should.not_be.false 62 | 63 | # Expect style 64 | False | expect.to.be.false 65 | 66 | # Expect style - negation form 67 | False | expect.to_not.be.false 68 | """ 69 | 70 | # Is the operator a keyword 71 | kind = Operator.Type.ACCESSOR 72 | 73 | # Operator keywords 74 | operators = ('false',) 75 | 76 | # Error message templates 77 | expected_message = Operator.Dsl.Message( 78 | 'a value that is "False"', 79 | 'a value that is not "False"', 80 | ) 81 | 82 | def match(self, subject): 83 | if not self.ctx.negate and not isinstance(subject, bool): 84 | return False, ['subject is not a bool type'] 85 | 86 | return subject is False, [] 87 | -------------------------------------------------------------------------------- /grappa/context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Context(object): 5 | """ 6 | Context is used to keep state and share data across test operators. 7 | 8 | Context typically stores operator defined flags that are accessed later 9 | by other operators in order to retrieve information or modify its 10 | evaluation behavior. 11 | 12 | From test runner perspective, a new context is created on every 13 | assertion execution phase, so context data is context-dependent of 14 | the testing scope. 15 | 16 | Attributes: 17 | flags (dict): stores context shared data by flag name. 18 | 19 | Example:: 20 | 21 | from grappa.context import Context 22 | 23 | ctx = Context() 24 | 25 | ctx.negate = True 26 | 27 | if ctx.has('negate'): 28 | ctx.negate = False 29 | ctx.value = 'foo' 30 | 31 | ctx.negate # => False 32 | ctx.value # => 'foo' 33 | """ 34 | 35 | def __init__(self, defaults=None, **kw): 36 | self.__dict__['flags'] = defaults or {'negate': False} 37 | 38 | for name, value in kw.items(): 39 | self.__dict__['flags'][name] = value 40 | 41 | def has(self, flag): 42 | """ 43 | Returns `True` if the given flag name is present in the current 44 | context instance. 45 | 46 | Returns: 47 | bool 48 | """ 49 | return flag in self.flags 50 | 51 | def __getattr__(self, name): 52 | """ 53 | Overloads class attribute accessor method in order to proxy access to 54 | the store. 55 | """ 56 | return self.flags.get(name) 57 | 58 | def __setattr__(self, name, value): 59 | """ 60 | Overloads class set attribute method in order to proxy access to the 61 | store. 62 | """ 63 | self.flags[name] = value 64 | 65 | def clone(self): 66 | """ 67 | Returns a copy of the current `Context` instance. 68 | 69 | Returns: 70 | grappa.Context 71 | """ 72 | return Context(self.__dict__['flags'].copy()) 73 | 74 | def __repr__(self): 75 | """ 76 | Human-readable context data. 77 | 78 | Returns: 79 | str 80 | """ 81 | return str(self.__dict__['flags']) 82 | -------------------------------------------------------------------------------- /grappa/runner.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .reporter import ErrorReporter 3 | 4 | 5 | class Runner(object): 6 | """ 7 | Runner is responsible of triggering the registed assertion operators in the 8 | current engine. 9 | 10 | Arguments: 11 | engine (grappa.Engine) 12 | """ 13 | 14 | def __init__(self, engine): 15 | self.engine = engine 16 | 17 | def render_error(self, ctx, error): 18 | # Expose keywords via context (this should be improved) 19 | ctx.keywords = self.engine.keywords 20 | # Render error exception 21 | return ErrorReporter(ctx).run(error) 22 | 23 | def run_assertions(self, ctx): 24 | # Trigger assertion functions 25 | for assertion in self.engine.assertions: 26 | # Store current subject 27 | subject = ctx.subject 28 | 29 | # Run assertion with the given subject 30 | result = assertion(ctx.subject) 31 | 32 | # Check if the subject changed during operator execution 33 | if subject is not ctx.subject: 34 | # Register previous subject 35 | ctx.subjects.append(subject) 36 | 37 | # If assertion passed, just continue with it 38 | if result is True: 39 | continue 40 | 41 | # Forward original grappa error 42 | if all([isinstance(result, AssertionError), 43 | hasattr(result, '__grappa__')]): 44 | return result 45 | 46 | # Otherwise render assertion error accordingly 47 | return self.render_error(ctx, result) 48 | 49 | def run(self, ctx): 50 | """ 51 | Runs the current phase. 52 | """ 53 | # Reverse engine assertion if needed 54 | if ctx.reverse: 55 | self.engine.reverse() 56 | 57 | if self.engine.empty: 58 | raise AssertionError('grappa: no assertions to run') 59 | 60 | try: 61 | # Run assertion in series and return error, if present 62 | return self.run_assertions(ctx) 63 | except Exception as _err: 64 | # Handle legit grappa internval errors 65 | if getattr(_err, '__legit__', False): 66 | raise _err 67 | # Otherwise render it 68 | return self.render_error(ctx, _err) 69 | -------------------------------------------------------------------------------- /grappa/operators/callable.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..operator import Operator 3 | 4 | 5 | class CallableOperator(Operator): 6 | """ 7 | Asserts if a given subject is a callable type or an object that 8 | implements ``__call__()`` magic method. 9 | 10 | Example:: 11 | 12 | # Should style 13 | (lambda x: x) | should.be.callable 14 | 15 | # Should style - negation form 16 | None | should.not_be.callable 17 | 18 | # Expect style 19 | (lambda x: x) | expect.to.be.callable 20 | 21 | # Expect style - negation form 22 | None | expect.to_not.be.callable 23 | """ 24 | 25 | # Is the operator a keyword 26 | kind = Operator.Type.ACCESSOR 27 | 28 | # Disable diff report 29 | show_diff = False 30 | 31 | # Operator keywords 32 | operators = ('callable',) 33 | 34 | # Expected template message 35 | expected_message = Operator.Dsl.Message( 36 | 'a callable object, such as a function or method', 37 | 'a non callable object, such as a function or method' 38 | ) 39 | 40 | # Subject template message 41 | subject_message = Operator.Dsl.Message( 42 | 'an object which is a "{type}" type' 43 | ) 44 | 45 | # Assertion information 46 | information = ( 47 | Operator.Dsl.Help( 48 | Operator.Dsl.Description( 49 | 'Callable objects can be tested via "callable(x)" Python', 50 | 'built-in function.' 51 | ), 52 | Operator.Dsl.Reference( 53 | 'https://docs.python.org/3/library/functions.html#callable' 54 | ), 55 | ), 56 | Operator.Dsl.Help( 57 | Operator.Dsl.Description( 58 | 'Callable objects in Python are functions, methods, classes,', 59 | 'or any other object that implements "__call__()" method.' 60 | ), 61 | Operator.Dsl.Reference( 62 | 'http://stackoverflow.com/a/2436614' 63 | ), 64 | ), 65 | ) 66 | 67 | def match(self, value): 68 | if value is None: 69 | return False, ['a callable value cannot be "None"'] 70 | 71 | # Custom expectations messages 72 | self.expected = 'an callable object, such as a function or method' 73 | self.value = value 74 | 75 | return callable(value), [] 76 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, pypy3] 18 | experimental: [false] 19 | include: 20 | - os: ubuntu-latest 21 | python-version: pypy2 22 | experimental: false 23 | - os: ubuntu-latest 24 | python-version: 3.10-dev 25 | experimental: true 26 | 27 | runs-on: ${{ matrix.os }} 28 | continue-on-error: ${{ matrix.experimental }} 29 | name: Python ${{ matrix.python-version }} on ${{ matrix.os }} 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | 34 | - name: Set up Python ${{ matrix.python-version }} 35 | uses: actions/setup-python@v2 36 | if: "!endsWith(matrix.python-version, '-dev')" 37 | with: 38 | python-version: ${{ matrix.python-version }} 39 | 40 | - name: Set up Python ${{ matrix.python-version }} 41 | uses: deadsnakes/action@v2.0.0 42 | if: endsWith(matrix.python-version, '-dev') 43 | with: 44 | python-version: ${{ matrix.python-version }} 45 | env: 46 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true 47 | 48 | - name: Install dependencies 49 | run: | 50 | pip install -r requirements-dev.txt 51 | pip install -r requirements.txt 52 | 53 | - name: Lint with flake8 54 | run: | 55 | # stop the build if there are Python syntax errors or undefined names 56 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 57 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 58 | flake8 . --count --max-complexity=10 --statistics 59 | 60 | - name: Test with pytest 61 | run: | 62 | pytest -s -v --tb=native --capture=sys --cov grappa --cov-report term-missing tests 63 | 64 | - name: Coverage 65 | run: | 66 | coverage run --source grappa -m py.test 67 | coverage report 68 | -------------------------------------------------------------------------------- /grappa/operators/empty.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import numbers 3 | from ..operator import Operator 4 | 5 | 6 | class EmptyOperator(Operator): 7 | """ 8 | Asserts if a given subject is an empty object. 9 | 10 | A subject is considered empty if it's ``None``, ``0`` or ``len(subject)`` 11 | is equals to ``0``. 12 | 13 | Example:: 14 | 15 | # Should style 16 | [] | should.be.empty 17 | 18 | # Should style - negation form 19 | [1, 2, 3] | should.not_be.empty 20 | 21 | # Expect style 22 | tuple() | expect.to.be.empty 23 | 24 | # Expect style - negation form 25 | (1, 2, 3) | expect.to_not.be.empty 26 | """ 27 | 28 | # Is the operator a keyword 29 | kind = Operator.Type.ACCESSOR 30 | 31 | # Disable diff report 32 | show_diff = False 33 | 34 | # Operator keywords 35 | operators = ('empty',) 36 | 37 | # Expected template message 38 | expected_message = Operator.Dsl.Message( 39 | 'a value that is not "None" and its length is higher than zero' 40 | ) 41 | 42 | # Subject template message 43 | subject_message = Operator.Dsl.Message( 44 | 'an object with type "{type}" which its length cannot be measured' 45 | ) 46 | 47 | # Assertion information 48 | information = ( 49 | Operator.Dsl.Help( 50 | Operator.Dsl.Description( 51 | 'An empty object can be "None", "0" or "len(x) == 0".', 52 | 'Most objects in Python can be tested via "len(x)"', 53 | 'such as str, list, tuple, dict, generator...', 54 | 'as well as any object that implements "__len__()" method.', 55 | ), 56 | Operator.Dsl.Reference( 57 | 'https://docs.python.org/3/library/functions.html#len' 58 | ), 59 | ), 60 | ) 61 | 62 | def match(self, subject): 63 | if subject is None or subject is 0: # noqa F632 64 | return True 65 | 66 | if subject in (True, False): 67 | return False 68 | 69 | if isinstance(subject, numbers.Number) and subject != 0: 70 | return False 71 | 72 | try: 73 | return len(subject) == 0 74 | except TypeError: 75 | try: 76 | next(subject) 77 | except StopIteration: 78 | return True 79 | except Exception: 80 | return True 81 | return False 82 | -------------------------------------------------------------------------------- /tests/operators/raises_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from functools import partial 3 | 4 | 5 | def test_raises(should): 6 | def no_error(): 7 | pass 8 | 9 | def error(): 10 | raise AssertionError('foo') 11 | 12 | def error_with_params(foo_param): 13 | raise AssertionError(foo_param) 14 | 15 | error | should.raise_error(Exception) 16 | error | should.raise_error(AssertionError) 17 | error | should.do_not.raise_error(NotImplementedError) 18 | 19 | partial(error_with_params, "Foobar") | should.raise_error(AssertionError) 20 | partial(error_with_params, "Foobar") | should.to_not\ 21 | .raise_error(NotImplementedError) 22 | 23 | with pytest.raises(AssertionError): 24 | error | should.raise_error(NotImplementedError) 25 | 26 | with pytest.raises(AssertionError): 27 | error | should.do_not.raise_error(AssertionError) 28 | 29 | with pytest.raises(AssertionError): 30 | None | should.raise_error(AssertionError) 31 | 32 | with pytest.raises(AssertionError): 33 | no_error | should.raise_error(AssertionError) 34 | 35 | 36 | def test_raises_with_message_redirection(should): 37 | def error(): 38 | raise AssertionError('foo') 39 | 40 | def env_error(): 41 | raise EnvironmentError(3501, 'bar') 42 | 43 | error | should.raise_error(AssertionError) > should.equal('foo') 44 | 45 | error | should.raise_error(AssertionError) > should.contain('fo') 46 | 47 | error | should.do_not.raise_error(NotImplementedError) \ 48 | > should.equal('foo') 49 | 50 | env_error | should.raise_error(EnvironmentError) > should.contain('bar') 51 | 52 | env_error | should.raise_error(EnvironmentError) > should.equal('3501 bar') 53 | 54 | with pytest.raises(AssertionError): 55 | error | should.raise_error(AssertionError) > should.equal('fooe') 56 | 57 | with pytest.raises(AssertionError): 58 | error | should.raise_error(NotImplementedError) > should.equal('foo') 59 | 60 | 61 | def test_raises_custom_exception_message_redirection(should): 62 | class CustomException(Exception): 63 | message = 'foo' 64 | 65 | def __init__(self, *args): 66 | super(CustomException, self).__init__(self.message, *args) 67 | 68 | def custom_error(): 69 | raise CustomException('bar') 70 | 71 | custom_error | should.raise_error(CustomException) > should.equal('foo') 72 | 73 | custom_error | should.raise_error(CustomException) \ 74 | > should.do_not.equal('foo bar') 75 | -------------------------------------------------------------------------------- /grappa/operators/raises.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..operator import Operator 3 | 4 | 5 | class RaisesOperator(Operator): 6 | """ 7 | Asserts if a given function raises an exception. 8 | 9 | Example:: 10 | 11 | def fn(): 12 | raise ValueError('error') 13 | 14 | # Should style 15 | fn | should.raise_error() 16 | fn | should.raise_error(ValueError) 17 | fn | should.raise_error(AttributeError, ValueError) 18 | 19 | # Should style - negation form 20 | fn | should.do_not.raise_error() 21 | fn | should.do_not.raise_error(ValueError) 22 | fn | should.do_not.raise_error(AttributeError, ValueError) 23 | 24 | # Expect style 25 | fn | expect.to.raise_error() 26 | fn | expect.to.raise_error(ValueError) 27 | fn | expect.to.raise_error(AttributeError, ValueError) 28 | 29 | # Expect style - negation form 30 | fn | expect.to_not.raise_error() 31 | fn | expect.to_not.raise_error(ValueError) 32 | fn | expect.to_not.raise_error(AttributeError, ValueError) 33 | """ 34 | 35 | # Is the operator a keyword 36 | kind = Operator.Type.MATCHER 37 | 38 | # Disable diff report 39 | show_diff = False 40 | 41 | # Operator keywords 42 | operators = ('raises', 'raise_error', 'raise_errors') 43 | 44 | # Operator chain aliases 45 | aliases = ('to', 'that', 'are', 'instance', 'of') 46 | 47 | # Expected template message 48 | expected_message = Operator.Dsl.Message( 49 | 'a callable object that raises the exception(s) "{value}"', 50 | 'a callable object that do not raise the exception(s) "{value}"', 51 | ) 52 | 53 | # Subject template message 54 | subject_message = Operator.Dsl.Message( 55 | 'an object of type "{type}" with reference "{value}"', 56 | ) 57 | 58 | def after_success(self, obj, *keys): 59 | message = getattr(self.value, 'message', None) 60 | 61 | if not message: 62 | message = ' '.join([str(item) for item in self.value.args]) 63 | 64 | self.ctx.subject = message 65 | 66 | def match(self, fn, *errors): 67 | if not callable(fn): 68 | return False, ['subject must be a function or method'] 69 | 70 | try: 71 | fn() 72 | except Exception as err: 73 | self.value = err 74 | 75 | return isinstance(err, *errors), ['invalid raised exception'] 76 | else: 77 | return False, ['did not raise any exception'] 78 | -------------------------------------------------------------------------------- /grappa/operators/match.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | import six 4 | 5 | from ..operator import Operator 6 | 7 | 8 | class MatchOperator(Operator): 9 | """ 10 | Asserts if a given string matches a given regular expression. 11 | 12 | Example:: 13 | 14 | # Should style 15 | 'hello world' | should.match(r'Hello \\w+') 16 | 'hello world' | should.match(r'hello [A-Z]+', re.I)) 17 | 'hello world' | should.match.expression(r'hello [A-Z]+', re.I)) 18 | 19 | # Should style - negation form 20 | 'hello w0rld' | should.do_not.match(r'Hello \\w+') 21 | 'hello w0rld' | should.do_not.match(r'hello [A-Z]+', re.I)) 22 | 'hello world' | should.do_not.match.expression(r'hello [A-Z]+', re.I)) 23 | 24 | # Expect style 25 | 'hello world' | expect.to.match(r'Hello \\w+') 26 | 'hello world' | expect.to.match(r'hello [A-Z]+', re.I)) 27 | 'hello world' | expect.to.match.expression(r'hello [A-Z]+', re.I)) 28 | 29 | # Expect style - negation form 30 | 'hello w0rld' | expect.to_not.match(r'Hello \\w+') 31 | 'hello w0rld' | expect.to_not.match(r'hello [A-Z]+', re.I)) 32 | 'hello world' | expect.to_not.match.expression(r'hello [A-Z]+', re.I)) 33 | """ 34 | 35 | # Is the operator a keyword 36 | kind = Operator.Type.MATCHER 37 | 38 | # Operator keywords 39 | operators = ('match', 'matches') 40 | 41 | # Operator chain aliases 42 | aliases = ( 43 | 'value', 'string', 'expression', 'token', 44 | 'to', 'regex', 'regexp', 'word', 'phrase' 45 | ) 46 | 47 | # Disable diff report 48 | show_diff = True 49 | 50 | # Expected template message 51 | expected_message = Operator.Dsl.Message( 52 | 'a string that matches the expression "{value}"', 53 | 'a string that does not match the expression "{value}"', 54 | ) 55 | 56 | # Subject template message 57 | subject_message = Operator.Dsl.Message( 58 | 'an object of type "{type}" with value "{value}"', 59 | ) 60 | 61 | def match(self, subject, expected, *args): 62 | if not isinstance(subject, six.string_types): 63 | return False, ['subject must be a string, but got "{}"'.format( 64 | type(subject))] 65 | 66 | if not isinstance(expected, six.string_types): 67 | return False, [ 68 | 'value to match must be a string, but got "{}"'.format( 69 | type(expected)) 70 | ] 71 | 72 | return re.search(expected, subject, *args) is not None, [] 73 | -------------------------------------------------------------------------------- /grappa/reporters/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import six 4 | from ..empty import empty 5 | 6 | 7 | class BaseReporter(object): 8 | 9 | def __init__(self, ctx, error): 10 | self.ctx = ctx 11 | self.error = error 12 | 13 | def cut(self, value, size=40): 14 | text = str(value) 15 | return text[0:size] + ' ...' if len(text) > size else text 16 | 17 | def linefy(self, value): 18 | return str(value).replace(os.linesep, r'\n') 19 | 20 | def indentify(self, value): 21 | if not isinstance(value, six.string_types): 22 | return value 23 | 24 | lines = value.split(os.linesep) 25 | return '\n '.join(lines) 26 | 27 | def normalize(self, value, size=50, use_raw=True): 28 | if value is None: 29 | return value 30 | 31 | try: 32 | value = str(value) 33 | except: # noqa E722 34 | value = value 35 | 36 | if not hasattr(value, '__len__'): 37 | return value 38 | 39 | # Get output size 40 | raw_size = self.from_operator('raw_size') 41 | 42 | if use_raw and self.from_operator('raw_mode'): 43 | if raw_size: 44 | return self.cut(self.indentify(value), size=raw_size) 45 | else: 46 | return self.indentify(value) 47 | else: 48 | return self.linefy(self.cut(value, size=size)) 49 | 50 | def safe_length(self, value): 51 | try: 52 | return len(value) 53 | except: # noqa E722 54 | return '"unmeasurable"' 55 | 56 | def from_operator(self, name, defaults=None, operator=None): 57 | operator = operator or getattr(self.error, 'operator', None) 58 | if not operator: 59 | return defaults 60 | 61 | value = getattr(operator, name, defaults) 62 | return defaults if value is empty else value 63 | 64 | def render_tmpl(self, tmpl, value): 65 | placeholders = {} 66 | 67 | if '{value}' in tmpl: 68 | placeholders['value'] = self.normalize(value) 69 | 70 | if '{type}' in tmpl: 71 | placeholders['type'] = type(value).__name__ 72 | 73 | if '{length}' in tmpl: 74 | placeholders['length'] = self.safe_length(value) 75 | 76 | if '{call_count}' in tmpl: 77 | placeholders['call_count'] = getattr(value, 'call_count', 0) 78 | 79 | return tmpl.format(**placeholders) 80 | 81 | def run(self, error): 82 | raise NotImplementedError('run() method must be implemented') 83 | -------------------------------------------------------------------------------- /grappa/operators/index.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..operator import Operator 3 | 4 | 5 | class IndexOperator(Operator): 6 | """ 7 | Asserts that a given iterable has an item in a specific index. 8 | 9 | Example:: 10 | 11 | # Should style 12 | [1, 2, 3] | should.have.index(2) 13 | [1, 2, 3] | should.have.index(1) 14 | [1, 2, 3] | should.have.index.at(1) 15 | [1, 2, 3] | should.have.index.present(1) 16 | [1, 2, 3] | should.have.index.at(1).equal.to(2) 17 | [1, 2, 3] | should.have.index.at(1) > should.be.equal.to(2) 18 | 19 | # Should style - negation form 20 | [1, 2, 3] | should.not_have.index(4) 21 | [1, 2, 3] | should.not_have.index.at(4) 22 | [1, 2, 3] | should.have.index.at(1).to_not.equal.to(5) 23 | 24 | # Expect style 25 | [1, 2, 3] | expect.to.have.index(2) 26 | [1, 2, 3] | expect.to.have.index.at(1) 27 | [1, 2, 3] | expect.to.have.index.at(1).equal.to(2) 28 | [1, 2, 3] | expect.to.have.index.at(1) > expect.be.equal.to(2) 29 | 30 | # Expect style - negation form 31 | [1, 2, 3] | expect.to_not.have.index(2) 32 | [1, 2, 3] | expect.to_not.have.index.at(1) 33 | [1, 2, 3] | expect.to_not.have.index.at(1).equal.to(2) 34 | """ 35 | 36 | # Is the operator a keyword 37 | kind = Operator.Type.MATCHER 38 | 39 | # Disable diff report 40 | show_diff = False 41 | 42 | # Operator keywords 43 | operators = ('index',) 44 | 45 | # Operator chain aliases 46 | aliases = ('present', 'exists', 'at') 47 | 48 | # Expected message templates 49 | expected_message = Operator.Dsl.Message( 50 | 'a list/tuple that has index at {value}', 51 | 'a list/tuple that has not index at {value}', 52 | ) 53 | 54 | # Subject template message 55 | subject_message = Operator.Dsl.Message( 56 | 'an object of type "{type}" of length {length} with value "{value}"', 57 | ) 58 | 59 | def match(self, subject, index, **kw): 60 | if self._not_valid(subject): 61 | return False, ['subject is not a tuple/list'] 62 | 63 | # Get latest item 64 | if index == -1: 65 | index = len(subject) - 1 66 | 67 | # Ensure there is a valid index 68 | if index < 0 or index >= len(subject): 69 | return False, ['index {0!r} not found'.format(index)] 70 | 71 | # Expose value as subject 72 | self.ctx.subject = subject[index] 73 | 74 | return True, [] 75 | 76 | def _not_valid(self, subject): 77 | return not isinstance(subject, (list, tuple)) 78 | -------------------------------------------------------------------------------- /docs/attributes-operators.rst: -------------------------------------------------------------------------------- 1 | Attributes Operators 2 | ==================== 3 | 4 | These operators provides assertion/negation logic. 5 | 6 | Example operators: to_, be_ not_be_, which_ ... 7 | 8 | 9 | Assertion 10 | --------- 11 | 12 | be 13 | ^^ 14 | to 15 | ^^ 16 | has 17 | ^^^ 18 | have 19 | ^^^^ 20 | do 21 | ^^ 22 | include 23 | ^^^^^^^ 24 | satisfy 25 | ^^^^^^^ 26 | satisfies 27 | ^^^^^^^^^ 28 | _is 29 | ^^^ 30 | which 31 | ^^^^^ 32 | that 33 | ^^^^ 34 | that_is 35 | ^^^^^^^ 36 | which_is 37 | ^^^^^^^^ 38 | 39 | Semantic chainable attributes that defines non-negative assertions. 40 | 41 | Typically, you will use them implicitly in order to semantically describe your assertions. 42 | 43 | ======================= ======================== 44 | **Assertion mode** positive 45 | ----------------------- ------------------------ 46 | **Resets context** no 47 | ======================= ======================== 48 | 49 | .. code-block:: python 50 | 51 | 'foo' | should.be.equal.to('bar') 52 | 'foo' | should.have.length.of(3) 53 | 54 | {'foo': 'bar'} | should.have.key('foo').which.should.be.equal.to('bar') 55 | {'foo': 'bar'} | should.have.key('foo').that.should.have.length.of(3) 56 | 57 | .. code-block:: python 58 | 59 | expect('foo').to.equal.to('bar') 60 | expect('foo').to.have.length.of(3) 61 | 62 | expect({'foo': 'bar'}).to.have.key('foo').which.expect.to.be.equal('bar') 63 | expect({'foo': 'bar'}).to.have.key('foo').which.expect.to.have.length.of(3) 64 | 65 | 66 | Negation 67 | -------- 68 | 69 | not_be 70 | ^^^^^^ 71 | not_present 72 | ^^^^^^^^^^^ 73 | not_to 74 | ^^^^^^ 75 | to_not 76 | ^^^^^^ 77 | does_not 78 | ^^^^^^^^ 79 | do_not 80 | ^^^^^^ 81 | dont 82 | ^^^^ 83 | have_not 84 | ^^^^^^^^ 85 | not_have 86 | ^^^^^^^^ 87 | has_not 88 | ^^^^^^^ 89 | not_has 90 | ^^^^^^^ 91 | that_not 92 | ^^^^^^^^ 93 | which_not 94 | ^^^^^^^^^ 95 | is_not 96 | ^^^^^^ 97 | _not 98 | ^^^^ 99 | not_satisfy 100 | ^^^^^^^^^^^ 101 | 102 | Semantic chainable attributes that defines negative assertions. 103 | 104 | Typically, you will use them implicitly in order to semantically describe your assertions. 105 | 106 | ======================= ======================== 107 | **Assertion mode** negation 108 | ----------------------- ------------------------ 109 | **Resets context** no 110 | ======================= ======================== 111 | 112 | .. code-block:: python 113 | 114 | 'foo' | should.not_be.equal.to('bar') 115 | 'foo' | should.have_not.length.of(3) 116 | 117 | .. code-block:: python 118 | 119 | expect('foo').to_not.equal.to('bar') 120 | expect('foo').to.not_have.length.of(3) 121 | -------------------------------------------------------------------------------- /tests/operators/empty_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from grappa.operators.empty import EmptyOperator 3 | 4 | 5 | def test_should_empty(should): 6 | None | should.be.empty 7 | [] | should.be.empty 8 | '' | should.be.empty 9 | tuple() | should.be.empty 10 | 0 | should.be.empty 11 | iter([]) | should.be.empty 12 | 13 | with pytest.raises(AssertionError): 14 | False | should.be.empty 15 | 16 | with pytest.raises(AssertionError): 17 | True | should.be.empty 18 | 19 | with pytest.raises(AssertionError): 20 | 'foo' | should.be.empty 21 | 22 | with pytest.raises(AssertionError): 23 | [1, 2, 3] | should.be.empty 24 | 25 | with pytest.raises(AssertionError): 26 | iter([1, 2, 3]) | should.be.empty 27 | 28 | 29 | def test_expect_empty(expect): 30 | None | expect.to.be.empty 31 | [] | expect.to.be.empty 32 | '' | expect.to.be.empty 33 | tuple() | expect.to.be.empty 34 | 0 | expect.to.be.empty 35 | iter([]) | expect.to.be.empty 36 | 37 | with pytest.raises(AssertionError): 38 | False | expect.to.be.empty 39 | 40 | with pytest.raises(AssertionError): 41 | True | expect.to.be.empty 42 | 43 | with pytest.raises(AssertionError): 44 | 'foo' | expect.to.be.empty 45 | 46 | with pytest.raises(AssertionError): 47 | [1, 2, 3] | expect.to.be.empty 48 | 49 | with pytest.raises(AssertionError): 50 | iter([1, 2, 3]) | expect.to.be.empty 51 | 52 | 53 | def test_empty_operator(ctx): 54 | assert EmptyOperator(ctx).match(0) is True 55 | assert EmptyOperator(ctx).match(None) is True 56 | assert EmptyOperator(ctx).match('') is True 57 | assert EmptyOperator(ctx).match(iter([])) is True 58 | 59 | assert EmptyOperator(ctx).match(True) is False 60 | assert EmptyOperator(ctx).match(False) is False 61 | assert EmptyOperator(ctx).match('foo') is False 62 | assert EmptyOperator(ctx).match(123) is False 63 | assert EmptyOperator(ctx).match(0.2321) is False 64 | assert EmptyOperator(ctx).match(iter([1, 2, 3])) is False 65 | 66 | 67 | def test_empty_operator_properties(should): 68 | (EmptyOperator 69 | | should.have.property('kind') 70 | > should.be.equal.to('accessor')) 71 | 72 | (EmptyOperator 73 | | should.have.property('operators') 74 | > should.have.length.of(1) 75 | > should.be.equal.to(('empty',))) 76 | 77 | EmptyOperator | should.have.property('aliases') > should.be.empty 78 | 79 | EmptyOperator | should.have.property('expected_message') 80 | EmptyOperator | should.have.property('subject_message') 81 | 82 | (EmptyOperator | should.have.property('information') 83 | > should.be.a('tuple') 84 | > should.have.length.of(1)) 85 | -------------------------------------------------------------------------------- /grappa/engine.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .runner import Runner 3 | 4 | 5 | def isoperator(x): 6 | """ 7 | Returns `True` if the given object implements the required attributes for 8 | an operator. 9 | 10 | Returns: 11 | bool 12 | """ 13 | return all( 14 | hasattr(x, name) for name in ('run', 'operators', 'kind', '__class__') 15 | ) 16 | 17 | 18 | def register_operators(*operators): 19 | """ 20 | Registers one or multiple operators in the test engine. 21 | """ 22 | def validate(operator): 23 | if isoperator(operator): 24 | return True 25 | 26 | raise NotImplementedError('invalid operator: {}'.format(operator)) 27 | 28 | def register(operator): 29 | # Register operator by DSL keywords 30 | for name in operator.operators: 31 | # Check valid operators 32 | if name in Engine.operators: 33 | raise ValueError('operator name "{}" from {} is already ' 34 | 'in use by other operator'.format( 35 | name, 36 | operator.__name__ 37 | )) 38 | 39 | # Register operator by name 40 | Engine.operators[name] = operator 41 | 42 | # Validates and registers operators 43 | [register(operator) for operator in operators if validate(operator)] 44 | 45 | 46 | class Engine(object): 47 | """ 48 | Engine implements the test engine responsible of registering, storing and 49 | running test operators. 50 | """ 51 | 52 | # globally store test operators by name 53 | operators = {} 54 | 55 | def __init__(self): 56 | self.keywords = [] 57 | self.assertions = [] 58 | 59 | @staticmethod 60 | def register(*operators): 61 | """ 62 | Registers one or multiple operators in the test engine. 63 | """ 64 | register_operators(*operators) 65 | 66 | @property 67 | def empty(self): 68 | return len(self.assertions) == 0 69 | 70 | def add_assertion(self, assertion): 71 | self.assertions.append(assertion) 72 | 73 | def add_keyword(self, keyword): 74 | self.keywords.append(keyword) 75 | 76 | def reverse(self): 77 | self.assertions.reverse() 78 | 79 | def find_operator(self, name): 80 | return self.operators.get(name) 81 | 82 | def run(self, ctx): 83 | return Runner(self).run(ctx) 84 | 85 | def reset(self): 86 | self.__init__() 87 | 88 | def reset_keywords(self): 89 | self.keywords = [] 90 | 91 | def clone(self): 92 | engine = Engine() 93 | engine.keywords = self.keywords[:] 94 | engine.assertions = self.assertions[:] 95 | return engine 96 | -------------------------------------------------------------------------------- /docs/plugins.rst: -------------------------------------------------------------------------------- 1 | Plugins 2 | ======= 3 | 4 | Third-party plugins 5 | ------------------- 6 | 7 | List of third-party plugins for ``grappa``. 8 | 9 | **Official plugins** 10 | 11 | - `http`_ - HTTP protocol assertions with domain-specific DSL. 12 | - `server`_ - Server/system assertions with domain-specific DSL. 13 | 14 | **Community plugins** 15 | 16 | Did you create your own plugin? Open an issue or send a Pull Request! 17 | 18 | 19 | Creating your own plugin 20 | ------------------------ 21 | 22 | Creating operators 23 | ^^^^^^^^^^^^^^^^^^ 24 | 25 | .. code-block:: python 26 | 27 | from grappa import Operator 28 | 29 | class MyEqualOperator(Operator): 30 | """ 31 | MyOperator implements a custom `grappa` assertion operator. 32 | 33 | Operators should inherit from `grappa.Operator` for convenience 34 | and implement `match()` method. 35 | """ 36 | 37 | # List of operators keywords (required) 38 | operators = ('equal', 'same', 'eql') 39 | 40 | # Chain DSL aliases (optional) 41 | aliases = ('to', 'of', 'type', 'as') 42 | 43 | # Expected custom message template (optional) 44 | expected_message = Operator.Dsl.Message( 45 | 'expected a value that of type "{type}" that is equal to "{value}"', 46 | 'expected a a value that is not equal to "{value}"', 47 | ) 48 | 49 | def match(self, subject, expected, **kw): 50 | return subject == expected, ['subject is not equal to {}'.format(expected)] 51 | 52 | 53 | Alternatively, you can create and self-register simple, small operators via decorator: 54 | 55 | .. code-block:: python 56 | 57 | import grappa 58 | 59 | @grappa.accessor 60 | def my_accessor_operator(ctx, subject, **kw): 61 | return len(subject) > 3, ['subject length must be higher than 3'] 62 | 63 | 64 | .. code-block:: python 65 | 66 | import grappa 67 | 68 | @grappa.matcher 69 | def my_matcher_operator(ctx, subject, expected, **kw): 70 | return subject == expected, ['values are not equal'] 71 | 72 | 73 | Registering the plugin 74 | ^^^^^^^^^^^^^^^^^^^^^^ 75 | 76 | .. code-block:: python 77 | 78 | import grappa 79 | 80 | # Explicitly register operators 81 | def my_plugin_register(engine): 82 | engine.register(MyEqualOperator, MyOtherOperator) 83 | 84 | grappa.use(my_plugin_register) 85 | 86 | 87 | Alternatively, you can self-register your operator classes via ``register`` decorator: 88 | 89 | .. code-block:: python 90 | 91 | import grappa 92 | 93 | @grappa.register 94 | class MyCustomOeprator(grappa.Operator) 95 | pass 96 | 97 | 98 | .. _http: https://github.com/grappa-py/http 99 | .. _server: https://github.com/grappa-py/server 100 | -------------------------------------------------------------------------------- /docs/style.rst: -------------------------------------------------------------------------------- 1 | Assertion Styles 2 | ================ 3 | 4 | ``grappa`` is a behavior-oriented library that comes in two flavors: ``expect`` and ``should``. 5 | 6 | Both use the same chainable language to construct assertions, but they differ 7 | in the way an assertion is initially constructed by the use of different operators DSL. 8 | 9 | The BDD style is exposed through ``expect`` or ``should`` interfaces. 10 | In both scenarios, you chain together natural language assertions. 11 | 12 | 13 | should 14 | ------ 15 | 16 | The should style allows for the same chainable assertions as the ``expect`` interface, 17 | however it extends each object with a should property to start your chain. 18 | 19 | .. code-block:: python 20 | 21 | from grappa import should 22 | 23 | foo = 'bar' 24 | beverages = { 'tea': [ 'grappa', 'matcha', 'long' ] } 25 | 26 | foo | should.be.a('string') 27 | foo | should.equal('bar') 28 | foo | should.have.length.of(3) 29 | beverages | should.have.property('tea').with.length.of(3) 30 | 31 | 32 | .. note:: 33 | 34 | ``should`` can be used as a function like how the ``expect`` verb is usually used. 35 | 36 | .. code-block:: python 37 | 38 | should(foo).be.a('string') 39 | should('foo').to.be.equal('foo') 40 | should('foo').have.length.of(3) 41 | should(beverages).have.property('tea').with.length.of(3) 42 | 43 | 44 | .. tip:: 45 | 46 | For this assertion style, accessors and matchers operators will raise ``pylint`` 47 | warnings that can be disabled with these comments. 48 | 49 | To ignore rules for the whole module place them before ``import`` statements. 50 | 51 | .. code-block:: python 52 | 53 | True | should.be.true # pylint: disable=W0104 54 | True | should.be.true # pylint: disable=pointless-statement 55 | 56 | True | should.equal(True) # pylint: disable=W0106 57 | True | should.equal(True) # pylint: disable=expression-not-assigned 58 | 59 | expect 60 | ------ 61 | 62 | .. code-block:: python 63 | 64 | from grappa import expect 65 | 66 | foo = 'bar' 67 | beverages = { 'tea': [ 'grappa', 'matcha', 'long' ] } 68 | 69 | expect(foo).to.be.a('string') 70 | expect(foo).to.equal('bar') 71 | expect(foo).to.have.length.of(3) 72 | expect(beverages).to.have.property('tea').that.has.length.of(3) 73 | 74 | 75 | .. note:: 76 | 77 | ``expect`` can be used with piping operator like how the ``should`` verb is usually used. 78 | 79 | .. code-block:: python 80 | 81 | foo | expect.to.be.a('string') 82 | foo | expect.to.equal('bar') 83 | foo | expect.to.have.length.of(3) 84 | beverages | expect.to.have.property('tea').that.has.length.of(3) 85 | 86 | 87 | .. tip:: 88 | 89 | For this assertion style, accessors operators will raise ``pylint`` 90 | warnings that can be disabled with these comments. 91 | 92 | To ignore rules for the whole module place them before ``import`` statements. 93 | 94 | .. code-block:: python 95 | 96 | expect(True).to.be.true # pylint: disable=W0106 97 | expect(True).to.be.true # pylint: disable=expression-not-assigned 98 | 99 | -------------------------------------------------------------------------------- /tests/operators/keys_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from array import array 3 | 4 | 5 | def test_expect_key(should): 6 | {'foo': 'bar'} | should.have.key('foo') 7 | {'foo': 'bar'} | should.have.key('foo') > should.be.equal.to('bar') 8 | 'bar' | should.be.equal.to('bar') 9 | 10 | ({'foo': {'bar': True}} 11 | | should.have.key('foo') 12 | > should.be.a('dict') 13 | > should.have.length.of(1) 14 | > should.have.key('bar') 15 | > should.be.true) 16 | 17 | should({'foo': 'bar'}).have.key('foo') > should.be.equal.to('bar') 18 | should({'foo': 'bar'}).have.key('foo').which.should.be.equal.to('bar') 19 | 20 | with pytest.raises(AssertionError): 21 | {'foo': 'bar'} | should.have.key('bar') 22 | 23 | with pytest.raises(AssertionError): 24 | [] | should.have.key('bar') 25 | 26 | with pytest.raises(AssertionError): 27 | should({'foo': 'bar'}).have.key('foo') > should.be.equal.to('foo') 28 | 29 | with pytest.raises(AssertionError): 30 | should({'foo': 'bar'}).have.key('foo').which.should.be.equal.to('pepe') 31 | 32 | 33 | def test_not_expect_key(should): 34 | {'foo': 'bar'} | should.not_have.key('foobar') 35 | 36 | {'foo': 'bar', 'fuu': 'bor'} | should.not_have.key('foobar') 37 | 38 | with pytest.raises(AssertionError): 39 | {'foo': 'bar', 'fuu': 'bor'} | should.not_have.key('foo') 40 | 41 | 42 | def test_expect_keys(should): 43 | myDict = {'foo': 'bar', 'fuu': 'bor'} 44 | 45 | myDict | should.have.keys('foo', 'fuu') 46 | 47 | myDict | should.have.keys(('foo', 'fuu')) 48 | 49 | myDict | should.have.keys(['foo', 'fuu']) 50 | 51 | myDict | should.have.keys({'foo', 'fuu'}) 52 | 53 | {1: 'bar', 2: 'bor'} | should.have.keys(array('i', [1, 2])) 54 | 55 | with pytest.raises(AssertionError): 56 | myDict | should.not_have.keys('foo', 'fuu') 57 | 58 | with pytest.raises(AssertionError): 59 | myDict | should.not_have.keys(('foo', 'fuu')) 60 | 61 | with pytest.raises(AssertionError): 62 | myDict | should.not_have.keys(['foo', 'fuu']) 63 | 64 | with pytest.raises(AssertionError): 65 | myDict | should.not_have.keys({'foo', 'fuu'}) 66 | 67 | with pytest.raises(AssertionError): 68 | {1: 'bar', 2: 'bor'} | should.not_have.keys(array('i', [1, 2])) 69 | 70 | 71 | def test_not_expect_keys(should): 72 | myDict = {'foo': 'baz', 'fuu': 'boz'} 73 | 74 | myDict | should.not_have.keys('foo', 'bar') 75 | 76 | myDict | should.not_have.keys(('foo', 'bar')) 77 | 78 | myDict | should.not_have.keys(['foo', 'bar']) 79 | 80 | myDict | should.not_have.keys({'foo', 'bar'}) 81 | 82 | {1: 'baz', 2: 'boz'} | should.not_have.keys(array('i', [1, 5])) 83 | 84 | with pytest.raises(AssertionError): 85 | myDict | should.have.keys('foo', 'bar') 86 | 87 | with pytest.raises(AssertionError): 88 | myDict | should.have.keys(('foo', 'bar')) 89 | 90 | with pytest.raises(AssertionError): 91 | myDict | should.have.keys(['foo', 'bar']) 92 | 93 | with pytest.raises(AssertionError): 94 | myDict | should.have.keys({'foo', 'bar'}) 95 | 96 | with pytest.raises(AssertionError): 97 | {1: 'baz', 2: 'boz'} | should.have.keys(array('i', [1, 5])) 98 | -------------------------------------------------------------------------------- /tests/operators/bool_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from grappa.operators.bool import TrueOperator, FalseOperator 3 | 4 | 5 | def test_should_true(should): 6 | True | should.be.true 7 | 8 | with pytest.raises(AssertionError): 9 | False | should.be.true 10 | 11 | with pytest.raises(AssertionError): 12 | None | should.be.true 13 | 14 | with pytest.raises(AssertionError): 15 | 'foo' | should.be.true 16 | 17 | with pytest.raises(AssertionError): 18 | [1, 2, 3] | should.be.true 19 | 20 | 21 | def test_expect_true(expect): 22 | True | expect.to.be.true 23 | 24 | with pytest.raises(AssertionError): 25 | False | expect.to.be.true 26 | 27 | with pytest.raises(AssertionError): 28 | None | expect.to.be.true 29 | 30 | with pytest.raises(AssertionError): 31 | 'foo' | expect.to.be.true 32 | 33 | with pytest.raises(AssertionError): 34 | [1, 2, 3] | expect.to.be.true 35 | 36 | 37 | def test_should_false(should): 38 | False | should.be.false 39 | 40 | with pytest.raises(AssertionError): 41 | True | should.be.false 42 | 43 | with pytest.raises(AssertionError): 44 | None | should.be.false 45 | 46 | with pytest.raises(AssertionError): 47 | 'foo' | should.be.false 48 | 49 | with pytest.raises(AssertionError): 50 | [1, 2, 3] | should.be.false 51 | 52 | 53 | def test_expect_false(expect): 54 | False | expect.to.be.false 55 | 56 | with pytest.raises(AssertionError): 57 | True | expect.to.be.false 58 | 59 | with pytest.raises(AssertionError): 60 | None | expect.to.be.false 61 | 62 | with pytest.raises(AssertionError): 63 | 'foo' | expect.to.be.false 64 | 65 | with pytest.raises(AssertionError): 66 | [1, 2, 3] | expect.to.be.false 67 | 68 | 69 | def test_true_operator(ctx): 70 | assert TrueOperator(ctx).match(True) == (True, []) 71 | assert TrueOperator(ctx).match(False) == (False, []) 72 | 73 | assert TrueOperator(ctx).match(0) == (False, 74 | ['subject is not a bool type']) 75 | 76 | 77 | def test_true_operator_message(should): 78 | (TrueOperator 79 | | should.have.property('kind') 80 | > should.be.equal.to('accessor')) 81 | 82 | (TrueOperator 83 | | should.have.property('operators') 84 | > should.be.equal.to(('true',))) 85 | 86 | TrueOperator | should.have.property('aliases') > should.be.empty 87 | 88 | TrueOperator | should.have.property('expected_message') 89 | 90 | 91 | def test_false_operator(ctx): 92 | assert FalseOperator(ctx).match(False) == (True, []) 93 | assert FalseOperator(ctx).match(True) == (False, []) 94 | 95 | assert FalseOperator(ctx).match(0) == (False, 96 | ['subject is not a bool type']) 97 | 98 | 99 | def test_false_operator_message(should): 100 | (FalseOperator 101 | | should.have.property('kind') 102 | > should.be.equal.to('accessor')) 103 | 104 | (FalseOperator 105 | | should.have.property('operators') 106 | > should.be.equal.to(('false',))) 107 | 108 | FalseOperator | should.have.property('aliases') > should.be.empty 109 | 110 | FalseOperator | should.have.property('expected_message') 111 | -------------------------------------------------------------------------------- /docs/accessors-operators.rst: -------------------------------------------------------------------------------- 1 | Accessors Operators 2 | =================== 3 | 4 | These operators do not accept expectation arguments but performs assertion logic. 5 | 6 | Example operators: none_, true_, false_, empty_, callable_ ... 7 | 8 | 9 | true 10 | ---- 11 | 12 | Asserts if a given subject is `True` value. 13 | 14 | ======================= ======================== 15 | **Related operators** false_ 16 | ======================= ======================== 17 | 18 | .. code-block:: python 19 | 20 | 'foo' | should.be.true 21 | 'foo' | should.not_be.true 22 | 23 | .. code-block:: python 24 | 25 | expect('foo').to.be.true 26 | expect('foo').to_not.be.true 27 | 28 | 29 | false 30 | ----- 31 | 32 | Asserts if a given subject is `False` value. 33 | 34 | ======================= ======================== 35 | **Related operators** true_ 36 | ======================= ======================== 37 | 38 | .. code-block:: python 39 | 40 | 'foo' | should.be.false 41 | 'foo' | should.not_be.false 42 | 43 | .. code-block:: python 44 | 45 | expect('foo').to.be.false 46 | expect('foo').to_not.be.false 47 | 48 | 49 | callable 50 | -------- 51 | 52 | Asserts if a given subject is a callable type or an object that 53 | implements ``__call__()`` magic method. 54 | 55 | ======================= ======================== 56 | **Related operators** implements_ 57 | ======================= ======================== 58 | 59 | .. code-block:: python 60 | 61 | (lambda x: x) | should.be.callable 62 | None | should.not_be.callable 63 | 64 | .. code-block:: python 65 | 66 | expect(lambda x: x).to.be.callable 67 | expect(None).to_not.be.callable 68 | 69 | 70 | empty 71 | ----- 72 | 73 | Asserts if a given subject is an empty object. 74 | 75 | A subject is considered empty if it's ``None``, ``0`` or ``len(subject)`` 76 | is equals to ``0``. 77 | 78 | ======================= ======================== 79 | **Related operators** present_ none_ 80 | ======================= ======================== 81 | 82 | .. code-block:: python 83 | 84 | [] | should.be.empty 85 | [1, 2, 3] | should.not_be.empty 86 | 87 | .. code-block:: python 88 | 89 | expect(tuple()).to.be.empty 90 | expect((1, 2, 3)).to_not.be.empty 91 | 92 | 93 | none 94 | ---- 95 | 96 | Asserts if a given subject is ``None``. 97 | 98 | ======================= ======================== 99 | **Related operators** present_ empty_ 100 | ======================= ======================== 101 | 102 | .. code-block:: python 103 | 104 | None | should.be.none 105 | 'foo' | should.not_be.none 106 | 107 | .. code-block:: python 108 | 109 | expect(None).to.be.none 110 | expect('foo').to_not.be.none 111 | 112 | 113 | exists 114 | ------ 115 | present 116 | ------- 117 | 118 | Asserts if a given subject is not ``None`` or a negative value 119 | if evaluated via logical unary operator. 120 | 121 | This operator is the opposite of empty_. 122 | 123 | ======================= ======================== 124 | **Related operators** none_ empty_ 125 | ======================= ======================== 126 | 127 | .. code-block:: python 128 | 129 | 'foo' | should.be.present 130 | '' | should.not_be.present 131 | 132 | .. code-block:: python 133 | 134 | expect('foo').to.be.present 135 | expect(False).to_not.be.present 136 | 137 | 138 | .. _`implements`: http://grappa.readthedocs.io/en/latest/matchers-operators.html#implements 139 | -------------------------------------------------------------------------------- /grappa/operators/contain.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from array import array 3 | from six.moves import collections_abc 4 | import six 5 | 6 | from ..operator import Operator 7 | 8 | 9 | class ContainOperator(Operator): 10 | """ 11 | Asserts if a given value or values can be found 12 | in a another object. 13 | 14 | Example:: 15 | 16 | # Should style 17 | 'foo bar' | should.contain('bar') 18 | ['foo', 'bar'] | should.contain('bar') 19 | ['foo', 'bar'] | should.contain('foo', 'bar') 20 | [{'foo': True}, 'bar'] | should.contain({'foo': True}) 21 | 22 | # Should style - negation form 23 | 'foo bar' | should.do_not.contain('bar') 24 | ['foo', 'bar'] | should.do_not.contain('baz') 25 | 26 | # Expect style 27 | 'foo bar' | expect.to.contain('bar') 28 | ['foo', 'bar'] | expect.to.contain('bar') 29 | ['foo', 'bar'] | expect.to.contain('foo', 'bar') 30 | [{'foo': True}, 'bar'] | expect.to.contain({'foo': True}) 31 | 32 | # Expect style - negation form 33 | 'foo bar' | expect.to_not.contain('bar') 34 | ['foo', 'bar'] | expect.to_not.contain('baz') 35 | """ 36 | 37 | # Is the operator a keyword 38 | kind = Operator.Type.MATCHER 39 | 40 | # Enable diff report 41 | show_diff = True 42 | 43 | # Operator keywords 44 | operators = ('contain', 'contains', 'includes') 45 | 46 | # Operator chain aliases 47 | aliases = ('value', 'item', 'string', 'text', 'expression', 'data') 48 | 49 | # Expected template message 50 | expected_message = Operator.Dsl.Message( 51 | 'a value that contains "{value}"', 52 | 'a value that does not contains "{value}"', 53 | ) 54 | 55 | # Subject template message 56 | subject_message = Operator.Dsl.Message( 57 | 'a value of type "{type}" with content "{value}"', 58 | ) 59 | 60 | # Stores types to normalize before the assertion 61 | NORMALIZE_TYPES = ( 62 | collections_abc.Iterator, 63 | collections_abc.MappingView, 64 | collections_abc.Set, 65 | array 66 | ) 67 | 68 | LIST_TYPES = (tuple, list, set, array) 69 | 70 | def match(self, subject, *values): 71 | if isinstance(subject, self.NORMALIZE_TYPES): 72 | subject = list(subject) 73 | elif isinstance(subject, collections_abc.Mapping): 74 | subject = list(subject.values()) 75 | 76 | if not isinstance(subject, collections_abc.Sequence): 77 | return False, ['is not a valid sequence type'] 78 | 79 | reasons = [] 80 | 81 | if len(values) == 1 and isinstance(values[0], self.LIST_TYPES): 82 | values = list(values[0]) 83 | 84 | for value in values: 85 | matches_any, reason = self._matches_any(value, subject) 86 | reasons.append(reason) 87 | 88 | if not matches_any: 89 | return False, [reason] 90 | 91 | return True, reasons 92 | 93 | def _matches_any(self, expected, subject): 94 | if len(subject) == 0: 95 | return False, 'empty item' 96 | 97 | if isinstance(subject, six.string_types): 98 | if expected in subject: 99 | return True, 'item {0!r} found'.format(expected) 100 | return False, 'item {0!r} not found'.format(expected) 101 | 102 | for item in subject: 103 | if item == expected: 104 | return True, 'item {0!r} found'.format(expected) 105 | 106 | return False, 'item {0!r} not found'.format(expected) 107 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | grappa 4 | ====== 5 | Behavior-oriented, expressive assertion library for Python. 6 | 7 | :copyright: (c) 2017 Tomas Aparicio 8 | :license: MIT 9 | """ 10 | 11 | import os 12 | import sys 13 | import codecs 14 | from setuptools import setup, find_packages 15 | from setuptools.command.test import test as TestCommand 16 | 17 | # Publish command 18 | if sys.argv[-1] == 'publish': 19 | os.system('python setup.py sdist upload') 20 | sys.exit() 21 | 22 | 23 | setup_requires = [] 24 | if 'test' in sys.argv: 25 | setup_requires.append('pytest') 26 | 27 | 28 | def read_version(package): 29 | with open(os.path.join(package, '__init__.py'), 'r') as fd: 30 | for line in fd: 31 | if line.startswith('__version__ = '): 32 | return line.split()[-1].strip().strip("'") 33 | 34 | 35 | # Get package current version 36 | version = read_version('grappa') 37 | 38 | 39 | class PyTest(TestCommand): 40 | def finalize_options(self): 41 | TestCommand.finalize_options(self) 42 | self.test_args = ['tests/'] 43 | self.test_suite = True 44 | 45 | def run_tests(self): 46 | # import here, cause outside the eggs aren't loaded 47 | import pytest 48 | errno = pytest.main(self.test_args) 49 | sys.exit(errno) 50 | 51 | 52 | with codecs.open('requirements-dev.txt', encoding='utf-8') as f: 53 | tests_require = f.read().splitlines() 54 | with codecs.open('requirements.txt', encoding='utf-8') as f: 55 | install_requires = f.read().splitlines() 56 | with codecs.open('README.rst', encoding='utf-8') as f: 57 | readme = f.read() 58 | with codecs.open('History.rst', encoding='utf-8') as f: 59 | history = f.read() 60 | 61 | 62 | setup( 63 | name='grappa', 64 | version=version, 65 | author='Tomas Aparicio', 66 | author_email='tomas@aparicio.me', 67 | description=( 68 | 'Behavior-oriented, expressive, developer-friendly assertions library' 69 | ), 70 | url='https://github.com/grappa-py/grappa', 71 | license='MIT', 72 | long_description=readme + '\n\n' + history, 73 | long_description_content_type='text/x-rst', 74 | py_modules=['grappa'], 75 | zip_safe=False, 76 | tests_require=tests_require, 77 | install_requires=install_requires, 78 | packages=find_packages(exclude=['tests', 'examples']), 79 | package_data={'': [ 80 | 'LICENSE', 'README.rst', 'History.rst', 81 | 'requirements.txt', 'requirements-dev.txt' 82 | ]}, 83 | package_dir={'grappa': 'grappa'}, 84 | include_package_data=True, 85 | cmdclass={'test': PyTest}, 86 | classifiers=[ 87 | 'Intended Audience :: Developers', 88 | 'Intended Audience :: System Administrators', 89 | 'Operating System :: OS Independent', 90 | 'Development Status :: 4 - Beta', 91 | 'Natural Language :: English', 92 | 'License :: OSI Approved :: MIT License', 93 | 'Programming Language :: Python :: 2.7', 94 | 'Programming Language :: Python :: 3', 95 | 'Programming Language :: Python :: 3.4', 96 | 'Programming Language :: Python :: 3.5', 97 | 'Programming Language :: Python :: 3.6', 98 | 'Programming Language :: Python :: 3.7', 99 | 'Programming Language :: Python :: 3.8', 100 | 'Topic :: Software Development', 101 | 'Topic :: Software Development :: Libraries :: Python Modules', 102 | 'Programming Language :: Python :: Implementation :: CPython', 103 | 'Programming Language :: Python :: Implementation :: PyPy' 104 | ], 105 | ) 106 | -------------------------------------------------------------------------------- /grappa/operators/length.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..operator import Operator 3 | 4 | 5 | class LengthOperator(Operator): 6 | """ 7 | Asserts that a given iterable object has exact length. 8 | 9 | Example:: 10 | 11 | # Should style 12 | 'foo' | should.have.length(3) 13 | [1, 2, 3] | should.have.length.of(3) 14 | iter([1, 2, 3]) | should.have.length.equal.to(3) 15 | 16 | # Should style - negation form 17 | 'foobar' | should.not_have.length(3) 18 | [1, 2, 3, 4] | should.not_have.length.of(3) 19 | iter([1, 2, 3, 4]) | should.not_have.length.equal.to(3) 20 | 21 | # Expect style 22 | 'foo' | expect.to.have.length(3) 23 | [1, 2, 3] | expect.to.have.length.of(3) 24 | iter([1, 2, 3]) | expect.to.have.length.equal.to(3) 25 | 26 | # Expect style - negation form 27 | 'foobar' | expect.to_not.have.length(3) 28 | [1, 2, 3, 4] | expect.to_not.have.length.of(3) 29 | iter([1, 2, 3, 4]) | expect.to_not.have.length.equal.to(3) 30 | """ 31 | 32 | # Is the operator a keyword 33 | kind = Operator.Type.MATCHER 34 | 35 | # Disable diff report 36 | show_diff = False 37 | 38 | # Operator keywords 39 | operators = ('length', 'size') 40 | 41 | # Operator chain aliases 42 | aliases = ('equal', 'to', 'of') 43 | 44 | # Error message templates 45 | expected_message = Operator.Dsl.Message( 46 | 'an object that its length is equal to {value}', 47 | 'an object that its length is not equal to {value}', 48 | ) 49 | 50 | # Subject template message 51 | subject_message = Operator.Dsl.Message( 52 | 'an object of type "{type}" with length {length}' 53 | ) 54 | 55 | information = ( 56 | Operator.Dsl.Help( 57 | Operator.Dsl.Description( 58 | 'Object length is measured by using "len()" built-in', 59 | 'Python function or consuming an lazy iterable, such as a', 60 | 'generator. Most built-in types and objects in Python', 61 | 'can be tested that way, such as str, list, tuple, dict...', 62 | 'as well as any object that implements "__len__()" method.' 63 | ), 64 | Operator.Dsl.Reference( 65 | 'https://docs.python.org/3/library/functions.html#len' 66 | ), 67 | ), 68 | ) 69 | 70 | def __init__(self, *args, **kw): 71 | Operator.__init__(self, *args, **kw) 72 | 73 | def is_number(self, subject): 74 | return isinstance(subject, (int, float)) 75 | 76 | def on_access(self, subject): 77 | # If already a number, just continue with it 78 | if self.is_number(subject): 79 | self.ctx.length = False 80 | return True, [] 81 | 82 | # Set original subject reference 83 | try: 84 | self.ctx.subject = len(subject) 85 | self.ctx.length = False 86 | except Exception: 87 | return False, ['cannot measure length of the given object'] 88 | 89 | return True, [] 90 | 91 | def match(self, subject, expected): 92 | try: 93 | length = subject if self.is_number(subject) else len(subject) 94 | return length == expected, [ 95 | 'unexpected object length: {}'.format(length)] 96 | except TypeError: 97 | try: 98 | return len([i for i in subject]) == expected 99 | except Exception: 100 | pass 101 | return False, ['cannot measure the length of the given object'] 102 | -------------------------------------------------------------------------------- /grappa/assertion.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .base import BaseTest 3 | 4 | 5 | class AssertionProxy(BaseTest): 6 | """ 7 | AssertionProxy is used as proxy delegator call chain for test 8 | definitions. 9 | """ 10 | 11 | def __init__(self, resolver, operator, fn): 12 | self._called = False 13 | self._accessed = False 14 | self._fn = fn 15 | self._op = operator 16 | self._resolver = resolver 17 | self._test = resolver.test 18 | 19 | def __call__(self, expected, *args, **kw): 20 | if self._called: 21 | raise RuntimeError('grappa: operator already called') 22 | if not self._called: 23 | self._called = True 24 | return self._fn(expected, *args, **kw) 25 | 26 | def _match_alias(self, name): 27 | return hasattr(self._op, 'aliases') and name in self._op.aliases 28 | 29 | def _match_suboperator(self, name): 30 | def create(operator): 31 | return self._resolver.run_matcher(operator(self._test._ctx)) 32 | 33 | # Check that operator has suboperators 34 | if not getattr(self._op, 'suboperators', None): 35 | return False 36 | 37 | # Match parent operators 38 | for op in self._op.suboperators: 39 | # Match suboperator names ignoring first words 40 | for opname in op.operators: 41 | if name == opname: 42 | return create(op) 43 | if '_' in opname and name == '_'.join(opname.split('_')[1:]): 44 | return create(op) 45 | 46 | def _on_access(self): 47 | # Define on_access operator function 48 | def on_access(subject): 49 | try: 50 | self._op.on_access(subject) 51 | finally: 52 | return True 53 | 54 | # Add assertion function 55 | self._test._engine.add_assertion(on_access) 56 | 57 | # Flag proxy state as accessed operator 58 | self._accessed = True 59 | 60 | def is_on_access(self): 61 | # Trigger operator on_access operator event method, if available 62 | return hasattr(self._op, 'on_access') and not self._accessed 63 | 64 | def __getattr__(self, name): 65 | """ 66 | Overloads attribute accessor in order to load the assertion 67 | operator dynamically. 68 | """ 69 | # Match operator aliases for chaining 70 | if self._match_alias(name): 71 | self._test._engine.add_keyword(name) 72 | return self 73 | 74 | # Trigger operator on_access operator event method, if available 75 | if self.is_on_access(): 76 | self._on_access() 77 | 78 | # Match suboperator, if needed 79 | suboperator = self._match_suboperator(name) 80 | if suboperator: 81 | # Register keyword for suboperator 82 | self._test._engine.add_keyword(name) 83 | 84 | # Trigger suboperator on_access event method, if available 85 | if suboperator.is_on_access(): 86 | suboperator._on_access() 87 | 88 | # Return suboperator proxy 89 | return suboperator 90 | 91 | # Delegate access to global assertion instance 92 | return getattr(self._test, name) 93 | 94 | def __ror__(self, value): 95 | """ 96 | Overloads ``|`` operator. 97 | """ 98 | return self._fn(value)._trigger() 99 | 100 | def __gt__(self, value): 101 | """ 102 | Overloads ``>`` operator. 103 | """ 104 | return self.__ror_(value) 105 | -------------------------------------------------------------------------------- /grappa/operators/been_called_with.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..decorators import mock_implementation_validator 3 | from ..operator import Operator 4 | 5 | 6 | class BeenCalledWithOperator(Operator): 7 | """ 8 | Asserts if a given mock subject have been called at least once 9 | with specified arguments. 10 | 11 | Warning:: 12 | 13 | Piping style assertions is not yet supported. 14 | 15 | Example:: 16 | 17 | # Should style 18 | should(mock).have.been_called_with('foo') 19 | 20 | # Should style - negation form 21 | should(mock).have_not.been_called_with('foo', 10) 22 | 23 | # Expect style 24 | expect(mock).to.have.been_called_with('foo') 25 | 26 | # Expect style - negation form 27 | expect(mock).to.have_not.been_called_with('foo', True, 150) 28 | expect(mock).to_not.have.been_called_with('foo') 29 | """ 30 | 31 | # Is the operator a keyword 32 | kind = Operator.Type.MATCHER 33 | 34 | # Disable diff report 35 | show_diff = False 36 | 37 | # Operator keywords 38 | operators = ('been_called_with',) 39 | 40 | # Error message templates 41 | expected_message = Operator.Dsl.Message( 42 | 'a mock that has been called at least once with arguments', 43 | 'a mock that has not been called with arguments', 44 | ) 45 | 46 | # Subject message template 47 | subject_message = Operator.Dsl.Message( 48 | 'a mock that has not been called with arguments', 49 | 'a mock that has been called at least once with arguments', 50 | ) 51 | 52 | @mock_implementation_validator 53 | def match(self, subject, *args, **kwargs): 54 | try: 55 | subject.assert_called_with(*args, **kwargs) 56 | return True 57 | except AssertionError as error: 58 | return False, error.args[0].splitlines() 59 | 60 | 61 | class BeenCalledOnceWithOperator(Operator): 62 | """ 63 | Asserts if a given mock subject have been called once 64 | with specified arguments. 65 | 66 | Warning:: 67 | 68 | Piping style assertions is not yet supported. 69 | 70 | Example:: 71 | 72 | # Should style 73 | should(mock).have.been_called_once_with('foo') 74 | 75 | # Should style - negation form 76 | should(mock).have_not.been_called_once_with('foo', 10) 77 | 78 | # Expect style 79 | expect(mock).to.have.been_called_once_with('foo') 80 | 81 | # Expect style - negation form 82 | expect(mock).to.have_not.been_called_once_with('foo', True, 150) 83 | expect(mock).to_not.have.been_called_once_with('foo') 84 | """ 85 | 86 | # Is the operator a keyword 87 | kind = Operator.Type.MATCHER 88 | 89 | # Disable diff report 90 | show_diff = False 91 | 92 | # Operator keywords 93 | operators = ('been_called_once_with',) 94 | 95 | # Error message templates 96 | expected_message = Operator.Dsl.Message( 97 | 'a mock that has been called once with arguments', 98 | 'a mock that has not been called once with arguments', 99 | ) 100 | 101 | # Subject message template 102 | subject_message = Operator.Dsl.Message( 103 | 'a mock that has been called {call_count} time(s) with arguments', 104 | 'a mock that has been called once with arguments', 105 | ) 106 | 107 | @mock_implementation_validator 108 | def match(self, subject, *args, **kwargs): 109 | try: 110 | subject.assert_called_once_with(*args, **kwargs) 111 | return True 112 | except AssertionError as error: 113 | return False, error.args[0].splitlines() 114 | -------------------------------------------------------------------------------- /grappa/reporters/code.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | import linecache 4 | import traceback 5 | import functools 6 | from colorama import Fore, Style 7 | 8 | from ..config import config 9 | from .base import BaseReporter 10 | 11 | 12 | class Trace(object): 13 | """ 14 | Python < 3.4 traceback compatibility wrapper for Python +3.5 15 | """ 16 | 17 | def __init__(self, trace): 18 | self.name = trace[2] 19 | self.line = trace[-1] 20 | self.lineno = trace[1] 21 | self.filename = trace[0] 22 | 23 | 24 | class CodeReporter(BaseReporter): 25 | """ 26 | CodeReporter matches and renders the fragment of the code 27 | """ 28 | 29 | title = 'Where' 30 | 31 | LINES = 8 32 | INDENT_SPACES = 4 33 | 34 | # Regular expression for invokation based expressions 35 | PIPE_EXPR = re.compile(r'[\|][\s+]?(should|expect)[\.]') 36 | FN_CALL_EXPR = re.compile(r'(should|expect)[\s+]?[\(|\.]') 37 | 38 | # Context manager based assertions that does not imply new test calls. 39 | CONTEXT_EXPR = re.compile( 40 | r'[\.](not)?[\_]?(have|has|be|to|that|\_is|is\_not|' 41 | r'satisfy|which|that\_is|which\_is|include)[\_]?(not)?[\.]') 42 | 43 | def match_line(self, line): 44 | return any([ 45 | CodeReporter.PIPE_EXPR.search(line), 46 | CodeReporter.FN_CALL_EXPR.search(line), 47 | CodeReporter.CONTEXT_EXPR.search(line) 48 | ]) 49 | 50 | def find_trace(self): 51 | # Get latest exception stack 52 | trace_list = list(traceback.extract_stack()) 53 | trace_list.reverse() 54 | 55 | for trace in trace_list: 56 | # Normalize traceback in old Python versions 57 | if isinstance(trace, tuple): 58 | trace = Trace(trace) 59 | # Match code line 60 | if self.match_line(trace.line): 61 | return trace 62 | 63 | def header(self, trace): 64 | return 'File "{}", line {}, in {}\n\n'.format( 65 | trace.filename, 66 | trace.lineno, 67 | trace.name, 68 | ) 69 | 70 | def render_code(self, trace): 71 | lmin = trace.lineno - CodeReporter.LINES 72 | lmax = trace.lineno + CodeReporter.LINES 73 | max_len = max(len(str(lmin)), len(str(lmax))) 74 | 75 | def calculate_space(line): 76 | return ' ' * (max_len - len(str(line))) 77 | 78 | def render_lines(buf, line): 79 | # Retrieve line of code from the trackback 80 | code = linecache.getline(trace.filename, line) 81 | if len(code) == 0: 82 | return buf 83 | 84 | # Calculate indentation and line head space for aligment 85 | space = calculate_space(line) 86 | indent = ' ' * CodeReporter.INDENT_SPACES 87 | 88 | # Line head 89 | head = '{}{}{}| {}'.format( 90 | Fore.GREEN if config.use_colors else '', 91 | space, line, 92 | Style.RESET_ALL if config.use_colors else '' 93 | ) 94 | 95 | if line == trace.lineno: 96 | if config.use_colors: 97 | head += Fore.RED + '> ' + Style.RESET_ALL 98 | else: 99 | head += '> ' 100 | else: 101 | head += ' ' 102 | 103 | buf.append(indent + head + code) 104 | return buf 105 | 106 | code = functools.reduce(render_lines, range(lmin, lmax), []) 107 | return self.header(trace) + (''.join(code)) 108 | 109 | def run(self, error): 110 | if not config.show_code: 111 | return None 112 | 113 | trace = self.find_trace() 114 | return self.render_code(trace) if trace else None 115 | -------------------------------------------------------------------------------- /grappa/operators/property.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..operator import Operator 3 | 4 | 5 | class PropertyOperator(Operator): 6 | """ 7 | Asserts if a given object has property or properties. 8 | 9 | Example:: 10 | 11 | class Foo(object): 12 | bar = 'foo' 13 | 14 | def baz(): 15 | pass 16 | 17 | # Should style 18 | Foo() | should.have.property('bar') 19 | Foo() | should.have.properties('bar', 'baz') 20 | Foo() | should.have.properties.present.equal.to('bar', 'baz') 21 | 22 | # Should style - negation form 23 | Foo() | expect.to_not.have.property('bar') 24 | Foo() | expect.to_not.have.properties('bar', 'baz') 25 | Foo() | expect.to_not.have.properties.present.equal.to('bar', 'baz') 26 | 27 | # Expect style 28 | Foo() | expect.to.have.property('bar') 29 | Foo() | expect.to.have.properties('bar', 'baz') 30 | Foo() | expect.to.have.properties.present.equal.to('bar', 'baz') 31 | 32 | # Expect style - negation form 33 | Foo() | expect.to_not.have.property('bar') 34 | Foo() | expect.to_not.have.properties('bar', 'baz') 35 | Foo() | expect.to_not.have.properties.present.equal.to('bar', 'baz') 36 | """ 37 | 38 | # Is the operator a keyword 39 | kind = Operator.Type.MATCHER 40 | 41 | # Disable diff report 42 | show_diff = False 43 | 44 | # Operator keywords 45 | operators = ('properties', 'property', 'attribute', 'attributes') 46 | 47 | # Operator chain aliases 48 | aliases = ('present', 'equal', 'to') 49 | 50 | # Expected template message 51 | expected_message = Operator.Dsl.Message( 52 | 'an object that has the following properties "{value}"', 53 | 'an object that has not the following properties "{value}"', 54 | ) 55 | 56 | # Subject template message 57 | subject_message = Operator.Dsl.Message( 58 | 'an object of type "{type}" with data "{value}"', 59 | ) 60 | 61 | def after_success(self, obj, *keys): 62 | if self.ctx.negate: 63 | return 64 | 65 | # Get attribute keys 66 | self.ctx.subject = [getattr(obj, x) for x in keys] 67 | 68 | if len(keys) == 1 and len(self.ctx.subject): 69 | self.ctx.subject = self.ctx.subject[0] 70 | 71 | def match(self, subject, *args, **kwargs): 72 | success_reasons = [] 73 | for name in args: 74 | has_property, reason = self._has_property(subject, name) 75 | if not has_property: 76 | return False, [reason] 77 | else: 78 | success_reasons.append(reason) 79 | 80 | for name, value in kwargs.items(): 81 | has_property, reason = self._has_property(subject, name, value) 82 | if not has_property: 83 | return False, [reason] 84 | else: 85 | success_reasons.append(reason) 86 | 87 | return True, success_reasons 88 | 89 | def _has_property(self, subject, name, *args): 90 | if args: 91 | try: 92 | value = getattr(subject, name) 93 | except AttributeError: 94 | return False, 'property {0!r} not found'.format(name) 95 | else: 96 | expected_value = args[0] 97 | result, _ = expected_value._match(value) 98 | if not result: 99 | return False, 'property {0!r} {1!r} not found'.format( 100 | name, expected_value) 101 | return True, 'property {0!r} {1!r} found'.format( 102 | name, expected_value) 103 | 104 | if not hasattr(subject, name): 105 | return False, 'property {0!r} not found'.format(name) 106 | return True, 'property {0!r} found'.format(name) 107 | -------------------------------------------------------------------------------- /grappa/operators/keys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from array import array 3 | from six.moves import collections_abc 4 | 5 | from ..operator import Operator 6 | 7 | 8 | class KeysOperator(Operator): 9 | """ 10 | Asserts that a given dictionary has a key or keys. 11 | 12 | Example:: 13 | 14 | # Should style 15 | {'foo': True} | should.have.key('foo') 16 | {'foo': True, 'bar': False} | should.have.keys('bar', 'foo') 17 | {'foo': True, 'bar': False} | should.have.keys(('bar', 'foo')) 18 | {'foo': True, 'bar': False} | should.have.keys(['bar', 'foo']) 19 | {'foo': True, 'bar': False} | should.have.keys({'bar', 'foo'}) 20 | {1: True, 2: False} | should.have.keys(array('i', [1, 2])) 21 | 22 | # Should style - negation form 23 | {'bar': True} | should.not_have.key('foo') 24 | {'baz': True, 'bar': False} | should.not_have.keys('bar', 'foo') 25 | {'baz': True, 'bar': False} | should.not_have.keys(('bar', 'foo')) 26 | {'baz': True, 'bar': False} | should.not_have.keys(['bar', 'foo']) 27 | {'baz': True, 'bar': False} | should.not_have.keys({'bar', 'foo'}) 28 | {10: True, 2: False} | should.not_have.keys(array('i', [1, 2])) 29 | 30 | # Expect style 31 | {'foo': True} | expect.to.have.key('foo') 32 | {'foo': True, 'bar': False} | expect.to.have.keys('bar', 'foo') 33 | {'foo': True, 'bar': False} | expect.to.have.keys(('bar', 'foo')) 34 | {'foo': True, 'bar': False} | expect.to.have.keys(['bar', 'foo']) 35 | {'foo': True, 'bar': False} | expect.to.have.keys({'bar', 'foo'}) 36 | {1: True, 2: False} | expect.to.have.keys(array('i', [1, 2])) 37 | 38 | # Expect style - negation form 39 | {'bar': True} | expect.to_not.have.key('foo') 40 | {'baz': True, 'bar': False} | expect.to_not.have.keys('bar', 'foo') 41 | {'baz': True, 'bar': False} | expect.to_not.have.keys(('bar', 'foo')) 42 | {'baz': True, 'bar': False} | expect.to_not.have.keys(['bar', 'foo']) 43 | {'baz': True, 'bar': False} | expect.to_not.have.keys({'bar', 'foo'}) 44 | {10: True, 2: False} | expect.to_not.have.keys(array('i', [1, 2])) 45 | """ 46 | 47 | # Is the operator a keyword 48 | kind = Operator.Type.MATCHER 49 | 50 | # Operator keywords 51 | operators = ('keys', 'key',) 52 | 53 | # Operator chain aliases 54 | aliases = ('present', 'equal', 'to') 55 | 56 | # Expected message templates 57 | expected_message = Operator.Dsl.Message( 58 | 'a dictionary-like object that has the key(s) "{value}"', 59 | 'a dictionary-like object that has not the key(s) "{value}"', 60 | ) 61 | 62 | # Subject template message 63 | subject_message = Operator.Dsl.Message( 64 | 'an object of type "{type}" with value "{value}"', 65 | ) 66 | 67 | LIST_TYPES = (tuple, list, set, array) 68 | 69 | def after_success(self, obj, *keys): 70 | if not self.ctx.negate: 71 | self.ctx.subject = [obj[x] for x in obj if x in keys] 72 | 73 | if len(keys) == 1 and len(self.ctx.subject): 74 | if isinstance(self.ctx.subject, list): 75 | self.ctx.subject = self.ctx.subject[0] 76 | else: 77 | self.ctx.subject = list(self.ctx.subject.keys())[0] 78 | 79 | def match(self, subject, *keys): 80 | if not isinstance(subject, collections_abc.Mapping): 81 | return False, ['subject is not a dict type'] 82 | 83 | reasons = [] 84 | 85 | if len(keys) == 1 and isinstance(keys[0], self.LIST_TYPES): 86 | keys = list(keys[0]) 87 | 88 | for name in keys: 89 | if name in subject: 90 | has_key = True 91 | reason = 'key {0!r} found'.format(name) 92 | else: 93 | has_key = False 94 | reason = 'key {0!r} not found'.format(name) 95 | 96 | if not has_key: 97 | return False, [reason] 98 | 99 | reasons.append(reason) 100 | 101 | return True, reasons 102 | -------------------------------------------------------------------------------- /grappa/operators/implements.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import inspect 3 | import functools 4 | from ..operator import Operator 5 | 6 | 7 | class ImplementsOperator(Operator): 8 | """ 9 | Asserts if a given object implements an interface of methods. 10 | 11 | Example:: 12 | 13 | class Foo(object): 14 | def bar(): 15 | pass 16 | 17 | def baz(): 18 | pass 19 | 20 | # Should style 21 | Foo() | should.implements('bar') 22 | Foo() | should.implements.method('bar') 23 | Foo() | should.implement.methods('bar', 'baz') 24 | Foo() | should.implement.interface('bar', 'baz') 25 | Foo() | should.satisfies.interface('bar', 'baz') 26 | 27 | # Should style - negation form 28 | Foo() | should.do_not.implements('bar') 29 | Foo() | should.do_not.implement.methods('bar', 'baz') 30 | Foo() | should.do_not.implement.interface('bar', 'baz') 31 | Foo() | should.do_not.satisfy.interface('bar', 'baz') 32 | 33 | # Expect style 34 | Foo() | expect.to.implement('bar') 35 | Foo() | expect.to.implement.method('bar') 36 | Foo() | expect.to.implement.methods('bar', 'baz') 37 | Foo() | expect.to.implement.interface('bar', 'baz') 38 | Foo() | expect.to.satisfy.interface('bar', 'baz') 39 | 40 | # Expect style - negation form 41 | Foo() | expect.to_not.implement('bar') 42 | Foo() | expect.to_not.implement.method('bar') 43 | Foo() | expect.to_not.implement.methods('bar', 'baz') 44 | Foo() | expect.to_not.implement.interface('bar', 'baz') 45 | Foo() | expect.to_not.satisfy.interface('bar', 'baz') 46 | """ 47 | 48 | # Is the operator a keyword 49 | kind = Operator.Type.MATCHER 50 | 51 | # Disable diff report 52 | show_diff = False 53 | 54 | # Operator keywords 55 | operators = ('implements', 'implement', 'interface') 56 | 57 | # Operator chain aliases 58 | aliases = ('interface', 'methods', 'method') 59 | 60 | # Expected template message 61 | expected_message = Operator.Dsl.Message( 62 | 'an object that implements the following members "{value}"', 63 | 'an object that does not implement the following members "{value}"', 64 | ) 65 | 66 | # Subject template message 67 | subject_message = Operator.Dsl.Message( 68 | 'an object of type "{type}" with content "{value}"', 69 | ) 70 | 71 | # Assertion information 72 | information = ( 73 | Operator.Dsl.Help( 74 | Operator.Dsl.Description( 75 | 'Object interface implementation is verified by using the', 76 | 'Python built-in function "hasattr" along with', 77 | '"inspect.ismethod()" function in order to infer if a given', 78 | 'object implements the required methods.' 79 | ), 80 | Operator.Dsl.Reference( 81 | 'https://docs.python.org/3.6/library/functions.html#hasattr' 82 | ), 83 | ), 84 | ) 85 | 86 | def has_method(self, cls, method): 87 | return any([inspect.ismethod(getattr(cls, method)), 88 | inspect.isfunction(getattr(cls, method))]) 89 | 90 | def match(self, cls, *methods): 91 | if len(methods) == 0: 92 | return False, ['methods to implement cannot be empty'] 93 | 94 | def validate(reasons, method): 95 | try: 96 | if not hasattr(cls, method): 97 | template = 'object does not have property "{}"' 98 | reasons.append(template.format(method)) 99 | elif not self.has_method(cls, method): 100 | template = 'object does not implement method "{}"' 101 | reasons.append(template.format(method)) 102 | except Exception: 103 | template = '"{}" is a "{}" but it must be a string' 104 | reasons.append(template.format( 105 | method, method.__class__.__name__ 106 | )) 107 | return reasons 108 | 109 | reasons = functools.reduce(validate, methods, []) 110 | return len(reasons) == 0, reasons 111 | -------------------------------------------------------------------------------- /grappa/operators/been_called.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ..decorators import mock_implementation_validator 3 | from ..operator import Operator 4 | 5 | 6 | class BeenCalledOperator(Operator): 7 | """ 8 | Asserts if a given mock subject have been called at least once. 9 | 10 | Warning:: 11 | 12 | Piping style assertions is not yet supported. 13 | 14 | Example:: 15 | 16 | # Should style 17 | should(mock).have.been_called 18 | 19 | # Should style - negation form 20 | should(mock).have_not.been_called 21 | 22 | # Expect style 23 | expect(mock).to.have.been_called 24 | 25 | # Expect style - negation form 26 | expect(mock).to.have_not.been_called 27 | expect(mock).to_not.have.been_called 28 | """ 29 | 30 | # Is the operator a keyword 31 | kind = Operator.Type.ACCESSOR 32 | 33 | # Disable diff report 34 | show_diff = False 35 | 36 | # Operator keywords 37 | operators = ('been_called',) 38 | 39 | # Error message templates 40 | expected_message = Operator.Dsl.Message( 41 | 'a mock that has been called at least once', 42 | 'a mock that has not been called', 43 | ) 44 | 45 | # Subject message template 46 | subject_message = Operator.Dsl.Message( 47 | 'a mock that has not been called', 48 | 'a mock that has been called at least once', 49 | ) 50 | 51 | @mock_implementation_validator 52 | def match(self, subject): 53 | return subject.called 54 | 55 | 56 | class BeenCalledOnceOperator(Operator): 57 | """ 58 | Asserts if a given mock subject have been called once. 59 | 60 | Warning:: 61 | 62 | Piping style assertions is not yet supported. 63 | 64 | Example:: 65 | 66 | # Should style 67 | should(mock).have.been_called_once 68 | 69 | # Should style - negation form 70 | should(mock).have_not.been_called_once 71 | 72 | # Expect style 73 | expect(mock).to.have.been_called_once 74 | 75 | # Expect style - negation form 76 | expect(mock).to.have_not.been_called_once 77 | expect(mock).to_not.have.been_called_once 78 | """ 79 | 80 | # Is the operator a keyword 81 | kind = Operator.Type.ACCESSOR 82 | 83 | # Disable diff report 84 | show_diff = False 85 | 86 | # Operator keywords 87 | operators = ('been_called_once',) 88 | 89 | # Error message templates 90 | expected_message = Operator.Dsl.Message( 91 | 'a mock that has been called once', 92 | 'a mock that has not been called once', 93 | ) 94 | 95 | # Subject message template 96 | subject_message = Operator.Dsl.Message( 97 | 'a mock that has been called {call_count} time(s)', 98 | 'a mock that has been called once', 99 | ) 100 | 101 | @mock_implementation_validator 102 | def match(self, subject): 103 | return subject.call_count == 1 104 | 105 | 106 | class BeenCalledTimesOperator(Operator): 107 | """ 108 | Asserts if a given mock subject have been called n times. 109 | 110 | Warning:: 111 | 112 | Piping style assertions is not yet supported. 113 | 114 | Example:: 115 | 116 | # Should style 117 | should(mock).have.been_called_times(3) 118 | 119 | # Should style - negation form 120 | should(mock).have_not.been_called_times(3) 121 | 122 | # Expect style 123 | expect(mock).to.have.been_called_times(0) 124 | 125 | # Expect style - negation form 126 | expect(mock).to.have_not.been_called_times(1) 127 | expect(mock).to_not.have.been_called_times(3) 128 | """ 129 | 130 | # Is the operator a keyword 131 | kind = Operator.Type.MATCHER 132 | 133 | # Disable diff report 134 | show_diff = True 135 | 136 | # Operator keywords 137 | operators = ('been_called_times',) 138 | 139 | # Error message templates 140 | expected_message = Operator.Dsl.Message( 141 | 'a mock that has been called {value} times', 142 | 'a mock that has not been called {value} times', 143 | ) 144 | 145 | subject_message = Operator.Dsl.Message( 146 | 'a mock that has been called {call_count} times', 147 | 'a mock that has not been called {call_count} times', 148 | ) 149 | 150 | @mock_implementation_validator 151 | def match(self, subject, expected): 152 | return subject.call_count == expected 153 | -------------------------------------------------------------------------------- /grappa/decorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import inspect 3 | import functools 4 | import six 5 | from .api import TestProxy 6 | from .engine import Engine 7 | from .operator import Operator 8 | 9 | 10 | # Explicit module members to export 11 | __all__ = ( 12 | 'operator', 13 | 'attribute', 14 | 'register' 15 | ) 16 | 17 | 18 | def operator(name=None, operators=None, aliases=None, kind=None): 19 | """ 20 | Registers a new operator function in the test engine. 21 | 22 | Arguments: 23 | *args: variadic arguments. 24 | **kw: variadic keyword arguments. 25 | 26 | Returns: 27 | function 28 | """ 29 | def delegator(assertion, subject, expected, *args, **kw): 30 | return assertion.test(subject, expected, *args, **kw) 31 | 32 | def decorator(fn): 33 | operator = Operator(fn=fn, aliases=aliases, kind=kind) 34 | _name = name if isinstance(name, six.string_types) else fn.__name__ 35 | operator.operators = (_name,) 36 | 37 | _operators = operators 38 | if isinstance(_operators, list): 39 | _operators = tuple(_operators) 40 | 41 | if isinstance(_operators, tuple): 42 | operator.operators += _operators 43 | 44 | # Register operator 45 | Engine.register(operator) 46 | return functools.partial(delegator, operator) 47 | 48 | return decorator(name) if inspect.isfunction(name) else decorator 49 | 50 | 51 | def attribute(*args, **kw): 52 | """ 53 | Registers a new attribute only operator function in the test engine. 54 | 55 | Arguments: 56 | *args: variadic arguments. 57 | **kw: variadic keyword arguments. 58 | 59 | Returns: 60 | function 61 | """ 62 | return operator(kind=Operator.Type.ATTRIBUTE, *args, **kw) 63 | 64 | 65 | def register(operator): 66 | """ 67 | Registers a new operator class in the test engine. 68 | 69 | Arguments: 70 | operator (Operator): operator class to register. 71 | 72 | Returns: 73 | Operator: operator class. 74 | """ 75 | Engine.register(operator) 76 | return operator 77 | 78 | 79 | def mock_implementation_validator(func): 80 | """ 81 | Validate that a mock conform to the implementation required to run 82 | have_been and have_been_with operators. 83 | 84 | Otherwise returns a Grappa tuple with missing implementation reasons. 85 | 86 | Arguments: 87 | operator (Operator): a been_called or been_called_with operator. 88 | subject: a mock whose implementation is to be validated. 89 | *args: variadic arguments. 90 | **kw: variadic keyword arguments. 91 | 92 | Returns: 93 | (function|tuple) 94 | """ 95 | @functools.wraps(func) 96 | def wrapper(operator, subject, *args, **kwargs): 97 | expect = TestProxy('expect') 98 | 99 | propery_reason_template = 'a property named "{}" is expected' 100 | 101 | def validate_props(reasons, prop): 102 | try: 103 | expect(subject).to.have.property(prop) 104 | except AssertionError: 105 | reasons.append(propery_reason_template.format(prop)) 106 | return reasons 107 | 108 | method_reason_template = 'a method named "{}" is expected' 109 | 110 | def validate_methods(reasons, method): 111 | try: 112 | expect(subject).to.implement.methods(method) 113 | except AssertionError: 114 | reasons.append(method_reason_template.format(method)) 115 | return reasons 116 | 117 | expected_props = ('called', 'call_count') 118 | reasons = functools.reduce(validate_props, expected_props, []) 119 | 120 | expected_methods = ('assert_called_with', 'assert_called_once_with') 121 | reasons = functools.reduce(validate_methods, expected_methods, reasons) 122 | 123 | if reasons: 124 | operator.information = ( 125 | Operator.Dsl.Help( 126 | Operator.Dsl.Description( 127 | 'Required implementation is based on unittest.mock.Mock class.', # noqa E501 128 | '', 129 | 'Properties required', 130 | ' {}'.format(', '.join(expected_props)), 131 | 'Methods required', 132 | ' {}'.format(', '.join(expected_methods)), 133 | '', 134 | ), 135 | Operator.Dsl.Reference( 136 | 'https://docs.python.org/3/library/unittest.mock.html#the-mock-class', # noqa E501 137 | ), 138 | Operator.Dsl.Reference( 139 | 'https://pypi.org/project/pytest-mock/', 140 | ), 141 | ), 142 | ) 143 | 144 | reasons.insert(0, 'mock implementation is incomplete') 145 | return False, reasons 146 | 147 | return func(operator, subject, *args, **kwargs) 148 | 149 | return wrapper 150 | -------------------------------------------------------------------------------- /docs/error-reporting.rst: -------------------------------------------------------------------------------- 1 | Error Reporting 2 | =============== 3 | 4 | Feedback while testing is key. Seeing errors in your tests is not a nice thing 5 | because informs you something is wrong with your code. 6 | This can even introduce frustration and FUD to developers. 7 | 8 | ``grappa`` provides a detailed and **human friendly** behavior-oriented error reporting 9 | to intuitively and effectively help the developer answer the following questions: 10 | 11 | - what test failed 12 | - what are the reasons of the failure 13 | - what we expect to pass the test 14 | - what we got instead 15 | - where the error actually is (with embedded code) 16 | - how to solve the error 17 | - additional information about the how the assertion works 18 | 19 | 20 | Standard errors vs grappa errors 21 | -------------------------------- 22 | 23 | A typical assertion error report using ``nosetests``: 24 | 25 | .. code-block:: python 26 | 27 | def test_native_assertion(): 28 | x = [1, 2, 3] 29 | assert len(x) > 3 30 | 31 | .. code-block:: bash 32 | 33 | ====================================================================== 34 | FAIL: tests.should_test.test_should_api 35 | ---------------------------------------------------------------------- 36 | Traceback (most recent call last): 37 | File ".pyenv/versions/3.6.0/lib/python3.6/site-packages/nose/case.py", line 198, in runTest 38 | self.test(*self.arg) 39 | File "grappa/tests/should_test.py", line 11, in test_should_api 40 | assert len(x) > 3 41 | AssertionError 42 | 43 | Now, a ``grappa`` error report using ``nosetests``: 44 | 45 | .. code-block:: python 46 | 47 | def test_grappa_assertion(): 48 | x = [1, 2, 3] 49 | x | should.be.have.length.of(4) 50 | 51 | 52 | .. code-block:: bash 53 | 54 | ====================================================================== 55 | FAIL: tests.should_test.test_grappa_assert 56 | ---------------------------------------------------------------------- 57 | Traceback (most recent call last): 58 | File ".pyenv/versions/3.6.0/lib/python3.6/site-packages/nose/case.py", line 198, in runTest 59 | self.test(*self.arg) 60 | File "grappa/tests/should_test.py", line 16, in test_grappa_assert 61 | x | should.be.have.length.of(4) 62 | File "grappa/grappa/test.py", line 248, in __ror__ 63 | return self.__overload__(value) 64 | File "grappa/grappa/test.py", line 236, in __overload__ 65 | return self.__call__(subject, overload=True) 66 | File "grappa/grappa/test.py", line 108, in __call__ 67 | return self._trigger() if overload else Test(subject) 68 | File "grappa/grappa/test.py", line 153, in _trigger 69 | raise err 70 | AssertionError: Oops! Something went wrong! 71 | 72 | The following assertion was not satisfied 73 | subject "[1, 2, 3]" should be have length of "4" 74 | 75 | Reasons 76 | ▸ unexpected object length: 3 77 | 78 | What we expected 79 | an object that can be length measured and its length is equal to 4 80 | 81 | What we got instead 82 | an object of type "list" with length 3 83 | 84 | Information 85 | ▸ An empty object is typically tested via "len(x)" 86 | built-in function. Most built-in types and objects in Python 87 | can be tested that way, such as str, list, generator... 88 | as well as any object that implements "__len__()" method 89 | and returns "0" as length. 90 | — Reference: https://docs.python.org/3/library/functions.html#len 91 | 92 | Where 93 | File "grappa/tests/should_test.py", line 16, in test_grappa_assert 94 | 95 | 8| 96 | 9| def test_native_assert(): 97 | 10| x = [1, 2, 3] 98 | 11| assert len(x) == 4 99 | 12| 100 | 13| 101 | 14| def test_grappa_assert(): 102 | 15| x = [1, 2, 3] 103 | 16| > x | should.be.have.length.of(4) 104 | 17| 105 | 18| 106 | 19| def test_bool(): 107 | 20| True | should.be.true | should.be.present 108 | 21| False | should.be.false | should.be.equal.to(False) 109 | 22| False | should.be.false | should.not_be.equal.to(True) 110 | 111 | Error behavior 112 | -------------- 113 | 114 | ``grappa`` raises an standard ``AssertionError`` exception when an assertion is not satisfied, 115 | with some additional properties that provides context data from ``grappa`` for further debugging. 116 | 117 | Additional error properties: 118 | 119 | - **__grappa__** ``bool`` - Error flag that indicates the error was originated by ``grappa``. 120 | - **__cause__** ``Exception`` - Original exception error, if any. Python >= 3.5 uses this property to enhance traceback. 121 | - **context** ``grappa.Context`` - Current test ``grappa`` context instance. Only for low-level debugging. 122 | 123 | 124 | Custom error messages 125 | --------------------- 126 | 127 | You can include arbitrary custom messages that would be included in the error report providing additional context information. 128 | 129 | .. code-block:: python 130 | 131 | 'foo' | should.be.equal('bar', msg='additional error message') 132 | 133 | expect('foo').to.equal('bar', msg='additional error message') 134 | -------------------------------------------------------------------------------- /grappa/operators/type.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import types 3 | import inspect 4 | from array import array 5 | 6 | from ..operator import Operator 7 | 8 | # Type alias mappings 9 | MAPPINGS = { 10 | 'string': str, 11 | 'int': int, 12 | 'integer': int, 13 | 'number': int, 14 | 'object': object, 15 | 'float': float, 16 | 'bool': bool, 17 | 'boolean': bool, 18 | 'complex': complex, 19 | 'list': list, 20 | 'dict': dict, 21 | 'dictionary': dict, 22 | 'tuple': tuple, 23 | 'set': set, 24 | 'array': array, 25 | 'lambda': types.LambdaType, 26 | 'generator': types.GeneratorType, 27 | 'asyncgenerator': getattr(types, 'GeneratorType', None), 28 | 'class': 'class', 29 | 'method': 'method', 30 | 'module': 'module', 31 | 'function': 'function', 32 | 'coroutine': 'coroutine', 33 | 'generatorfunction': 'generatorfunction', 34 | 'generator function': 'generatorfunction', 35 | 'coroutinefunction': 'coroutinefunction', 36 | } 37 | 38 | 39 | class TypeOperator(Operator): 40 | """ 41 | Asserts if a given object satisfies a type. 42 | 43 | You can use both a type alias string or a ``type`` object. 44 | 45 | Example:: 46 | 47 | # Should style 48 | 1 | should.be.an('int') 49 | 1 | should.be.an('number') 50 | True | should.be.a('bool') 51 | True | should.be.type(bool) 52 | 'foo' | should.be.a(str) 53 | 'foo' | should.be.a('string') 54 | [1, 2, 3] | should.be.a('list') 55 | [1, 2, 3] | should.have.type.of(list) 56 | (1, 2, 3) | should.be.a('tuple') 57 | (1, 2, 3) | should.have.type.of(tuple) 58 | (lamdba x: x) | should.be.a('lambda') 59 | 'foo' | should.be.instance.of('string') 60 | 61 | # Should style - negation form 62 | 1 | should.not_be.an('int') 63 | 1 | should.not_be.an('number') 64 | True | should.not_be.a('bool') 65 | True | should.not_be.type(bool) 66 | 'foo' | should.not_be.a(str) 67 | 'foo' | should.not_be.a('string') 68 | [1, 2, 3] | should.not_be.a('list') 69 | [1, 2, 3] | should.have_not.type.of(list) 70 | (1, 2, 3) | should.not_be.a('tuple') 71 | (1, 2, 3) | should.have_not.type.of(tuple) 72 | (lamdba x: x) | should.not_be.a('lambda') 73 | 74 | # Expect style 75 | 1 | expect.to.be.an('int') 76 | 1 | expect.to.be.an('number') 77 | True | expect.to.be.a('bool') 78 | True | expect.to.be.type(bool) 79 | 'foo' | expect.to.be.a(str) 80 | 'foo' | expect.to.be.a('string') 81 | [1, 2, 3] | expect.to.be.a('list') 82 | [1, 2, 3] | expect.to.have.type.of(list) 83 | (1, 2, 3) | expect.to.be.a('tuple') 84 | (1, 2, 3) | expect.to.have.type.of(tuple) 85 | (lamdba x: x) | expect.to.be.a('lambda') 86 | 'foo' | expect.to.be.instance.of('string') 87 | 88 | # Expect style - negation form 89 | 1 | expect.to_not.be.an('int') 90 | 1 | expect.to_not.be.an('number') 91 | True | expect.to_not.be.a('bool') 92 | True | expect.to_not.be.type(bool) 93 | 'foo' | expect.to_not.be.a(str) 94 | 'foo' | expect.to_not.be.a('string') 95 | [1, 2, 3] | expect.to_not.be.a('list') 96 | [1, 2, 3] | expect.to_not.have.type.of(list) 97 | (1, 2, 3) | expect.to_not.be.a('tuple') 98 | (1, 2, 3) | expect.to_not.have.type.of(tuple) 99 | (lamdba x: x) | expect.to_not.be.a('lambda') 100 | 'foo' | expect.to_not.be.instance.of('string') 101 | """ 102 | 103 | # Is the operator a keyword 104 | kind = Operator.Type.MATCHER 105 | 106 | # Operator keywords 107 | operators = ('type', 'types', 'a', 'an', 'instance') 108 | 109 | # Operator chain aliases 110 | aliases = ('type', 'types', 'of', 'equal', 'to') 111 | 112 | # Subject message template 113 | expected_message = Operator.Dsl.Message( 114 | 'an object that is a "{value}" type', 115 | 'an object that is not a "{value}" type' 116 | ) 117 | 118 | # Subject template message 119 | subject_message = Operator.Dsl.Message( 120 | 'an object of type "{type}" with value "{value}"' 121 | ) 122 | 123 | def match(self, value, expected): 124 | # Custom expectations yielded values 125 | self.value = type(value).__name__ 126 | self.expected = expected 127 | 128 | # Get type alias 129 | if type(expected) is str: 130 | self.expected = expected 131 | _expected = MAPPINGS.get(expected) 132 | 133 | # Overwrite type value string 134 | self.expected = _expected 135 | 136 | if not _expected: 137 | raise ValueError('unsupported type alias: {}'.format(expected)) 138 | 139 | if type(_expected) is str: 140 | return getattr(inspect, 'is{}'.format(expected))(value) 141 | 142 | expected = _expected 143 | 144 | # Check None type 145 | if expected is None: 146 | return value is None 147 | 148 | return isinstance(value, expected) 149 | -------------------------------------------------------------------------------- /tests/operators/contain_test.py: -------------------------------------------------------------------------------- 1 | from array import array 2 | import pytest 3 | 4 | 5 | def test_should_contain(should): 6 | 'hello world' | should.contain('world') | should.contain('hello') 7 | 'hello world' | should.contain('w') | should.contain('o') 8 | 9 | [1, 2, 3] | should.contain(1) | should.contain(3) 10 | 11 | ('foo', 'bar', 123) | should.contain('bar') | should.contain(123) 12 | 13 | {'foo', 'bar', 123} | should.contain('bar') | should.contain(123) 14 | 15 | [{'foo': 1}] | should.contain({'foo': 1}) 16 | 17 | array('i', [1, 2, 3]) | should.contain(1) | should.contain(2) 18 | 19 | {'foo': 'bar', 'fuu': 2} | should.contain('bar') | should.contain(2) 20 | 21 | with pytest.raises(AssertionError): 22 | 'hello world' | should.contain('planet') 23 | 24 | with pytest.raises(AssertionError): 25 | 'hello world' | should.contain('t') 26 | 27 | with pytest.raises(AssertionError): 28 | [1, 2, 3] | should.contain(4) 29 | 30 | with pytest.raises(AssertionError): 31 | ('foo', 'bar', 123) | should.contain('baz') 32 | 33 | with pytest.raises(AssertionError): 34 | {'foo', 'bar', 123} | should.contain('baz') 35 | 36 | with pytest.raises(AssertionError): 37 | [{'foo': 1}] | should.contain({'foo': 2}) 38 | 39 | with pytest.raises(AssertionError): 40 | array('i', [1, 2, 3]) | should.contain(4) 41 | 42 | with pytest.raises(AssertionError): 43 | {'foo': 'bar', 'fuu': 2} | should.contain('baz') 44 | 45 | 46 | def test_should_contain_any(should): 47 | 'hello world' | should.contain('world', 'hello') 48 | 'hello world' | should.contain(('world', 'hello')) 49 | 'hello world' | should.contain(['world', 'hello']) 50 | 'hello world' | should.contain({'world', 'hello'}) 51 | 52 | 'hello world' | should.contain('w', 'o') 53 | 'hello world' | should.contain(('w', 'o')) 54 | 'hello world' | should.contain(['w', 'o']) 55 | 'hello world' | should.contain({'w', 'o'}) 56 | 57 | [1, 2, 3] | should.contain(1, 3) 58 | [1, 2, 3] | should.contain((1, 3)) 59 | [1, 2, 3] | should.contain([1, 3]) 60 | [1, 2, 3] | should.contain({1, 3}) 61 | 62 | ('foo', 'bar', 123) | should.contain('bar', 123) 63 | {'foo', 'bar', 123} | should.contain(('bar', 123)) 64 | {'foo', 'bar', 123} | should.contain({'bar', 123}) 65 | {'foo', 'bar', 123} | should.contain(['bar', 123]) 66 | 67 | [{'foo': 1}, {'bar': 2}] | should.contain({'foo': 1}, {'bar': 2}) 68 | [{'foo': 1}, {'bar': 2}] | should.contain(({'foo': 1}, {'bar': 2})) 69 | [{'foo': 1}, {'bar': 2}] | should.contain([{'foo': 1}, {'bar': 2}]) 70 | 71 | array('i', [1, 2, 3]) | should.contain(1, 2) 72 | array('i', [1, 2, 3]) | should.contain((1, 2)) 73 | array('i', [1, 2, 3]) | should.contain({1, 2}) 74 | array('i', [1, 2, 3]) | should.contain([1, 2]) 75 | 76 | {'foo': 'bar', 'fuu': 'bor'} | should.contain('bar', 'bor') 77 | {'foo': 'bar', 'fuu': 'bor'} | should.contain(('bar', 'bor')) 78 | {'foo': 'bar', 'fuu': 'bor'} | should.contain(['bar', 'bor']) 79 | {'foo': 'bar', 'fuu': 'bor'} | should.contain({'bar', 'bor'}) 80 | 81 | 82 | def test_should_not_contain_any(should): 83 | 'hello planet' | should._not.contain('world', 'hello') 84 | 'hello planet' | should._not.contain(('world', 'hello')) 85 | 'hello planet' | should._not.contain(['world', 'hello']) 86 | 'hello planet' | should._not.contain({'world', 'hello'}) 87 | 88 | 'hello planet' | should._not.contain('w', 'o') 89 | 'hello planet' | should._not.contain(('w', 'o')) 90 | 'hello planet' | should._not.contain(['w', 'o']) 91 | 'hello planet' | should._not.contain({'w', 'o'}) 92 | 93 | [1, 2, 3] | should._not.contain(1, 4) 94 | [1, 2, 3] | should._not.contain((1, 4)) 95 | [1, 2, 3] | should._not.contain([1, 4]) 96 | [1, 2, 3] | should._not.contain({1, 4}) 97 | 98 | ('foo', 'bar', 123) | should._not.contain('baz', 123) 99 | {'foo', 'bar', 123} | should._not.contain(('baz', 123)) 100 | {'foo', 'bar', 123} | should._not.contain({'baz', 123}) 101 | {'foo', 'bar', 123} | should._not.contain(['baz', 123]) 102 | 103 | [{'foo': 1}, {'bar': 2}] | should._not.contain({'foo': 1}, {'baz': 2}) 104 | [{'foo': 1}, {'bar': 2}] | should._not.contain(({'foo': 1}, {'baz': 2})) 105 | [{'foo': 1}, {'bar': 2}] | should._not.contain([{'foo': 1}, {'baz': 2}]) 106 | 107 | array('i', [1, 2, 3]) | should._not.contain(1, 4) 108 | array('i', [1, 2, 3]) | should._not.contain((1, 4)) 109 | array('i', [1, 2, 3]) | should._not.contain({1, 4}) 110 | array('i', [1, 2, 3]) | should._not.contain([1, 4]) 111 | 112 | {'foo': 'bar', 'fuu': 'bor'} | should._not.contain('baz', 'bor') 113 | {'foo': 'bar', 'fuu': 'bor'} | should._not.contain(('baz', 'bor')) 114 | {'foo': 'bar', 'fuu': 'bor'} | should._not.contain(['baz', 'bor']) 115 | {'foo': 'bar', 'fuu': 'bor'} | should._not.contain({'baz', 'bor'}) 116 | 117 | 118 | def test_should_contain_failures(should): 119 | with pytest.raises(AssertionError): 120 | () | should.contain('bar') 121 | 122 | with pytest.raises(AssertionError): 123 | 1 | should.contain('bar') 124 | -------------------------------------------------------------------------------- /docs/integrations.rst: -------------------------------------------------------------------------------- 1 | Integrations 2 | ============ 3 | 4 | ``grappa`` is library/framework agnostic, so you can use it with your preferred 5 | testing framework, such as `nose`, `pytest`, `behave`... 6 | 7 | This page provides some useful information and configuration tips 8 | for a better and more friendly integration between ``grappa`` and 9 | testing framework. 10 | 11 | pytest 12 | ------ 13 | 14 | Friendly error traceback 15 | ^^^^^^^^^^^^^^^^^^^^^^^^ 16 | 17 | ``pytest``, by default, provides a custom error reporting based on the traceback 18 | analysis, pretty much as ``grappa`` does, but unfortunately in a less friendly way 19 | for the developer. 20 | 21 | You can partially or completely disable this in ``pytest`` behavior in order to have 22 | more friendly errors generated by ``grappa``. 23 | 24 | In order to do that you should simply pass the following flag to: 25 | 26 | .. code-block:: bash 27 | 28 | $ pytest --tb=native 29 | 30 | Or alternatively, you can simply partially disable it: 31 | 32 | .. code-block:: bash 33 | 34 | $ pytest --tb=short 35 | 36 | 37 | The difference would be from this: 38 | 39 | .. code-block:: bash 40 | 41 | ________________ test_expect_context_manager ________________ 42 | tests/should_test.py:277: in test_expect_context_manager 43 | should('foo').be.equal('bar') 44 | grappa/assertion.py:16: in __call__ 45 | return self._fn(expected, *args, **kw) 46 | grappa/resolver.py:60: in wrapper 47 | return self.test._trigger() 48 | grappa/test.py:145: in _trigger 49 | raise err 50 | E AssertionError: Oops! Something went wrong! 51 | E 52 | E The following assertion was not satisfied 53 | E subject "foo" should be equal "bar" 54 | E 55 | E What we expected 56 | E a value that is equal to "foo" 57 | E 58 | E What we got instead 59 | E an value of type "str" with data "foo" 60 | E 61 | E Where 62 | E File "tests/should_test.py", line 277, in test_expect_context_manager 63 | E 64 | E 269| with pytest.raises(AssertionError): 65 | E 270| 10 | should.not_be.between.to(5, 10) 66 | E 271| 67 | E 272| with pytest.raises(AssertionError): 68 | E 273| None | should.be.between.numbers(10, 10) 69 | E 274| 70 | E 275| 71 | E 276| def test_expect_context_manager(): 72 | E 277| > should('foo').be.equal('bar') 73 | E 278| 74 | E 279| with should('foo'): 75 | E 280| should.be.equal('foo') 76 | E 281| should.have.length.of(3) 77 | E 282| should.have.type('string') 78 | E 283| 79 | E 284| with should('foo'): 80 | 81 | Into this: 82 | 83 | .. code-block:: bash 84 | 85 | Traceback (most recent call last): 86 | File "tests/should_test.py", line 277, in test_expect_context_manager 87 | should('foo').be.equal('bar') 88 | File "grappa/assertion.py", line 16, in __call__ 89 | return self._fn(expected, *args, **kw) 90 | File "grappa/resolver.py", line 60, in wrapper 91 | return self.test._trigger() 92 | File "grappa/test.py", line 145, in _trigger 93 | raise err 94 | AssertionError: Oops! Something went wrong! 95 | 96 | The following assertion was not satisfied 97 | subject "foo" should be equal "bar" 98 | 99 | What we expected 100 | a value that is equal to "foo" 101 | 102 | What we got instead 103 | an value of type "str" with data "foo" 104 | 105 | Where 106 | File "tests/should_test.py", line 277, in test_expect_context_manager 107 | 108 | 269| with pytest.raises(AssertionError): 109 | 270| 10 | should.not_be.between.to(5, 10) 110 | 271| 111 | 272| with pytest.raises(AssertionError): 112 | 273| None | should.be.between.numbers(10, 10) 113 | 274| 114 | 275| 115 | 276| def test_expect_context_manager(): 116 | 277| > should('foo').be.equal('bar') 117 | 278| 118 | 279| with should('foo'): 119 | 280| should.be.equal('foo') 120 | 281| should.have.length.of(3) 121 | 282| should.have.type('string') 122 | 283| 123 | 284| with should('foo'): 124 | 125 | 126 | Dependency injection using fixtures 127 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 128 | 129 | In order to minimize boilerplate across tests, you can create a ``pytest`` fixture 130 | that would be automatically injected via across test functions/methods in your test suites: 131 | 132 | First, create a ``conftest.py`` file in the directory where your test files lives with the following content: 133 | 134 | .. code-block:: python 135 | 136 | # tests/conftest.py 137 | import pytest 138 | from grappa import should as _should, expect as _expect 139 | 140 | @pytest.fixture 141 | def should(): 142 | return _should 143 | 144 | 145 | @pytest.fixture 146 | def expect(): 147 | return _expect 148 | 149 | Then, from a test file module, you can simply declare a test function that accepts a fixture argument, such as: 150 | 151 | .. code-block:: python 152 | 153 | # tests/sample_test.py 154 | 155 | def test_should_fixture(should): 156 | 'foo' | should.have.length.of(3) | should.be.equal.to('foo') 157 | 158 | 159 | def test_expect_fixture(expect): 160 | 'foo' | expect.to.have.length.of(3) | expect.to.be.equal.to('foo') 161 | -------------------------------------------------------------------------------- /grappa/resolver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import functools 3 | from .empty import empty 4 | from .assertion import AssertionProxy 5 | from .operator import OperatorTypes 6 | 7 | 8 | class OperatorResolver(object): 9 | """ 10 | Resolves and triggers an operator based on its name identifier. 11 | 12 | This class is highly-coupled to `grappa.Test` and consumes `grappa.Engine` 13 | and `grappa.Context` in order to trigger operator resolution logic. 14 | """ 15 | 16 | def __init__(self, test): 17 | self.test = test 18 | self.ctx = test._ctx 19 | self.engine = test._engine 20 | 21 | def run_attribute(self, operator): 22 | operator.run() 23 | 24 | def run_accessor(self, operator): 25 | # Register assertion function 26 | def assertion(subject): 27 | operator.ctx.subject = subject 28 | return operator.run(subject) 29 | 30 | # Add assertion function 31 | self.test._engine.add_assertion(assertion) 32 | 33 | # Self-trigger tests if running as global 34 | if self.ctx.chained or self.ctx.subject is not empty: 35 | self.test._trigger() 36 | 37 | return self.test 38 | 39 | def run_matcher(self, operator): 40 | # Process assert operators 41 | def wrapper(*expected, **kw): 42 | # Register keyword call 43 | self.engine.add_keyword({'call': expected, 'operator': operator}) 44 | 45 | # Retrieve optional custom assertion message 46 | if 'msg' in kw: 47 | # Set user-defined message 48 | self.ctx.message = kw.pop('msg') 49 | 50 | def assertion(subject): 51 | # Register call subjects 52 | operator.ctx.subject = subject 53 | operator.ctx.expected = expected 54 | return operator.run(subject, *expected, **kw) 55 | 56 | # Register assertion function 57 | self.test._engine.add_assertion(assertion) 58 | 59 | # Trigger tests on function call if running as chained call 60 | if self.ctx.chained or self.ctx.subject is not empty: 61 | return self.test._trigger() 62 | 63 | return self.test 64 | 65 | return AssertionProxy(self, operator, wrapper) 66 | 67 | def attribute_error_message(self, name): 68 | def reduce_operators(buf, operator): 69 | columns = 4 70 | name, op = operator 71 | data = buf[op.kind] 72 | 73 | if len(data[-1]) < columns: 74 | data[-1].append(name) 75 | else: 76 | buf[op.kind].append([name]) 77 | 78 | return buf 79 | 80 | def calculate_space(name): 81 | max_space = 20 82 | spaces = max_space - len(name) 83 | return ''.join([' ' for _ in range(spaces if spaces else 0)]) 84 | 85 | def spacer(names): 86 | return ''.join([name + calculate_space(name) for name in names]) 87 | 88 | def join(names): 89 | return '\n '.join([spacer(line) for line in names]) 90 | 91 | # Reduce operators names and select them per type 92 | operators = functools.reduce( 93 | reduce_operators, self.engine.operators.items(), { 94 | OperatorTypes.ATTRIBUTE: [[]], 95 | OperatorTypes.ACCESSOR: [[]], 96 | OperatorTypes.MATCHER: [[]] 97 | }) 98 | 99 | # Compose available operators message by type 100 | values = [' {}S:\n {}'.format(kind.upper(), join(names)) 101 | for kind, names in operators.items()] 102 | 103 | # Compose and return assertion message error 104 | return ('"{}" has no assertion operator called "{}"\n\n' 105 | ' However, you can use one of the following operators:\n\n' 106 | '{}\n').format(self.ctx.style, name, '\n\n'.join(values)) 107 | 108 | def resolve(self, name): 109 | # Check if should stop the call chain 110 | if self.ctx.stop_chain: 111 | raise RuntimeError( 112 | 'grappa: test operator "{}" does not allow ' 113 | 'chained calls.'.format(self.ctx.stop_chain.operator_name)) 114 | 115 | # Find an assertion operator by name 116 | operator = self.engine.find_operator(name) 117 | 118 | # Raise attribute error 119 | if not operator: 120 | raise AttributeError(self.attribute_error_message(name)) 121 | 122 | # Register attribute access 123 | self.engine.add_keyword(name) 124 | 125 | # Create operator instance with current context 126 | operator = operator(context=self.ctx, operator_name=name) 127 | 128 | # Check chainable operator logic is enabled 129 | if getattr(operator, 'chainable', True) is False: 130 | self.ctx.stop_chain = operator 131 | 132 | # Reset context sequence 133 | if self.ctx.reset: 134 | self.engine.reset_keywords() 135 | self.ctx.reset = False 136 | # self.ctx.reverse = True 137 | 138 | # Dynamically retrieve operator 139 | method_name = 'run_{}'.format(operator.kind) 140 | run_operator = getattr(self, method_name, None) 141 | 142 | # If operator kind is not support, raise an exception 143 | if not run_operator: 144 | raise ValueError('operator "{}" has not a valid kind "{}"'.format( 145 | operator.__class__.__name__, 146 | operator.kind 147 | )) 148 | 149 | # Register operator assertion for lazy execution 150 | return run_operator(operator) or self.test 151 | -------------------------------------------------------------------------------- /grappa/operators/start_end.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import collections 3 | from six.moves import collections_abc 4 | 5 | from ..operator import Operator 6 | from ..constants import STR_TYPES 7 | 8 | 9 | class StarEndBaseOpeartor(Operator): 10 | 11 | # Is the operator a keyword 12 | kind = Operator.Type.MATCHER 13 | 14 | # Chain aliases 15 | aliases = ( 16 | 'word', 'string', 'number', 'name', 'numbers', 'items', 17 | 'value', 'char', 'letter', 'by', 'character', 'item', 18 | ) 19 | 20 | def match(self, subject, *expected): 21 | if self.is_unordered_dict(subject): 22 | return False, ['does not have ordered keys'] 23 | 24 | return self.matches(subject, *expected) 25 | 26 | def is_unordered_dict(self, subject): 27 | if isinstance(subject, collections_abc.Mapping): 28 | if not hasattr(collections, 'OrderedDict'): 29 | return True 30 | return not isinstance(subject, collections.OrderedDict) 31 | return False 32 | 33 | 34 | class StartWithOperator(StarEndBaseOpeartor): 35 | """ 36 | Asserts if a given value starts with a specific items. 37 | 38 | Example:: 39 | 40 | # Should style 41 | 'foo' | should.start_with('f') 42 | 'foo' | should.start_with('fo') 43 | [1, 2, 3] | should.start_with.number(1) 44 | iter([1, 2, 3]) | should.start_with.numbers(1, 2) 45 | OrderedDict([('foo', 0), ('bar', 1)]) | should.start_with.item('foo') 46 | 47 | # Should style - negation form 48 | 'foo' | should.do_not.start_with('o') 49 | 'foo' | should.do_not.start_with('o') 50 | [1, 2, 3] | should.do_not.start_with(2) 51 | iter([1, 2, 3]) | should.do_not.start_with.numbers(3, 4) 52 | OrderedDict([('foo', 0), ('bar', 1)]) | should.start_with('bar') 53 | 54 | # Expect style 55 | 'foo' | expect.to.start_with('f') 56 | 'foo' | expect.to.start_with('fo') 57 | [1, 2, 3] | expect.to.start_with.number(1) 58 | iter([1, 2, 3]) | expect.to.start_with.numbers(1, 2) 59 | OrderedDict([('foo', 0), ('bar', 1)]) | expect.to.start_with('foo') 60 | 61 | # Expect style - negation form 62 | 'foo' | expect.to_not.start_with('f') 63 | 'foo' | expect.to_not.start_with('fo') 64 | [1, 2, 3] | expect.to_not.start_with.number(1) 65 | iter([1, 2, 3]) | expect.to_not.start_with.numbers(1, 2) 66 | OrderedDict([('foo', 0), ('bar', 1)]) | expect.to_not.start_with('foo') 67 | """ 68 | 69 | # Operator keywords 70 | operators = ('start_with', 'starts_with', 'startswith') 71 | 72 | # Expected template message 73 | expected_message = Operator.Dsl.Message( 74 | 'an object that starts with items "{value}"', 75 | 'an object that does not start with items "{value}"', 76 | ) 77 | 78 | # Subject template message 79 | subject_message = Operator.Dsl.Message( 80 | 'an object of type "{type}" with value "{value}"', 81 | ) 82 | 83 | def matches(self, subject, *expected): 84 | if isinstance(subject, STR_TYPES): 85 | return ( 86 | subject.startswith(expected[0]), 87 | ['starts with {0!r}'.format(subject[:-len(expected[0])])]) 88 | 89 | head = list(subject)[:len(expected)] 90 | return ( 91 | list(expected) == head, 92 | ['starts with {0!r}'.format(head)]) 93 | 94 | 95 | class EndWithOperator(StarEndBaseOpeartor): 96 | """ 97 | Asserts if a given value ends with a specific items. 98 | 99 | Example:: 100 | 101 | # Should style 102 | 'foo' | should.ends_with('o') 103 | 'foo' | should.ends_with('oo') 104 | [1, 2, 3] | should.ends_with.number(3) 105 | iter([1, 2, 3]) | should.ends_with.numbers(2, 3) 106 | OrderedDict([('foo', 0), ('bar', 1)]) | should.ends_with.item('bar') 107 | 108 | # Should style - negation form 109 | 'foo' | should.do_not.ends_with('f') 110 | 'foo' | should.do_not.ends_with('o') 111 | [1, 2, 3] | should.do_not.ends_with(2) 112 | iter([1, 2, 3]) | should.do_not.ends_with.numbers(3, 4) 113 | OrderedDict([('foo', 0), ('bar', 1)]) | should.ends_with('foo') 114 | 115 | # Expect style 116 | 'foo' | expect.to.ends_with('o') 117 | 'foo' | expect.to.ends_with('oo') 118 | [1, 2, 3] | expect.to.ends_with.number(3) 119 | iter([1, 2, 3]) | expect.to.ends_with.numbers(2, 3) 120 | OrderedDict([('foo', 0), ('bar', 1)]) | expect.to.ends_with('bar') 121 | 122 | # Expect style - negation form 123 | 'foo' | expect.to_not.ends_with('f') 124 | 'foo' | expect.to_not.ends_with('oo') 125 | [1, 2, 3] | expect.to_not.ends_with.number(2) 126 | iter([1, 2, 3]) | expect.to_not.ends_with.numbers(1, 2) 127 | OrderedDict([('foo', 0), ('bar', 1)]) | expect.to_not.ends_with('foo') 128 | """ 129 | 130 | # Operator keywords 131 | operators = ('end_with', 'ends_with', 'endswith') 132 | 133 | # Expected template message 134 | expected_message = Operator.Dsl.Message( 135 | 'an object that ends with items "{value}"', 136 | 'an object that does not end with items "{value}"', 137 | ) 138 | 139 | # Subject template message 140 | subject_message = Operator.Dsl.Message( 141 | 'an object of type "{type}" with value "{value}"', 142 | ) 143 | 144 | def matches(self, subject, *expected): 145 | if isinstance(subject, STR_TYPES): 146 | return ( 147 | subject.endswith(expected[0]), 148 | ['ends with {0!r}'.format(subject[-len(expected[0]):])]) 149 | 150 | tail = list(subject)[-len(expected):] 151 | return ( 152 | list(expected) == tail, 153 | ['ends with {0!r}'.format(tail)]) 154 | -------------------------------------------------------------------------------- /docs/getting-started.rst: -------------------------------------------------------------------------------- 1 | Getting started 2 | =============== 3 | 4 | Installation 5 | ------------ 6 | 7 | Using ``pip`` package manager: 8 | 9 | .. code-block:: bash 10 | 11 | pip install --upgrade grappa 12 | 13 | Or install the latest sources from Github: 14 | 15 | .. code-block:: bash 16 | 17 | pip install -e git+git://github.com/grappa-py/grappa.git#egg=grappa 18 | 19 | 20 | Importing grappa 21 | ---------------- 22 | 23 | For ``should`` assertion style, use: 24 | 25 | .. code-block:: python 26 | 27 | from grappa import should 28 | 29 | For ``expect`` assertion style, use: 30 | 31 | .. code-block:: python 32 | 33 | from grappa import expect 34 | 35 | 36 | Basic assertions 37 | ---------------- 38 | 39 | Equality assertions: 40 | 41 | .. code-block:: python 42 | 43 | 'foo' | should.be.equal.to('foo') 44 | 45 | 'foo' | expect.to.be.equal.to('foo') 46 | 47 | .. code-block:: python 48 | 49 | [1, 2, 3] | should.be.equal.to([1, 2, 3]) 50 | 51 | [1, 2, 3] | expect.to.be.equal.to([1, 2, 3]) 52 | 53 | Value type assertions: 54 | 55 | .. code-block:: python 56 | 57 | 1.54 | should.be.a('float') 58 | 59 | 1.54 | expect.to.be.a('float') 60 | 61 | .. code-block:: python 62 | 63 | 'foo' | should.be.a('string') 64 | 65 | 'foo' | expect.to.be.a('string') 66 | 67 | .. code-block:: python 68 | 69 | [1, 2, 3] | should.be.a('list') 70 | 71 | [1, 2, 3] | expect.to.be.a('list') 72 | 73 | Measure length: 74 | 75 | .. code-block:: python 76 | 77 | iter([1, 2, 3]) | should.have.length.of(3) 78 | 79 | iter([1, 2, 3]) | expect.to.have.length.of(3) 80 | 81 | Custom message errors 82 | --------------------- 83 | 84 | .. code-block:: python 85 | 86 | [1, 2, 3] | should.have.length.of(2, msg='list must have 2 items') 87 | 88 | .. code-block:: python 89 | 90 | 'hello world!' | should.have.contain.word('planet', msg='planet word is mandatory') 91 | 92 | Negation assertions 93 | ------------------- 94 | 95 | .. code-block:: python 96 | 97 | 'foo' | should.not_be.equal.to('bar') 98 | 99 | 'foo' | expect.to_not.be.equal.to('bar') 100 | 101 | .. code-block:: python 102 | 103 | [1, 2, 3] | should.not_be.have.length.of('bar') 104 | 105 | 'foo' | expect.to_not.be.equal.to('bar') 106 | 107 | Context based assertion for DRYer code 108 | -------------------------------------- 109 | 110 | .. code-block:: python 111 | 112 | with should({'foo': 'bar'}): 113 | should.be.a(dict) 114 | should.have.length(1) 115 | should.have.key('foo').that.should.be.equal.to('bar') 116 | 117 | with expect({'foo': 'bar'}): 118 | expect.to.be.a(dict) 119 | expect.to.have.length(1) 120 | expect.to.have.key('foo').to.be.equal('bar') 121 | 122 | Testing exceptions 123 | ------------------ 124 | 125 | .. code-block:: python 126 | 127 | (lambda: x) | should.raises(NameError) 128 | 129 | (lambda: x) | expect.to.raises(NameError) 130 | 131 | .. code-block:: python 132 | 133 | (lambda: x) | should.do_not.raises(RuntimeError) 134 | 135 | (lambda: x) | expect.to_not.raises(RuntimeError) 136 | 137 | Featured assertions 138 | ------------------- 139 | 140 | Dictionary keys assertion 141 | 142 | .. code-block:: python 143 | 144 | {'foo': True} | should.have.key('foo') 145 | 146 | {'foo': True} | expect.to.have.key('foo') 147 | 148 | .. code-block:: python 149 | 150 | class Foo(object): 151 | bar = True 152 | 153 | def baz(self): 154 | pass 155 | 156 | Foo() | should.have.properties('bar', 'baz') 157 | 158 | Foo() | should.have.properties('bar', 'baz') 159 | 160 | 161 | Conditional assertions 162 | ---------------------- 163 | 164 | ``all`` assertion composition, equivalent to ``and`` operator. 165 | 166 | You can define ``N`` number of composed assertions. 167 | 168 | .. code-block:: python 169 | 170 | {'foo': True} | should.all(should.have.key('foo'), should.have.length.of(1)) 171 | 172 | {'foo': True} | expect.all(expect.to.have.key('foo'), expect.to.have.length.of(1)) 173 | 174 | 175 | ``any`` assertion composition, equivalent to ``or`` operator. 176 | You can define ``N`` number of composed assertions. 177 | 178 | .. code-block:: python 179 | 180 | {'foo': True} | should.any(should.have.key('bar'), should.have.length.of(1)) 181 | 182 | {'foo': True} | expect.any(expect.to.have.key('bar'), expect.to.have.length.of(1)) 183 | 184 | 185 | Composing assertions 186 | -------------------- 187 | 188 | Using ``which``/``that`` attribute operators for chained assertions: 189 | 190 | .. code-block:: python 191 | 192 | {'foo': True} | should.have.key('foo').which.should.be.true 193 | 194 | {'foo': True} | expect.to.have.key('foo').that.expect.to.be.true 195 | 196 | Using ``|`` for multiple assertions composition (equivalent to ``all``/``and`` composition): 197 | 198 | .. code-block:: python 199 | 200 | {'foo': True} | should.be.a('dict') | should.have.key('foo') | should.have.length.of(1) 201 | 202 | {'foo': True} | expect.to.be.a('dict') | expect.to.have.key('foo') | expect.to.have.length.of(1) 203 | 204 | 205 | Chained assertions 206 | ------------------ 207 | 208 | Using ``>`` operator for chained assertion instead of ``which``/``that`` operators for assertion composition: 209 | 210 | .. code-block:: python 211 | 212 | {'foo': True} | should.have.key('foo') > should.be.true 213 | 214 | {'foo': True} | expect.to.have.key('foo') > expect.to.be.true 215 | 216 | 217 | More complex chained assertions: 218 | 219 | .. code-block:: python 220 | 221 | (object 222 | | should.have.property('foo') 223 | > should.be.a('tuple') 224 | > should.have.length.of(3) 225 | > should.be.equal.to(('foo', 'bar', 'baz'))) 226 | 227 | .. code-block:: python 228 | 229 | (dictionary 230 | | should.have.key('foo') 231 | > should.be.a('list') 232 | > should.have.length.of(3) 233 | > should.be.equal.to(['foo', 'bar', 'baz'])) 234 | 235 | 236 | How to compose assertions 237 | ------------------------- 238 | 239 | See `operators composition`_ section. 240 | 241 | .. _installation: http://grappa.readthedocs.io/en/latest/intro.html#installation 242 | .. _`operators composition`: http://grappa.readthedocs.io/en/latest/composition.html 243 | -------------------------------------------------------------------------------- /tests/operators/been_called_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | 5 | def test_been_called(expect, mocker): 6 | mock_called = mocker.patch('os.path.join') 7 | os.path.join('home', 'log.txt') 8 | 9 | expect(mock_called).to.have.been_called 10 | 11 | with pytest.raises(AssertionError): 12 | expect(mock_called).to.have_not.been_called 13 | 14 | mock_not_called = mocker.patch('os.rmdir') 15 | 16 | expect(mock_not_called).to.have_not.been_called 17 | 18 | with pytest.raises(AssertionError): 19 | expect(mock_not_called).to.have.been_called 20 | 21 | mock_called_several_times = mocker.patch('os.rename') 22 | os.rename('/home/log.txt', '/home/log_new.txt') 23 | os.rename('/home/log.txt', '/home/log_new.txt') 24 | 25 | expect(mock_called_several_times).to.have.been_called 26 | 27 | with pytest.raises(AssertionError): 28 | expect(mock_called_several_times).to.have_not.been_called 29 | 30 | 31 | def test_been_called_times(expect, mocker): 32 | mock_called = mocker.patch('os.path.join') 33 | os.path.join('home', 'log.txt') 34 | os.path.join('home', 'log.txt') 35 | os.path.join('home', 'log.txt') 36 | 37 | expect(mock_called).to.have.been_called_times(3) 38 | 39 | with pytest.raises(AssertionError): 40 | expect(mock_called).to.have_not.been_called_times(3) 41 | 42 | mock_not_called = mocker.patch('os.rmdir') 43 | 44 | expect(mock_not_called).to.have.been_called_times(0) 45 | expect(mock_not_called).to.have_not.been_called_times(3) 46 | 47 | with pytest.raises(AssertionError): 48 | expect(mock_not_called).to.have.been_called_times(3) 49 | 50 | 51 | def test_been_called_with(expect, mocker): 52 | mock_called = mocker.patch('os.path.join') 53 | os.path.join('home', 'log.txt') 54 | 55 | expect(mock_called).to.have.been_called_with('home', 'log.txt') 56 | 57 | with pytest.raises(AssertionError): 58 | expect(mock_called).to.have_not.been_called_with('home', 'log.txt') 59 | 60 | mock_not_called = mocker.patch('os.rmdir') 61 | 62 | expect(mock_not_called).to.have_not.been_called_with('home', 'log.txt') 63 | 64 | with pytest.raises(AssertionError): 65 | expect(mock_not_called).to.have.been_called_with('home', 'log.txt') 66 | 67 | 68 | def test_been_called_once(expect, mocker): 69 | mock_called = mocker.patch('os.path.join') 70 | os.path.join('home', 'log.txt') 71 | 72 | expect(mock_called).to.have.been_called_once 73 | 74 | with pytest.raises(AssertionError): 75 | expect(mock_called).to.have_not.been_called_once 76 | 77 | mock_not_called = mocker.patch('os.rmdir') 78 | 79 | expect(mock_not_called).to.have_not.been_called_once 80 | 81 | with pytest.raises(AssertionError): 82 | expect(mock_not_called).to.have.been_called_once 83 | 84 | mock_called_several_times = mocker.patch('os.rename') 85 | os.rename('/home/log.txt', '/home/log_new.txt') 86 | os.rename('/home/log.txt', '/home/log_new.txt') 87 | 88 | expect(mock_called_several_times).to.have_not.been_called_once 89 | 90 | with pytest.raises(AssertionError): 91 | expect(mock_called_several_times).to.have.been_called_once 92 | 93 | 94 | def test_been_called_once_with(expect, mocker): 95 | mock_called = mocker.patch('os.path.join') 96 | os.path.join('home', 'log.txt') 97 | 98 | expect(mock_called).to.have.been_called_once_with('home', 'log.txt') 99 | 100 | with pytest.raises(AssertionError): 101 | expect(mock_called).to_not.been_called_once_with('home', 'log.txt') 102 | 103 | mock_not_called = mocker.patch('os.rmdir') 104 | 105 | expect(mock_not_called).to.have_not.been_called_once_with('/home/log.txt') 106 | 107 | with pytest.raises(AssertionError): 108 | expect(mock_not_called).to.have.been_called_once_with('/home/log.txt') 109 | 110 | mock_called_several_times = mocker.patch('os.rename') 111 | os.rename('/home/log.txt', '/home/log_new.txt') 112 | os.rename('/home/log.txt', '/home/log_new.txt') 113 | 114 | expect(mock_called_several_times).to.have_not.been_called_once 115 | 116 | with pytest.raises(AssertionError): 117 | expect(mock_called_several_times).to.have.been_called_once 118 | 119 | 120 | def test_been_called_with_a_spy(expect, mocker): 121 | class Foo(): 122 | def bar(self, string, padding): 123 | return string.zfill(padding) 124 | 125 | foo = Foo() 126 | spy = mocker.spy(foo, 'bar') 127 | 128 | expect(foo.bar('foo', 5)).to.be('00foo') 129 | 130 | expect(spy).to.have.been_called 131 | expect(spy).to.have.been_called_once 132 | expect(spy).to.have.been_called_times(1) 133 | expect(spy).to.have.been_called_with('foo', 5) 134 | expect(spy).to.have.been_called_once_with('foo', 5) 135 | 136 | with pytest.raises(AssertionError): 137 | expect(spy).to.have_not.been_called 138 | 139 | with pytest.raises(AssertionError): 140 | expect(spy).to.have_not.been_called_with('foo', 5) 141 | 142 | with pytest.raises(AssertionError): 143 | expect(spy).to.have_not.been_called_once 144 | 145 | with pytest.raises(AssertionError): 146 | expect(spy).to.have_not.been_called_times(1) 147 | 148 | with pytest.raises(AssertionError): 149 | expect(spy).to.have_not.been_called_once_with('foo', 5) 150 | 151 | 152 | def test_been_called_with_a_stub(expect, mocker): 153 | def foo(bar): 154 | bar('test') 155 | 156 | stub = mocker.stub('bar_stub') 157 | foo(stub) 158 | 159 | expect(stub).to.have.been_called 160 | expect(stub).to.have.been_called_once 161 | expect(stub).to.have.been_called_times(1) 162 | expect(stub).to.have.been_called_with('test') 163 | expect(stub).to.have.been_called_once_with('test') 164 | 165 | with pytest.raises(AssertionError): 166 | expect(stub).to.have_not.been_called 167 | 168 | with pytest.raises(AssertionError): 169 | expect(stub).to.have_not.been_called_with('test') 170 | 171 | with pytest.raises(AssertionError): 172 | expect(stub).to.have_not.been_called_once 173 | 174 | with pytest.raises(AssertionError): 175 | expect(stub).to.have_not.been_called_times(1) 176 | 177 | with pytest.raises(AssertionError): 178 | expect(stub).to.have_not.been_called_once_with('test') 179 | 180 | 181 | def test_been_called_with_an_incompatible_object(expect, mocker): 182 | def foo(): 183 | pass 184 | 185 | foo() 186 | 187 | with pytest.raises(AssertionError): 188 | expect(foo).to.have.been_called 189 | 190 | with pytest.raises(AssertionError): 191 | expect(foo).to.have.been_called_once 192 | 193 | with pytest.raises(AssertionError): 194 | expect(foo).to.have.been_called_times(1) 195 | 196 | with pytest.raises(AssertionError): 197 | expect(foo).to.have.been_called_with('something') 198 | 199 | with pytest.raises(AssertionError): 200 | expect(foo).to.have.been_called_once_with('something') 201 | --------------------------------------------------------------------------------