diff --git a/eventful-test-helpers.cabal b/eventful-test-helpers.cabal
--- a/eventful-test-helpers.cabal
+++ b/eventful-test-helpers.cabal
@@ -3,7 +3,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           eventful-test-helpers
-version:        0.1.2
+version:        0.1.3
 synopsis:       Common module used for eventful tests
 description:    Common module used for eventful tests
 category:       Database,Eventsourcing
@@ -32,6 +32,7 @@
     , extra
     , hspec
     , monad-logger
+    , text
   exposed-modules:
       Eventful.TestHelpers
   default-language: Haskell2010
diff --git a/src/Eventful/TestHelpers.hs b/src/Eventful/TestHelpers.hs
--- a/src/Eventful/TestHelpers.hs
+++ b/src/Eventful/TestHelpers.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE RankNTypes #-}
 {-# LANGUAGE TemplateHaskell #-}
 
@@ -12,8 +13,15 @@
   , counterAggregate
   , CounterEvent (..)
   , CounterCommand (..)
+  , EventStoreRunner (..)
+  , GloballyOrderedEventStoreRunner (..)
   , eventStoreSpec
   , sequencedEventStoreSpec
+  , StreamProjectionCacheRunner (..)
+  , streamProjectionCacheSpec
+  , GloballyOrderedProjectionCacheRunner (..)
+  , globallyOrderedProjectionCacheSpec
+  , Text
   , module X
   ) where
 
@@ -24,6 +32,7 @@
 import Data.Aeson
 import Data.Aeson.Casing
 import Data.Aeson.TH
+import Data.Text (Text)
 import Test.Hspec
 
 import Eventful
@@ -47,6 +56,12 @@
   (Counter 0)
   (\(Counter k) (Added x) -> Counter (k + x))
 
