Skip to content

Commit

Permalink
Merge pull request #64 from sgissinger/master
Browse files Browse the repository at this point in the history
Fixes #53, #55, #48, #59 (partially) and added mock call operators
  • Loading branch information
h2non committed Nov 21, 2020
2 parents e62e9ee + 6e4e54f commit fbaf340
Show file tree
Hide file tree
Showing 19 changed files with 878 additions and 67 deletions.
67 changes: 67 additions & 0 deletions .github/workflows/python-package.yml
@@ -0,0 +1,67 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: Python CI

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, pypy3]
experimental: [false]
include:
- os: ubuntu-latest
python-version: pypy2
experimental: false
- os: ubuntu-latest
python-version: 3.10-dev
experimental: true

runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
name: Python ${{ matrix.python-version }} on ${{ matrix.os }}

steps:
- uses: actions/checkout@v2

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
if: "!endsWith(matrix.python-version, '-dev')"
with:
python-version: ${{ matrix.python-version }}

- name: Set up Python ${{ matrix.python-version }}
uses: deadsnakes/action@v2.0.0
if: endsWith(matrix.python-version, '-dev')
with:
python-version: ${{ matrix.python-version }}
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true

- name: Install dependencies
run: |
pip install -r requirements-dev.txt
pip install -r requirements.txt
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --max-complexity=10 --statistics
- name: Test with pytest
run: |
pytest -s -v --tb=native --capture=sys --cov grappa --cov-report term-missing tests
- name: Coverage
run: |
coverage run --source grappa -m py.test
coverage report
7 changes: 5 additions & 2 deletions .travis.yml
Expand Up @@ -6,13 +6,16 @@ python:
- "3.6"
- "3.7"
- "3.8"
- pypy
- "3.9"
- pypy3
- nightly

matrix:
jobs:
allow_failures:
- python: nightly
include:
- python: pypy
env: CRYPTOGRAPHY_ALLOW_OPENSSL_102=true

sudo: false

Expand Down
74 changes: 74 additions & 0 deletions grappa/decorators.py
Expand Up @@ -2,6 +2,7 @@
import inspect
import functools
import six
from .api import TestProxy
from .engine import Engine
from .operator import Operator

Expand Down Expand Up @@ -73,3 +74,76 @@ def register(operator):
"""
Engine.register(operator)
return operator


def mock_implementation_validator(func):
"""
Validate that a mock conform to the implementation required to run
have_been and have_been_with operators.
Otherwise returns a Grappa tuple with missing implementation reasons.
Arguments:
operator (Operator): a been_called or been_called_with operator.
subject: a mock whose implementation is to be validated.
*args: variadic arguments.
**kw: variadic keyword arguments.
Returns:
(function|tuple)
"""
@functools.wraps(func)
def wrapper(operator, subject, *args, **kwargs):
expect = TestProxy('expect')

propery_reason_template = 'a property named "{}" is expected'

def validate_props(reasons, prop):
try:
expect(subject).to.have.property(prop)
except AssertionError:
reasons.append(propery_reason_template.format(prop))
return reasons

method_reason_template = 'a method named "{}" is expected'

def validate_methods(reasons, method):
try:
expect(subject).to.implement.methods(method)
except AssertionError:
reasons.append(method_reason_template.format(method))
return reasons

expected_props = ('called', 'call_count')
reasons = functools.reduce(validate_props, expected_props, [])

expected_methods = ('assert_called_with', 'assert_called_once_with')
reasons = functools.reduce(validate_methods, expected_methods, reasons)

if reasons:
operator.information = (
Operator.Dsl.Help(
Operator.Dsl.Description(
'Required implementation is based on unittest.mock.Mock class.', # noqa E501
'',
'Properties required',
' {}'.format(', '.join(expected_props)),
'Methods required',
' {}'.format(', '.join(expected_methods)),
'',
),
Operator.Dsl.Reference(
'https://docs.python.org/3/library/unittest.mock.html#the-mock-class', # noqa E501
),
Operator.Dsl.Reference(
'https://pypi.org/project/pytest-mock/',
),
),
)

reasons.insert(0, 'mock implementation is incomplete')
return False, reasons

return func(operator, subject, *args, **kwargs)

return wrapper
6 changes: 6 additions & 0 deletions grappa/operators/__init__.py
Expand Up @@ -27,6 +27,12 @@
('implements', 'ImplementsOperator'),
('raises', 'RaisesOperator'),

('been_called', 'BeenCalledOperator',
'BeenCalledTimesOperator',
'BeenCalledOnceOperator'),
('been_called_with', 'BeenCalledWithOperator',
'BeenCalledOnceWithOperator'),

('bool', 'TrueOperator', 'FalseOperator'),
('start_end', 'StartWithOperator', 'EndWithOperator'),

Expand Down
2 changes: 1 addition & 1 deletion grappa/operators/attributes.py
Expand Up @@ -13,7 +13,7 @@ def be(ctx):
Semantic attributes providing chainable declarative DSL
for assertions.
"""
ctx.negate = False
pass


