Hedgehog on a REST API, part 2
This is a short follow-up to Hedgehog on a REST API where I actually run the tests in that post.
Fixing an issue with the model
The first issue I run into is
━━━ Main ━━━ ✗ sequential failed after 18 tests and 1 shrink. ┏━━ tst/test-01.hs ━━━ 89 ┃ getUser :: (MonadGen n, MonadIO m) => Command n m State 90 ┃ getUser = Command gen exec [ Require r 91 ┃ , Ensure e 92 ┃ ] 93 ┃ where 94 ┃ gen (State m) = case M.keys m of 95 ┃ [] -> Nothing 96 ┃ ks -> Just $ GetUser <$> Gen.element ks 97 ┃ 98 ┃ exec (GetUser i) = liftIO $ do 99 ┃ mgr <- newManager defaultManagerSettings 100 ┃ getReq <- parseRequest $ "GET http://localhost:3000/users/" ++ show i 101 ┃ getResp <- httpLbs getReq mgr 102 ┃ let us = decode $ responseBody getResp :: Maybe [User] 103 ┃ return (status200 == responseStatus getResp, us) 104 ┃ 105 ┃ r (State m) (GetUser i) = i `elem` M.keys m 106 ┃ 107 ┃ e _ _ (GetUser _) (r, us) = do 108 ┃ r === True 109 ┃ assert $ isJust us 110 ┃ (length <$> us) === Just 1 ┃ ^^^^^^^^^^^^^^^^^^^^^^^^^^ ┃ │ Failed (- lhs =/= + rhs) ┃ │ - Just 0 ┃ │ + Just 1 ┏━━ tst/test-01.hs ━━━ 118 ┃ prop_seq :: Property 119 ┃ prop_seq = property $ do 120 ┃ actions <- forAll $ Gen.sequential (Range.linear 1 10) initialState [addUser, deleteUser, getUser] ┃ │ Var 0 = AddUser "" ┃ │ Var 1 = GetUser 1 121 ┃ resetWS 122 ┃ executeSequential initialState actions This failure can be reproduced by running: > recheck (Size 17) (Seed 2158538972777046104 (-1442908127347265675)) sequential ✗ 1 failed.
It's easy to verify this using httpie:
$ http -p b POST :3000/users userId:=0 "userName=" { "userId": 0, "userName": "" } $ http -p b GET :3000/users/1 []
It's clear that my assumption that User ID starts at 1 is wrong. Luckily
fixing that isn't too difficult. Instead of defining the update function for
addUser
as
u (State m) (AddUser n) _o = State $ M.insert k n m where k = succ $ foldl max 0 (M.keys m)
I define it as
u (State m) (AddUser n) _o = State $ M.insert k n m where k = case M.keys m of [] -> 0 ks -> succ $ foldl max 0 ks
The complete code at this point can be found here.
Fixing another issue with the model
With that fix in place another issue with the model shows up
━━━ Main ━━━ ✗ sequential failed after 74 tests and 2 shrinks. ┏━━ tst/test-01.hs ━━━ 91 ┃ getUser :: (MonadGen n, MonadIO m) => Command n m State 92 ┃ getUser = Command gen exec [ Require r 93 ┃ , Ensure e 94 ┃ ] 95 ┃ where 96 ┃ gen (State m) = case M.keys m of 97 ┃ [] -> Nothing 98 ┃ ks -> Just $ GetUser <$> Gen.element ks 99 ┃ 100 ┃ exec (GetUser i) = liftIO $ do 101 ┃ mgr <- newManager defaultManagerSettings 102 ┃ getReq <- parseRequest $ "GET http://localhost:3000/users/" ++ show i 103 ┃ getResp <- httpLbs getReq mgr 104 ┃ let us = decode $ responseBody getResp :: Maybe [User] 105 ┃ return (status200 == responseStatus getResp, us) 106 ┃ 107 ┃ r (State m) (GetUser i) = i `elem` M.keys m 108 ┃ 109 ┃ e _ _ (GetUser _) (r, us) = do 110 ┃ r === True 111 ┃ assert $ isJust us 112 ┃ (length <$> us) === Just 1 ┃ ^^^^^^^^^^^^^^^^^^^^^^^^^^ ┃ │ Failed (- lhs =/= + rhs) ┃ │ - Just 0 ┃ │ + Just 1 ┏━━ tst/test-01.hs ━━━ 120 ┃ prop_seq :: Property 121 ┃ prop_seq = property $ do 122 ┃ actions <- forAll $ Gen.sequential (Range.linear 1 10) initialState [addUser, deleteUser, getUser] ┃ │ Var 0 = AddUser "" ┃ │ Var 1 = DeleteUser 0 ┃ │ Var 2 = AddUser "" ┃ │ Var 3 = GetUser 0 123 ┃ resetWS 124 ┃ executeSequential initialState actions This failure can be reproduced by running: > recheck (Size 73) (Seed 3813043122711576923 (-444438259649958339)) sequential ✗ 1 failed.
Again, verifying this using httpie shows what the issue is
$ http -p b POST :3000/users userId:=0 "userName=" { "userId": 0, "userName": "" } $ http -p b DELETE :3000/users/0 $ http -p b POST :3000/users userId:=0 "userName=" { "userId": 1, "userName": "" } $ http -p b GET :3000/users/0 []
In other words, the model assumes that the 0 User ID get's re-used.
To fix this I need a bigger change. The central bit is that the state is changed to keep track of the index more explicitly. That is, it changes from
newtype State (v :: * -> *)= State (M.Map Int Text) deriving (Eq, Show)
to
data State (v :: * -> *)= State Int (M.Map Int Text) deriving (Eq, Show)
That change does, quite obviously, require a bunch of other changes in the other functions dealing with the state. The complete file can be viewed here.
All is well, or is it?
After this the tests pass, so all is good in the world, right?
In the test I defined the property over rather short sequences of commands. What
happens if I increase the (maximum) length of the sequences a bit? Instead using
Range.linear 1 10
I'll use Range.linear 1 1000
. Well, besides taking
slightly longer to run I get another sequence of commands that triggers an
issue:
━━━ Main ━━━ ✗ sequential failed after 13 tests and 29 shrinks. ┏━━ tst/test-01.hs ━━━ 87 ┃ getUser :: (MonadGen n, MonadIO m) => Command n m State 88 ┃ getUser = Command gen exec [ Require r 89 ┃ , Ensure e 90 ┃ ] 91 ┃ where 92 ┃ gen (State _ m) = case M.keys m of 93 ┃ [] -> Nothing 94 ┃ ks -> Just $ GetUser <$> Gen.element ks 95 ┃ 96 ┃ exec (GetUser i) = liftIO $ do 97 ┃ mgr <- newManager defaultManagerSettings 98 ┃ getReq <- parseRequest $ "GET http://localhost:3000/users/" ++ show i 99 ┃ getResp <- httpLbs getReq mgr 100 ┃ let us = decode $ responseBody getResp :: Maybe [User] 101 ┃ return (status200 == responseStatus getResp, us) 102 ┃ 103 ┃ r (State _ m) (GetUser i) = i `elem` M.keys m 104 ┃ 105 ┃ e _ _ (GetUser _) (r, us) = do 106 ┃ r === True 107 ┃ assert $ isJust us 108 ┃ (length <$> us) === Just 1 ┃ ^^^^^^^^^^^^^^^^^^^^^^^^^^ ┃ │ Failed (- lhs =/= + rhs) ┃ │ - Just 0 ┃ │ + Just 1 ┏━━ tst/test-01.hs ━━━ 116 ┃ prop_seq :: Property 117 ┃ prop_seq = property $ do 118 ┃ actions <- forAll $ Gen.sequential (Range.linear 1 1000) initialState [addUser, deleteUser, getUser] ┃ │ Var 0 = AddUser "" ┃ │ Var 2 = AddUser "" ┃ │ Var 5 = AddUser "" ┃ │ Var 7 = AddUser "" ┃ │ Var 9 = AddUser "" ┃ │ Var 11 = AddUser "" ┃ │ Var 20 = AddUser "" ┃ │ Var 28 = AddUser "" ┃ │ Var 30 = AddUser "" ┃ │ Var 32 = AddUser "" ┃ │ Var 33 = AddUser "" ┃ │ Var 34 = AddUser "" ┃ │ Var 37 = AddUser "" ┃ │ Var 38 = AddUser "" ┃ │ Var 41 = AddUser "" ┃ │ Var 45 = AddUser "" ┃ │ Var 47 = GetUser 15 119 ┃ resetWS 120 ┃ executeSequential initialState actions This failure can be reproduced by running: > recheck (Size 12) (Seed 2976784816810995551 (-47094630645854485)) sequential ✗ 1 failed.
That is, after inserting 16 users, we don't see any user when trying to get that 16th user (User ID 15). That's a proper bug in the server.
As a matter of fact, this is the bug I put into the server and was hoping to find. In particular, I wanted hedgehog to find the minimal sequence leading to this bug.1 Which it clearly has!
Footnotes:
If you recall from the previous post, I was interested in the integrated shrinking offered by hedgehog.