+counterGlobalProjection :: Projection Counter (GloballyOrderedEvent CounterEvent)
+counterGlobalProjection =
+  Projection
+  (Counter 0)
+  (\(Counter k) (GloballyOrderedEvent _ _ _ (Added x)) -> Counter (k + x))
+
 data CounterCommand
   = Increment
     { _counterCommandAmount :: Int
@@ -76,58 +91,78 @@
 
 -- Test harness for stores
 
+newtype EventStoreRunner m =
+  EventStoreRunner (forall a. (EventStore CounterEvent m -> m a) -> IO a)
+newtype GloballyOrderedEventStoreRunner m =
+  GloballyOrderedEventStoreRunner (forall a. (EventStore CounterEvent m -> GloballyOrderedEventStore CounterEvent m -> m a) -> IO a)
+
 eventStoreSpec
   :: (Monad m)
-  => IO (EventStore CounterEvent m, runargs)
-  -> (forall a. runargs -> m a -> IO a)
+  => EventStoreRunner m
   -> Spec
-eventStoreSpec makeStore runAsIO = do
+eventStoreSpec (EventStoreRunner withStore) = do
+  let
+    withStoreExampleEvents action = withStore $ \store -> do
+      _ <- insertExampleEvents store
+      action store
+
   context "when the event store is empty" $ do
 
     it "should return versions of -1 for a UUID" $ do
-      -- TODO: Abstract out creating all of these stores and having to manually
-      -- thread around makeStore, runAsIO, etc.
-      (store, runargs) <- liftIO makeStore
-      runAsIO runargs (getLatestVersion store nil) `shouldReturn` (-1)
+      withStore (\store -> getLatestVersion store nil) `shouldReturn` (-1)
 
   context "when a few events are inserted" $ do
     let
-      events = [Added 1, Added 4, Added (-3)]
+      sampleEvents = [Added 1, Added 4, Added (-3), Added 5]
+      withStore' action = withStore $ \store -> do
+        _ <- storeEvents store NoStream nil sampleEvents
+        action store
 
     it "should return events" $ do
-      (store, runargs) <- makeStore
-      events' <- runAsIO runargs $ do
-        _ <- storeEvents store NoStream nil events
-        getEvents store nil Nothing
-      (storedEventEvent <$> events') `shouldBe` events
-      --(storedEventSequenceNumber <$> events') `shouldBe` [1, 2, 3]
+      events' <- withStore' $ \store -> getEvents store nil allEvents
+      (storedEventEvent <$> events') `shouldBe` sampleEvents
 
     it "should return correct event versions" $ do
-      (store, runargs) <- makeStore
-      (latestVersion, allEvents, someEvents) <- runAsIO runargs $ do
-        _ <- storeEvents store NoStream nil events
-        (,,) <$>
+      (latestVersion, events) <- withStore' $ \store ->
+        (,) <$>
           getLatestVersion store nil <*>
-          getEvents store nil (Just (-1)) <*>
-          getEvents store nil (Just 1)
-      latestVersion `shouldBe` 2
-      (storedEventEvent <$> allEvents) `shouldBe` events
-      (storedEventEvent <$> someEvents) `shouldBe` drop 1 events
+          getEvents store nil allEvents
+      latestVersion `shouldBe` 3
+      (storedEventVersion <$> events) `shouldBe` [0, 1, 2, 3]
 
+    it "should return correct events with queries" $ do
+      (firstEvents, middleEvents, laterEvents, maxEvents) <- withStore' $ \store ->
+        (,,,) <$>
+          getEvents store nil (eventsUntil 1) <*>
+          getEvents store nil (eventsStartingAtUntil 1 2) <*>
+          getEvents store nil (eventsStartingAt 2) <*>
+          getEvents store nil (eventsStartingAtTakeLimit 0 2)
+      (storedEventEvent <$> firstEvents) `shouldBe` take 2 sampleEvents
+      (storedEventEvent <$> middleEvents) `shouldBe` take 2 (drop 1 sampleEvents)
+      (storedEventEvent <$> laterEvents) `shouldBe` drop 2 sampleEvents
+      (storedEventEvent <$> maxEvents) `shouldBe` take 2 sampleEvents
+
     it "should return the latest projection" $ do
-      (store, runargs) <- makeStore
-      projection <- runAsIO runargs $ do
-        _ <- storeEvents store NoStream nil events
-        getLatestProjection store counterProjection nil
-      projection `shouldBe` (Counter 2, 2)
+      projection <- withStore' $ \store ->
+        getLatestProjection store (streamProjection counterProjection nil)
+      streamProjectionState projection `shouldBe` Counter 7
+      streamProjectionVersion projection `shouldBe` 3
+      streamProjectionUuid projection `shouldBe` nil
 
+    it "should return the latest projection with some starting StreamProjection" $ do
+      projection <- withStore' $ \store -> do
+        initialEvents <- getEvents store nil (eventsUntil 1)
+        let initialProjection = latestProjection counterProjection (storedEventEvent <$> initialEvents)
+        getLatestProjection store (StreamProjection counterProjection nil 1 initialProjection)
+      streamProjectionState projection `shouldBe` Counter 7
+      streamProjectionVersion projection `shouldBe` 3
+      streamProjectionUuid projection `shouldBe` nil
+
   context "when events from multiple UUIDs are inserted" $ do
 
     it "should have the correct events for each aggregate" $ do
-      (store, runargs) <- makeStore
-      (events1, events2) <- runAsIO runargs $ do
-        _ <- insertExampleEvents store
-        (,) <$> getEvents store uuid1 Nothing <*> getEvents store uuid2 Nothing
+      (events1, events2) <- withStoreExampleEvents $ \store ->
+        (,) <$> getEvents store uuid1 allEvents <*> getEvents store uuid2 allEvents
       (storedEventEvent <$> events1) `shouldBe` Added <$> [1, 4]
       (storedEventEvent <$> events2) `shouldBe` Added <$> [2, 3, 5]
       (storedEventProjectionId <$> events1) `shouldBe` [uuid1, uuid1]
@@ -136,34 +171,41 @@
       (storedEventVersion <$> events2) `shouldBe` [0, 1, 2]
 
     it "should return correct event versions" $ do
-      (store, runargs) <- makeStore
-      (latestVersion1, latestVersion2, events1, events2) <- runAsIO runargs $ do
-        _ <- insertExampleEvents store
+      (latestVersion1, latestVersion2, events1, events2) <- withStoreExampleEvents $ \store ->
         (,,,) <$>
           getLatestVersion store uuid1 <*>
           getLatestVersion store uuid2 <*>
-          getEvents store uuid1 (Just 0) <*>
-          getEvents store uuid2 (Just 1)
+          getEvents store uuid1 allEvents <*>
+          getEvents store uuid2 allEvents
       latestVersion1 `shouldBe` 1
       latestVersion2 `shouldBe` 2
       storedEventEvent <$> events1 `shouldBe` [Added 1, Added 4]
-      storedEventEvent <$> events2 `shouldBe` [Added 3, Added 5]
+      storedEventEvent <$> events2 `shouldBe` [Added 2, Added 3, Added 5]
 
+    it "should return correct events with queries" $ do
+      (firstEvents, middleEvents, laterEvents, maxEvents) <- withStoreExampleEvents $ \store ->
+        (,,,) <$>
+          getEvents store uuid1 (eventsUntil 1) <*>
+          getEvents store uuid2 (eventsStartingAtUntil 1 2) <*>
+          getEvents store uuid2 (eventsStartingAt 2) <*>
+          getEvents store uuid1 (eventsStartingAtTakeLimit 1 1)
+      (storedEventEvent <$> firstEvents) `shouldBe` [Added 1, Added 4]
+      (storedEventEvent <$> middleEvents) `shouldBe` [Added 3, Added 5]
+      (storedEventEvent <$> laterEvents) `shouldBe` [Added 5]
+      (storedEventEvent <$> maxEvents) `shouldBe` [Added 4]
+
     it "should produce the correct projections" $ do
-      (store, runargs) <- makeStore
-      (proj1, proj2) <- runAsIO runargs $ do
-        _ <- insertExampleEvents store
+      (proj1, proj2) <- withStoreExampleEvents $ \store ->
         (,) <$>
-          getLatestProjection store counterProjection uuid1 <*>
-          getLatestProjection store counterProjection uuid2
-      proj1 `shouldBe` (Counter 5, 1)
-      proj2 `shouldBe` (Counter 10, 2)
+          getLatestProjection store (streamProjection counterProjection uuid1) <*>
+          getLatestProjection store (streamProjection counterProjection uuid2)
+      (streamProjectionState proj1, streamProjectionVersion proj1) `shouldBe` (Counter 5, 1)
+      (streamProjectionState proj2, streamProjectionVersion proj2) `shouldBe` (Counter 10, 2)
 
   describe "can handle event storage errors" $ do
 
     it "rejects some writes when event store isn't created" $ do
-      (store, runargs) <- makeStore
-      (err1, err2) <- runAsIO runargs $
+      (err1, err2) <- withStore $ \store -> do
         (,) <$>
           storeEvents store StreamExists nil [Added 1] <*>
           storeEvents store (ExactVersion 0) nil [Added 1]
@@ -171,12 +213,10 @@
       err2 `shouldBe` Just (EventStreamNotAtExpectedVersion (-1))
 
     it "should be able to store events starting with an empty stream" $ do
-      (store, runargs) <- makeStore
-      runAsIO runargs (storeEvents store NoStream nil [Added 1]) `shouldReturn` Nothing
+      withStore (\store -> storeEvents store NoStream nil [Added 1]) `shouldReturn` Nothing
 
     it "should reject storing events sometimes with a stream" $ do
-      (store, runargs) <- makeStore
-      (err1, err2, err3) <- runAsIO runargs $
+      (err1, err2, err3) <- withStore $ \store ->
         (,,) <$>
           storeEvents store NoStream nil [Added 1] <*>
           storeEvents store NoStream nil [Added 1] <*>
@@ -186,8 +226,7 @@
       err3 `shouldBe` Just (EventStreamNotAtExpectedVersion 0)
 
     it "should accepts storing events sometimes with a stream" $ do
-      (store, runargs) <- makeStore
-      errors <- runAsIO runargs $
+      errors <- withStore $ \store ->
         sequence
           [ storeEvents store NoStream nil [Added 1]
           , storeEvents store AnyVersion nil [Added 1]
@@ -198,28 +237,40 @@
 
 sequencedEventStoreSpec
   :: (Monad m)
-  => IO (EventStore CounterEvent m, GloballyOrderedEventStore CounterEvent m, runargs)
-  -> (forall a. runargs -> m a -> IO a)
+  => GloballyOrderedEventStoreRunner m
   -> Spec
-sequencedEventStoreSpec makeStore runAsIO = do
+sequencedEventStoreSpec (GloballyOrderedEventStoreRunner withStore) = do
   context "when the event store is empty" $ do
 
     it "shouldn't have any events" $ do
-      (_, globalStore, runargs) <- makeStore
-      length <$> runAsIO runargs (getSequencedEvents globalStore 0) `shouldReturn` 0
+      events <- withStore (\_ globalStore -> getSequencedEvents globalStore allEvents)
+      length events `shouldBe` 0
 
   context "when events from multiple UUIDs are inserted" $ do
 
     it "should have the correct events in global order" $ do
-      (store, globalStore, runargs) <- makeStore
-      events' <- runAsIO runargs $ do
+      events <- withStore $ \store globalStore -> do
         insertExampleEvents store
-        getSequencedEvents globalStore 0
-      (globallyOrderedEventEvent <$> events') `shouldBe` Added <$> [1..5]
-      (globallyOrderedEventProjectionId <$> events') `shouldBe` [uuid1, uuid2, uuid2, uuid1, uuid2]
-      (globallyOrderedEventVersion <$> events') `shouldBe` [0, 0, 1, 1, 2]
-      (globallyOrderedEventSequenceNumber <$> events') `shouldBe` [1..5]
+        getSequencedEvents globalStore allEvents
+      (globallyOrderedEventEvent <$> events) `shouldBe` Added <$> [1..5]
+      (globallyOrderedEventProjectionId <$> events) `shouldBe` [uuid1, uuid2, uuid2, uuid1, uuid2]
+      (globallyOrderedEventVersion <$> events) `shouldBe` [0, 0, 1, 1, 2]
+      (globallyOrderedEventSequenceNumber <$> events) `shouldBe` [1..5]
 
+    it "should handle queries" $ do
+      (firstEvents, middleEvents, laterEvents, maxEvents) <- withStore $ \store globalStore -> do
+        insertExampleEvents store
+        (,,,) <$>
+          getSequencedEvents globalStore (eventsUntil 2) <*>
+          getSequencedEvents globalStore (eventsStartingAtUntil 2 3) <*>
+          getSequencedEvents globalStore (eventsStartingAt 3) <*>
+          getSequencedEvents globalStore (eventsStartingAtTakeLimit 2 3)
+
+      (globallyOrderedEventEvent <$> firstEvents) `shouldBe` Added <$> [1..2]
+      (globallyOrderedEventEvent <$> middleEvents) `shouldBe` Added <$> [2..3]
+      (globallyOrderedEventEvent <$> laterEvents) `shouldBe` Added <$> [3..5]
+      (globallyOrderedEventEvent <$> maxEvents) `shouldBe` Added <$> [2..4]
+
 insertExampleEvents
   :: (Monad m)
   => EventStore CounterEvent m
@@ -235,3 +286,85 @@
 
 uuid2 :: UUID
 uuid2 = uuidFromInteger 2
+
+newtype StreamProjectionCacheRunner m =
+  StreamProjectionCacheRunner (forall a. (EventStore CounterEvent m -> StreamProjectionCache Counter m -> m a) -> IO a)
+
+streamProjectionCacheSpec
+  :: (Monad m)
+  => StreamProjectionCacheRunner m
+  -> Spec
+streamProjectionCacheSpec (StreamProjectionCacheRunner withStoreAndCache) = do
+  context "when the store is empty" $ do
+
+    it "should be able to store and load simple projections" $ do
+      snapshot <- withStoreAndCache $ \_ cache -> do
+        storeProjectionSnapshot cache nil 4 (Counter 100)
+        loadProjectionSnapshot cache nil
+      snapshot `shouldBe` Just (4, Counter 100)
+
+  context "when the store has some events in one stream" $ do
+
+    it "should load from a stream of events" $ do
+      snapshot <- withStoreAndCache $ \store cache -> do
+        _ <- storeEvents store AnyVersion nil [Added 1, Added 2]
+        getLatestProjectionWithCache store cache (streamProjection counterProjection nil)
+      streamProjectionVersion snapshot `shouldBe` 1
+      streamProjectionState snapshot `shouldBe` Counter 3
+
+    it "should work with updateProjectionCache" $ do
+      snapshot <- withStoreAndCache $ \store cache -> do
+        _ <- storeEvents store AnyVersion nil [Added 1, Added 2, Added 3]
+        updateProjectionCache store cache (streamProjection counterProjection nil)
+        getLatestProjectionWithCache store cache (streamProjection counterProjection nil)
+      streamProjectionUuid snapshot `shouldBe` nil
+      streamProjectionVersion snapshot `shouldBe` 2
+      streamProjectionState snapshot `shouldBe` Counter 6
+
+newtype GloballyOrderedProjectionCacheRunner m =
+  GloballyOrderedProjectionCacheRunner
+  (forall a.
+    ( EventStore CounterEvent m
+    -> GloballyOrderedEventStore CounterEvent m
+    -> GloballyOrderedProjectionCache Text Counter m -> m a
+    ) -> IO a)
+
+globallyOrderedProjectionCacheSpec
+  :: (Monad m)
+  => GloballyOrderedProjectionCacheRunner m
+  -> Spec
+globallyOrderedProjectionCacheSpec (GloballyOrderedProjectionCacheRunner withStoreAndCache) = do
+  context "when the store is empty" $ do
+
+    it "should be able to store and load simple projections" $ do
+      snapshot <- withStoreAndCache $ \_ _ cache -> do
+        storeProjectionSnapshot cache "key" 4 (Counter 100)
+        loadProjectionSnapshot cache "key"
+      snapshot `shouldBe` Just (4, Counter 100)
+
+  context "when the store has some events in one stream" $ do
+
+    it "should load from a global stream of events" $ do
+      snapshot <- withStoreAndCache $ \store globalStore cache -> do
+        _ <- storeEvents store AnyVersion nil [Added 1, Added 2]
+        getLatestGlobalProjectionWithCache globalStore cache (globallyOrderedProjection counterGlobalProjection) "key"
+      globallyOrderedProjectionSequenceNumber snapshot `shouldBe` 2
+      globallyOrderedProjectionState snapshot `shouldBe` Counter 3
+
+    it "should work with updateGlobalProjectionCache" $ do
+      snapshot <- withStoreAndCache $ \store globalStore cache -> do
+        _ <- storeEvents store AnyVersion nil [Added 1, Added 2, Added 3]
+        updateGlobalProjectionCache globalStore cache (globallyOrderedProjection counterGlobalProjection) "key"
+        getLatestGlobalProjectionWithCache globalStore cache (globallyOrderedProjection counterGlobalProjection) "key"
+      globallyOrderedProjectionSequenceNumber snapshot `shouldBe` 3
+      globallyOrderedProjectionState snapshot `shouldBe` Counter 6
+
+  context "when events from multiple UUIDs are inserted" $ do
+
+    it "should have the correct cached projection value" $ do
+      snapshot <- withStoreAndCache $ \store globalStore cache -> do
+        insertExampleEvents store
+        updateGlobalProjectionCache globalStore cache (globallyOrderedProjection counterGlobalProjection) "key"
+        getLatestGlobalProjectionWithCache globalStore cache (globallyOrderedProjection counterGlobalProjection) "key"
+      globallyOrderedProjectionSequenceNumber snapshot `shouldBe` 5
+      globallyOrderedProjectionState snapshot `shouldBe` Counter 15
