diff --git a/README.rst b/README.rst
index ba7921e..575245b 100644
--- a/README.rst
+++ b/README.rst
@@ -113,10 +113,12 @@ but now has contributions from the following people:
 
 .. _`Christopher Armstrong`: https://github.com/radix
 
+- `cyli`_
 - `lvh`_
 - `Manish Tomar`_
 - `Tom Prince`_
 
+.. _`cyli`: https://github.com/cyli
 .. _`lvh`: https://github.com/lvh
 .. _`Manish Tomar`: https://github.com/manishtomar
 .. _`Tom Prince`: https://github.com/tomprince
diff --git a/effect/test_testing.py b/effect/test_testing.py
index a7a878a..d6c01c1 100644
--- a/effect/test_testing.py
+++ b/effect/test_testing.py
@@ -18,6 +18,7 @@
     ESFunc,
     EQDispatcher,
     EQFDispatcher,
+    SequenceDispatcher,
     fail_effect,
     resolve_effect,
     resolve_stubs)
@@ -276,3 +277,41 @@ def test_perform(self):
         """When an intent matches, performing it returns the canned result."""
         d = EQFDispatcher([('hello', lambda i: (i, 'there'))])
         self.assertEqual(sync_perform(d, Effect('hello')), ('hello', 'there'))
+
+
+class SequenceDispatcherTests(TestCase):
+    """Tests for :obj:`SequenceDispatcher`."""
+
+    def test_mismatch(self):
+        """
+        When an intent isn't expected, a None is returned.
+        """
+        d = SequenceDispatcher([('foo', lambda i: 1 / 0)])
+        self.assertEqual(d('hello'), None)
+
+    def test_success(self):
+        """
+        Each intent is performed in sequence with the provided functions, as
+        long as the intents match.
+        """
+        d = SequenceDispatcher([
+            ('foo', lambda i: ('performfoo', i)),
+            ('bar', lambda i: ('performbar', i)),
+        ])
+        eff = Effect('foo').on(lambda r: Effect('bar').on(lambda r2: (r, r2)))
+        self.assertEqual(
+            sync_perform(d, eff),
+            (('performfoo', 'foo'), ('performbar', 'bar')))
+
+    def test_ran_out(self):
+        """When there are no more items left, None is returned."""
+        d = SequenceDispatcher([])
+        self.assertEqual(d('foo'), None)
+
+    def test_out_of_order(self):
+        """Order of items in the sequence matters."""
+        d = SequenceDispatcher([
+            ('bar', lambda i: ('performbar', i)),
+            ('foo', lambda i: ('performfoo', i)),
+        ])
+        self.assertEqual(d('foo'), None)
diff --git a/effect/testing.py b/effect/testing.py
index f4ab5c6..5cb08d1 100644
--- a/effect/testing.py
+++ b/effect/testing.py
@@ -243,3 +243,34 @@ def __call__(self, intent):
         for k, v in self.mapping:
             if k == intent:
                 return sync_performer(lambda d, i: v(i))
+
+
+class SequenceDispatcher(object):
+    """
+    A dispatcher which steps through a sequence of (intent, func) tuples and
+    runs ``func`` to perform intents in strict sequence.
+
+    So, if you expect to first perform an intent like ``MyIntent('a')`` and
+    then an intent like ``OtherIntent('b')``, you can create a dispatcher like
+    this::
+
+        SequenceDispatcher([
+            (MyIntent('a'), lambda i: 'my-intent-result'),
+            (OtherIntent('b'), lambda i: 'other-intent-result')
+        ])
+
+    :obj:`None` is returned if the next intent in the sequence is not equal to
+    the intent being performed, or if there are no more items left in the
+    sequence.
+    """
+    def __init__(self, sequence):
+        """:param list sequence: Sequence of (intent, fn)."""
+        self.sequence = sequence
+
+    def __call__(self, intent):
+        if len(self.sequence) == 0:
+            return
+        exp_intent, func = self.sequence[0]
+        if intent == exp_intent:
+            self.sequence = self.sequence[1:]
+            return sync_performer(lambda d, i: func(i))