Repository: grappa-py/grappa

Issue:
Checking for error/exception messages
Hello, 

I'm new to grappa. I'm trying to figure out how to test the contents of an Error/Exception.

Typically,

```python
import pytest

def method():
    raise RuntimeError('message')

def test_method():
    with pytest.raises(RuntimeError, match='message'):
        method()
```

How can that be achieved with Grappa? I can't find the missing step in documentation.

Thanks.



Discussion:

Patch:
diff --git a/History.rst b/History.rst
index 25b1517..1011db7 100755
--- a/History.rst
+++ b/History.rst
@@ -2,70 +2,6 @@
 History
 =======
 
-1.0.0 / 2020-11-21
-------------------
-
-  * feat: major version v1
-  * fix: #53, #55, #48, #59 (credits to @sgissinger)
-  * feat: added mock call operators (credits to @sgissinger)
-  * make github actions fail on every flake8
-  * pass flake8
-  * enforce implements operator
-  * missing kwargs argument
-  * add contain tests
-  * normalize code between contain and key and add lists expectations to contain
-  * improve been_called_with reasons when false
-  * use os.path.join instead of remove and Foo class for spy tests
-  * os.path.basename not a good candidate to count mock calls because it seems to call itself
-  * missing experimental for pypy2
-  * re-try to use pypy2 on github actions
-  * skip tests which does not work on pypy2
-  * add contain tests
-  * contain now supports dicts and arrays. Partially fix #59
-  * use isinstance
-  * improve keys operator tests and add array tests
-  * keys operator now supports arrays
-  * set env var in step only
-  * allow errors on 3.10-dev builds
-  * allow insecure commands for github action nightly
-  * upgrade action version
-  * run 3.10-dev on ubuntu only
-  * add python 3.10 nightly to github action pipeline
-  * set 50 chars to normalization size
-  * flake8 correction and docstrings upgrade
-  * add key operator tests
-  * key operator simplified and works with tuples, lists and sets. fixes #48 ?
-  * do not upgrade pip on github actions
-  * try to use pypy only on ubuntu for github actions
-  * use only include for pypy
-  * add python 3.9, prefer jobs over matrix, add pypy env var in travis ci
-  * add mocker stop all
-  * add python 3.9 to github actions
-  * pypy2 does not seem to work in github actions like it does in TravisCI
-  * exclude pypy2 on ubuntu
-  * exclude macos pypy2 from ci matrix
-  * use pypy2 instead
-  * add pypy to github actions
-  * Create python-package.yml
-  * added mock impl validator docstring and error tips
-  * added explicit docstrings to have_been operators
-  * use pytest-mock 2.0.0 for python 2.7
-  * make flake8 pass
-  * set exception __cause__ to get direct causes when internal exceptions occurs
-  * move mock implementation to decorators module
-  * add tests using pytest-mock spies and stubs which are basic funcs
-  * do not rely explicitly on MagicMock but test needed implementation
-  * better get attribute handling
-  * better called once messages
-  * add been_called operator tests
-  * add been_called operators
-  * missing end line
-  * fixes #55 should.not_have.key(...)
-  * once negated a sentence stays negated
-  * make accessor be called
-  * fix(Makefile): lovely tabs
-  * fix(Makefile): use twine for publishing
-
 0.1.12 / 2020-02-26
 --------------------
 
diff --git a/grappa/__init__.py b/grappa/__init__.py
index ddb6ea9..14834bb 100755
--- a/grappa/__init__.py
+++ b/grappa/__init__.py
@@ -40,4 +40,4 @@ __author__ = 'Tomas Aparicio'
 __license__ = 'MIT'
 
 # Current package version
-__version__ = '1.0.0'
+__version__ = '0.1.12'
diff --git a/grappa/operators/raises.py b/grappa/operators/raises.py
index 763f2a5..a60f91d 100644
--- a/grappa/operators/raises.py
+++ b/grappa/operators/raises.py
@@ -55,6 +55,14 @@ class RaisesOperator(Operator):
         'an object of type "{type}" with reference "{value}"',
     )
 
+    def after_success(self, obj, *keys):
+        message = getattr(self.value, 'message', None)
+
+        if not message:
+            message = ' '.join([str(item) for item in self.value.args])
+
+        self.ctx.subject = message
+
     def match(self, fn, *errors):
         if not callable(fn):
             return False, ['subject must be a function or method']
@@ -64,11 +72,6 @@ class RaisesOperator(Operator):
         except Exception as err:
             self.value = err
 
-            # If no errors present, just raise the exception
-            if not errors:
-                return True, []
-
-            # Otherwise match errors
             return isinstance(err, *errors), ['invalid raised exception']
         else:
             return False, ['did not raise any exception']


Test patch:
diff --git a/tests/operators/raises_test.py b/tests/operators/raises_test.py
index a3acdfa..cbdf978 100644
--- a/tests/operators/raises_test.py
+++ b/tests/operators/raises_test.py
@@ -3,12 +3,16 @@ from functools import partial
 
 
 def test_raises(should):
+    def no_error():
+        pass
+
     def error():
         raise AssertionError('foo')
 
     def error_with_params(foo_param):
         raise AssertionError(foo_param)
 
+    error | should.raise_error(Exception)
     error | should.raise_error(AssertionError)
     error | should.do_not.raise_error(NotImplementedError)
 
@@ -24,3 +28,47 @@ def test_raises(should):
 
     with pytest.raises(AssertionError):
         None | should.raise_error(AssertionError)
+
+    with pytest.raises(AssertionError):
+        no_error | should.raise_error(AssertionError)
+
+
+def test_raises_with_message_redirection(should):
+    def error():
+        raise AssertionError('foo')
+
+    def env_error():
+        raise EnvironmentError(3501, 'bar')
+
+    error | should.raise_error(AssertionError) > should.equal('foo')
+
+    error | should.raise_error(AssertionError) > should.contain('fo')
+
+    error | should.do_not.raise_error(NotImplementedError) \
+        > should.equal('foo')
+
+    env_error | should.raise_error(EnvironmentError) > should.contain('bar')
+
+    env_error | should.raise_error(EnvironmentError) > should.equal('3501 bar')
+
+    with pytest.raises(AssertionError):
+        error | should.raise_error(AssertionError) > should.equal('fooe')
+
+    with pytest.raises(AssertionError):
+        error | should.raise_error(NotImplementedError) > should.equal('foo')
+
+
+def test_raises_custom_exception_message_redirection(should):
+    class CustomException(Exception):
+        message = 'foo'
+
+        def __init__(self, *args):
+            super(CustomException, self).__init__(self.message, *args)
+
+    def custom_error():
+        raise CustomException('bar')
+
+    custom_error | should.raise_error(CustomException) > should.equal('foo')
+
+    custom_error | should.raise_error(CustomException) \
+        > should.do_not.equal('foo bar')