method 0.1.0.0 → 0.2.0.0
raw patch · 5 files changed
+71/−64 lines, 5 files
Files
- method.cabal +1/−1
- src/Control/Method.hs +18/−15
- src/Test/Method.hs +39/−22
- src/Test/Method/Mock.hs +9/−19
- test/Test/Method/MockSpec.hs +4/−7
method.cabal view
@@ -4,7 +4,7 @@ -- For further documentation, see http://haskell.org/cabal/users-guide/ name: method-version: 0.1.0.0+version: 0.2.0.0 synopsis: rebindable methods for improving testability description: This package provides Method typeclass, which represents
src/Control/Method.hs view
@@ -68,18 +68,17 @@ -- -- @ -- type UserRepository env = UserRepository {--- findById :: UserId -> 'RIO' env (Maybe User)--- create :: User -> 'RIO' env UserId+-- _findById :: UserId -> 'RIO' env (Maybe User)+-- _create :: User -> 'RIO' env UserId -- }+-- makeLenses UserRepository'' -- @ -- -- And add Has-pattern typeclass.--- It's better to user 'SimpleGetter' instead of 'Lens',--- because we rarely modify the interface. -- -- @ -- class HasUserRepository env where--- userRepositoryL :: 'SimpleGetter' env (UserRepository env)+-- userRepositoryL :: Lens\' env (UserRepository env) -- @ -- -- In @signup@ function, call @findById@ method via 'invoke'.@@ -87,7 +86,7 @@ -- @ -- signin :: HasUserRepository env => UserId -> Password -> RIO env (Maybe User) -- signin userId pass = do--- muser <- invoke (userRepositoryL . to findById) userId+-- muser <- invoke (userRepositoryL . findById) userId -- pure $ do -- user <- muser -- guard (authCheck user pass)@@ -100,13 +99,13 @@ -- @ -- userRepositoryImpl :: UserRepository env -- userRepositoryImpl = UserRepository {--- findById = ...,--- create = ...+-- _findById = ...,+-- _create = ... -- } -- -- data ProductionEnv = ProductionEnv -- instance HasUserRepository ProductionEnv where--- userRepositoryL = to $ const userRepositoryImpl+-- userRepositoryL = lens (const userRepositoryImpl) const -- @ -- -- In test code, inject @UserRepository@ mock implementation.@@ -114,21 +113,25 @@ -- @ -- userRepositoryMock :: UserRepository env -- userRepositoryMock = UserRepository {--- findById = \userId -> pure $ Just (User userId "password123")--- createUser = \user -> pure $ Just "example"+-- _findById = \userId -> pure $ Just (User userId "password123")+-- _createUser = \user -> pure $ Just "example" -- } ----- data TestEnv = TestEnv+-- data TestEnv = TestEnv { _userRepository :: UserRepository Env }+-- makeLenses TestEnv''+-- -- instance HasUserRepository TestEnv where--- userRepositoryL = to $ const userRepositoryMock+-- userRepositoryL = userRepository --+-- env = TestEnv userRepositoryMock+-- -- test :: Spec -- test = describe "signin" $ do -- it "return user for correct password" $ do--- runRIO TestEnv (signin "example" "password123")+-- runRIO env (signin "example" "password123") -- ``shouldReturn`` Just (User "example" "password123") -- it "return Nothing for incorrect password" $ do--- runRIO TestEnv (signin "example" "wrong")+-- runRIO env (signin "example" "wrong") -- ``shouldReturn`` Nothing -- @
src/Test/Method.hs view
@@ -20,7 +20,6 @@ thenMethod, throwNoStubShow, throwNoStub,- NoStubException (NoStubException), -- * Monitor @@ -68,8 +67,7 @@ when, ) import Test.Method.Mock- ( NoStubException (NoStubException),- mockup,+ ( mockup, thenAction, thenMethod, thenReturn,@@ -98,10 +96,10 @@ -- @ -- fizzbuzz :: Int -> IO String -- fizzbuzz = 'mockup' $ do--- 'when' ('args' (\x -> mod x 15 == 0)) `'thenReturn'` "fizzbuzz"--- 'when' ('args' (\x -> mod x 3 == 0)) `'thenReturn'` "fizz"--- 'when' ('args' (\x -> mod x 5 == 0)) `'thenReturn'` "buzz"--- 'when' ('args' (>=0)) `'thenMethod'` (\x -> pure $ show x)+-- 'when' ('args' (\\x -> mod x 15 == 0)) `'thenReturn'` "fizzbuzz"+-- 'when' ('args' (\\x -> mod x 3 == 0)) `'thenReturn'` "fizz"+-- 'when' ('args' (\\x -> mod x 5 == 0)) `'thenReturn'` "buzz"+-- 'when' ('args' (>=0)) `'thenMethod'` (\\x -> pure $ show x) -- 'throwNoStubShow' $ 'when' 'anything' -- @ --@@ -114,37 +112,56 @@ -- >>> fizzbuzz 5 -- "buzz" -- >>> fizzbuzz (-1)--- *** Exception: NoStubException "-1"+-- *** Exception: no stub found for argument: -1+-- CallStack (from HasCallStack):+-- error, called at src/Test/Method/Mock.hs:98:9 in method-0.2.0.0-inplace:Test.Method.Mock" -- @ -- $monitor --+-- Production code+-- -- @--- type ExampleMethod = Int -> String -> IO String--- example :: ExampleMethod--- example n s | n < 0 = throwString "negative n"--- | otherwise = pure $ concat $ replicate n s+-- type ExampleMethod env = Int -> String -> RIO env () ----- doit :: ExampleMethod -> IO ()--- doit example = (do--- example 2 "foo" >>= putStrLn--- example 3 "foo" >>= putStrLn--- example (-1) "bar" >>= putStrLn--- example 3 "bar" >>= putStrLn) `catchAny` (const $ pure ())+-- class HasExampleMethod env where+-- exampleL :: Lens\' env (ExampleMethod env)+--+-- doit :: HasExampleMethod env => RIO env ()+-- doit = (do+-- invoke exampleL 2 "foo"+-- invoke exampleL 3 "foo"+-- invoke exampleL (-1) "bar"+-- invoke exampleL 3 "bar") `catchAny` (const $ pure ()) -- @ --+-- Test code+-- -- @+-- data Env = Env { _example :: ExampleMethod env }+-- makeLenses Env''+--+-- instance HasExampleMethod Env where+-- exampleL = example+--+-- exampleMock :: ExampleMethod+-- exampleMock = 'mockup' $ do+-- 'when' ('args' ((<0), 'anything')) `'thenAction'` throwString "negative n"+-- 'when' 'anything' `'thenReturn'` ()+--+-- env = Env exampleMock+-- -- spec :: Spec -- spec = describe "doit" $ do--- before ('withMonitor_' $ \\monitor -> doit ('watch' monitor example))+-- before $ 'withMonitor_' $ \\monitor -> runRIO env $ local (exampleL %~ 'watch' monitor) doit ----- it "calls example _ \"foo\" twice" $ \\logs -> do+-- it "calls example _ \\\"foo\\\" twice" $ \\logs -> do -- logs `'shouldSatisfy'` ((==2) `'times'` 'call' ('args' ('anything', (=="foo")))) ----- it "calls example (-1) \"bar\" once" $ \\logs -> do+-- it "calls example (-1) \\\"bar\\\" once" $ \\logs -> do -- logs `'shouldSatisfy'` ((==1) `'times'` 'call' ('args' ((==(-1)), (=="bar")))) ----- it "does not call example 3 \"bar\" " $ \\logs -> do+-- it "does not call example 3 \\\"bar\\\" " $ \\logs -> do -- logs `'shouldSatisfy'` ((==0) `'times'` 'call' ('args' ((==3), (=="bar")))) -- @
src/Test/Method/Mock.hs view
@@ -19,7 +19,6 @@ thenMethod, throwNoStubShow, throwNoStub,- NoStubException (NoStubException), ) where @@ -27,8 +26,6 @@ ( Method (Args, Base, Ret, curryMethod, uncurryMethod), TupleLike (AsTuple, toTuple), )-import Data.Data (Typeable)-import RIO (Exception, MonadThrow (throwM)) import RIO.List (find) import RIO.Writer (MonadWriter (tell), Writer, execWriter) import Test.Method.Matcher (Matcher)@@ -40,11 +37,6 @@ | Combine (MockSpec method) (MockSpec method) | MockSpec (Matcher (Args method)) method -newtype NoStubException = NoStubException String- deriving (Show, Typeable)--instance Exception NoStubException- instance Semigroup (MockSpec method) where (<>) = Combine @@ -54,10 +46,10 @@ -- | generate a method from Mock DSL. -- Mock DSL consists of rules. -- On a call of generated method, the first rule matched the arguments is applied.-mockup :: (Method method, MonadThrow (Base method)) => Mock method -> method+mockup :: (Method method) => Mock method -> method mockup spec = buildMock (execWriter spec) -buildMock :: (Method method, MonadThrow (Base method)) => MockSpec method -> method+buildMock :: Method method => MockSpec method -> method buildMock spec = fromRules $ toRules spec -- | @matcher `'thenReturn'` value@ means the method return @value@@@ -81,13 +73,12 @@ thenMethod :: (Method method) => Matcher (Args method) -> method -> Mock method thenMethod matcher method = tell $ MockSpec matcher method --- | @'throwNoStubShow' matcher@ means the method throws a 'NoStubException'+-- | @'throwNoStubShow' matcher@ means the method raises a runtime exception -- if the arguments matches @matcher@. The argument tuple is converted to 'String' by -- using 'show' function. throwNoStubShow :: ( Method method, Show (AsTuple (Args method)),- MonadThrow (Base method), TupleLike (Args method) ) => Matcher (Args method) ->@@ -96,24 +87,23 @@ tell $ MockSpec matcher $ curryMethod $- throwM . NoStubException . show . toTuple+ error . ("no stub found for argument: " <>) . show . toTuple --- | @'throwNoStubShow' fshow matcher@ means the method throws a 'NoStubException'+-- | @'throwNoStubShow' fshow matcher@ means the method raises runtime exception -- if the arguments matches @matcher@. The argument tuple is converted to 'String' by -- using 'fshow' function.-throwNoStub :: (Method method, MonadThrow (Base method)) => (Args method -> String) -> (Args method -> Bool) -> Mock method+throwNoStub :: (Method method) => (Args method -> String) -> (Args method -> Bool) -> Mock method throwNoStub fshow matcher = tell $ MockSpec matcher $- curryMethod $- throwM . NoStubException . fshow+ curryMethod $ error . ("no stub found for argument: " <>) . fshow -fromRules :: (Method method, MonadThrow (Base method)) => [(Matcher (Args method), method)] -> method+fromRules :: Method method => [(Matcher (Args method), method)] -> method fromRules rules = curryMethod $ \args -> let ret = find (\(matcher, _) -> matcher args) rules in case ret of Just (_, method) -> uncurryMethod method args- Nothing -> throwM $ NoStubException "no mock"+ Nothing -> error "no stub. For debugging, use `throwNoStubShow anything`" toRules :: MockSpec method -> [(Matcher (Args method), method)] toRules = reverse . go []
test/Test/Method/MockSpec.hs view
@@ -1,8 +1,8 @@ module Test.Method.MockSpec where -import Test.Hspec (Selector, Spec, describe, it, shouldReturn, shouldThrow)+import Test.Hspec (Spec, anyErrorCall, describe, it, shouldReturn, shouldThrow) import Test.Method.Matcher (ArgsMatcher (args), args', when)-import Test.Method.Mock (NoStubException, mockup, thenReturn)+import Test.Method.Mock (mockup, thenReturn) spec :: Spec spec = do@@ -14,11 +14,8 @@ it "mock method 1 2 returns 3" $ do method 1 2 `shouldReturn` 3 - it "mock method 2 2 throws Exception" $ do- method 0 2 `shouldThrow` anyNoStubException+ it "mock method 2 2 throws ErrorCall" $ do+ method 0 2 `shouldThrow` anyErrorCall it "mock method 2 3 returns 42" $ do method 2 3 `shouldReturn` 42--anyNoStubException :: Selector NoStubException-anyNoStubException _ = True