Repository: kaste/mockito-python

Issue:
Add support for capturing all the arguments when there are multiple invocations
Hi @kaste 

First of all, Thank you for this project. I have been using mockito-python for a while now and really like its features (and its similarity with Java framework). 

I went through the issues in the repo and could not find one that is related to what my ask is. So creating this issue. If this was discussed before, please feel free to close it and link it as duplicate tagging the older issue.

### Issue

Currently the `ArgumentCaptor` available in the `matchers` module allows capturing the method arguments. The limitation of this is that it just captures the last value when there are multiple invocations. For a usecase when a particular method is called multiple times with different values, its good to verify that the invocations were done with the correct arguments

### Solution

There are 2 solutions I think of after going through the repo

1. Add a new class called `MultipleInvocationsArgumentCaptor` that does this job. I also have an implementation ready (along with tests) for the same in this [branch](https://github.com/kaste/mockito-python/compare/master...shashankrnr32:multicaptor?expand=1). I am happy to raise this as a PR. (_Also, feel free to choose a new name for this class if its too long._ 😄 ) 

2. Modify the current `ArgumentCaptor` to take in multiple matchers and change certain aspects with this. This would be a backward incompatible change and I personally go with the 1st option keeping the current code as it is. But I added this option in case you want to maintain close similarity with Java framework, then this would be the preferred option.

As I said, I already have changes ready for the 1st solution I added([See branch](https://github.com/kaste/mockito-python/compare/master...shashankrnr32:multicaptor?expand=1)). I will raise the PR once I get your response on the 1st solution. If you opt to go with the other option, I will be happy to work on it in my free time and raise a PR in a couple of days.

Thank you!


Comments:

Patch:
diff --git a/mockito/matchers.py b/mockito/matchers.py
index 9a44584..36c3815 100644
--- a/mockito/matchers.py
+++ b/mockito/matchers.py
@@ -61,7 +61,6 @@ The one usage you should not care about is a loose signature when using
 
 import re
 
-
 __all__ = [
     'and_', 'or_', 'not_',
     'eq', 'neq',
@@ -77,6 +76,7 @@ __all__ = [
     'kwargs', 'KWARGS'
 ]
 
+
 class _ArgsSentinel(object):
     def __repr__(self):
         return '*args'
@@ -106,6 +106,13 @@ KWARGS = kwargs = {KWARGS_SENTINEL: '_'}
 
 # """
 
+
+class MatcherError(RuntimeError):
+    '''Indicates generic runtime error raised by mockito-python matchers
+    '''
+    pass
+
+
 class Matcher:
     def matches(self, arg):
         pass
@@ -246,18 +253,24 @@ class Matches(Matcher):
 class ArgumentCaptor(Matcher):
     def __init__(self, matcher=None):
         self.matcher = matcher or Any()
-        self.value = None
+        self.all_values = []
 
     def matches(self, arg):
         result = self.matcher.matches(arg)
         if not result:
             return
-        self.value = arg
+        self.all_values.append(arg)
         return True
 
+    @property
+    def value(self):
+        if not self.all_values:
+            raise MatcherError("No argument value was captured!")
+        return self.all_values[-1]
+
     def __repr__(self):
-        return "<ArgumentCaptor: matcher=%s value=%s>" % (
-            repr(self.matcher), self.value,
+        return "<ArgumentCaptor: matcher=%s values=%s>" % (
+            repr(self.matcher), self.all_values,
         )
 
 
diff --git a/mockito/verification.py b/mockito/verification.py
index e7f766e..c36241b 100644
--- a/mockito/verification.py
+++ b/mockito/verification.py
@@ -22,6 +22,7 @@ import operator
 
 __all__ = ['never', 'VerificationError']
 
+
 class VerificationError(AssertionError):
     '''Indicates error during verification of invocations.
 


Test patch:
diff --git a/tests/matchers_test.py b/tests/matchers_test.py
index 0ffcbc3..c7c0445 100644
--- a/tests/matchers_test.py
+++ b/tests/matchers_test.py
@@ -17,7 +17,7 @@
 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 # THE SOFTWARE.
-
+from mockito.matchers import MatcherError
 from .test_base import TestBase
 from mockito import mock, verify
 from mockito.matchers import and_, or_, not_, eq, neq, lt, lte, gt, gte, \
@@ -211,30 +211,38 @@ class ArgumentCaptorTest(TestBase):
     def testShouldSatisfyIfInnerMatcherIsSatisfied(self):
         c = captor(contains("foo"))
         self.assertTrue(c.matches("foobar"))
+        self.assertListEqual(["foobar", ], c.all_values)
 
     def testShouldNotSatisfyIfInnerMatcherIsNotSatisfied(self):
         c = captor(contains("foo"))
         self.assertFalse(c.matches("barbam"))
+        self.assertListEqual([], c.all_values)
 
     def testShouldReturnNoneValueByDefault(self):
         c = captor(contains("foo"))
-        self.assertEqual(None, c.value)
+        self.assertListEqual([], c.all_values)
+        with self.assertRaises(MatcherError):
+            _ = c.value
 
     def testShouldReturnNoneValueIfDidntMatch(self):
         c = captor(contains("foo"))
         c.matches("bar")
-        self.assertEqual(None, c.value)
+        self.assertListEqual([], c.all_values)
+        with self.assertRaises(MatcherError):
+            _ = c.value
 
     def testShouldReturnLastMatchedValue(self):
         c = captor(contains("foo"))
         c.matches("foobar")
         c.matches("foobam")
         c.matches("bambaz")
+        self.assertListEqual(["foobar", "foobam"], c.all_values)
         self.assertEqual("foobam", c.value)
 
     def testShouldDefaultMatcherToAny(self):
         c = captor()
         c.matches("foo")
         c.matches(123)
+        self.assertListEqual(["foo", 123], c.all_values)
         self.assertEqual(123, c.value)