@attribute(operators=(
Expand Down
152 changes: 152 additions & 0 deletions grappa/operators/been_called.py
@@ -0,0 +1,152 @@
# -*- coding: utf-8 -*-
from ..decorators import mock_implementation_validator
from ..operator import Operator


class BeenCalledOperator(Operator):
"""
Asserts if a given mock subject have been called at least once.
Warning::
Piping style assertions is not yet supported.
Example::
# Should style
should(mock).have.been_called
# Should style - negation form
should(mock).have_not.been_called
# Expect style
expect(mock).to.have.been_called
# Expect style - negation form
expect(mock).to.have_not.been_called
expect(mock).to_not.have.been_called
"""

# Is the operator a keyword
kind = Operator.Type.ACCESSOR

# Disable diff report
show_diff = False

# Operator keywords
operators = ('been_called',)

# Error message templates
expected_message = Operator.Dsl.Message(
'a mock that has been called at least once',
'a mock that has not been called',
)

# Subject message template
subject_message = Operator.Dsl.Message(
'a mock that has not been called',
'a mock that has been called at least once',
)

@mock_implementation_validator
def match(self, subject):
return subject.called


class BeenCalledOnceOperator(Operator):
"""
Asserts if a given mock subject have been called once.
Warning::
Piping style assertions is not yet supported.
Example::
# Should style
should(mock).have.been_called_once
# Should style - negation form
should(mock).have_not.been_called_once
# Expect style
expect(mock).to.have.been_called_once
# Expect style - negation form
expect(mock).to.have_not.been_called_once
expect(mock).to_not.have.been_called_once
"""

# Is the operator a keyword
kind = Operator.Type.ACCESSOR

# Disable diff report
show_diff = False

# Operator keywords
operators = ('been_called_once',)

# Error message templates
expected_message = Operator.Dsl.Message(
'a mock that has been called once',
'a mock that has not been called once',
)

# Subject message template
subject_message = Operator.Dsl.Message(
'a mock that has been called {call_count} time(s)',
'a mock that has been called once',
)

@mock_implementation_validator
def match(self, subject):
return subject.call_count == 1


class BeenCalledTimesOperator(Operator):
"""
Asserts if a given mock subject have been called n times.
Warning::
Piping style assertions is not yet supported.
Example::
# Should style
should(mock).have.been_called_times(3)
# Should style - negation form
should(mock).have_not.been_called_times(3)
# Expect style
expect(mock).to.have.been_called_times(0)
# Expect style - negation form
expect(mock).to.have_not.been_called_times(1)
expect(mock).to_not.have.been_called_times(3)
"""

# Is the operator a keyword
kind = Operator.Type.MATCHER

# Disable diff report
show_diff = True

# Operator keywords
operators = ('been_called_times',)

# Error message templates
expected_message = Operator.Dsl.Message(
'a mock that has been called {value} times',
'a mock that has not been called {value} times',
)

subject_message = Operator.Dsl.Message(
'a mock that has been called {call_count} times',
'a mock that has not been called {call_count} times',
)

@mock_implementation_validator
def match(self, subject, expected):
return subject.call_count == expected

0 comments on commit fbaf340

Please sign in to comment.