-
Notifications
You must be signed in to change notification settings - Fork 123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Request for tutorial: How to test complicated stateful systems #139
Comments
Is the Haskell QuickCheck library (this library) at feature parity with Quviq's QuickCheck? The free version or the paid version? The free version does not have state machine based testing. Is this the reason why I'm unable to sync John Hughes' talk with the Haskell QuickCheck library - http://www.quviq.com/downloads/ |
Yes, unfortunately the state machine-based testing is not implemented in Haskell QuickCheck. It would be really nice to have it but no-one has implemented it. Quviq QuickCheck does have more features than Haskell QuickCheck, because of having full-time developers - Haskell QuickCheck is about equivalent to the free version, I would say. The only special support for testing imperative code in Haskell QuickCheck is the Since you ask, the model in the state machine testing is usually much simpler than the real system because it's implemented in the simplest possible way. For example, for the circular buffer, the model would simply be a list. For dets, I think it was just a list of key-value pairs. |
Having said that, your comment did inspire me to start hacking together a similar library for Haskell QuickCheck. So far, it seems quite plausible (I was worried that the types would get in the way, but they don't really), so maybe this is something we could have in the future. |
Happy to help. Sort of :)
How can a simple list be used as a circular buffer without explicitly dealing with the wraparound logic, at which point it become an independent implementation itself? |
This issue is also to seek guidance on how to use QC for real-life stateful apps. It seems the documentation is non-existent around this topic. For example, I can't visualise how to write tests for soemthing like a hotel reservation system. |
If we take the non state-machine approach, what would some properties look like in the case of a hotel reservation system?
|
In this case the circular buffer was used as a bounded queue, so you're not allowed to overfill it. That means the client can never observe the wraparound, it's purely an implementation detail. The precondition, postcondition and next-state function for each operation would then look something like this, where the state is given as a list get:
put(x)
|
Sounds reasonable.
This is something you would fix in your
This is a risk of course. You try to avoid it by making the model as simple as possible - no optimisations, no shortcuts. For example, in the hotel you might just define one function which takes the state of the booking system and checks if it's valid (e.g. no overbooking). Then you might say, if the hotel state is valid after making the booking then the booking should be accepted. I don't know the details of the booking system of course, but the point is you can often write the specification in a way which is more declarative than the implementation. |
I agree that we need more documentation around this. |
Do you know of anyone using QuickCheck for common business/domain oriented webapps (as opposed to complicated libraries like streams or parsers or compilers)? Probably motivating them to give a detailed talk or sharing some code can be a good start. |
@saurabhnanda My experience at work is with Scala with ScalaCheck, Scala's equivalent of QuickCheck, (which does have a version of state machine testing that I've basically never used). Unfortunately none of that is shareable and I don't have a great example to show from my own projects, although I'll see if I can whip one up in the coming days. I do, however, have a bit of experience with how ScalaCheck get used in run-of-the-mill business webapps, which sounds like it might be useful to you. In general, my takeaway has been even if checking the conceptual heart of your application can seem difficult, there's usually enough other incidental properties that are crucial to the correctness of your program to check that QuickCheck (or the equivalent) is something I include in all my work projects these days. Here's a couple of use cases that almost always come up that are usually independent of state checking and can be applied to the pure parts of your code (which are hopefully the majority).
One small note:
In addition to what @nick8325 says, I want to add it is often the case that certain invalid states are very easy to describe and check for with QuickCheck and it is often very worthwhile to even get that sort of partial coverage when devising all the necessary generators and structuring your code appropriately for checking full validity is more difficult and time-consuming. That is if you can't check your code always leaves things in a valid state, you can at least check that your code never leaves things in certain obvious invalid states. In general when I've tested the state of a CRUD system, I've almost always mocked out the persistence layer with something like a
Your hotel overbooking scenario fits in nicely; if I swapped out my persistence backend for a simple |
As an illustration of what @changlinli said, here is code that does property testing against complex logic. It runs several games of 7startups, with players playing random cards, and checks that all games terminate without throwing. The game logic is a free monad, allowing for several interpreters, including a pure one that is used for tests. Even though this tests a complicated piece of logic by only checking it doesn't crash, a few bugs were found! |
@changlinli thank you for your thoughtful reply 👍
This seems interesting. Do you generate the raw JSON strings and send them over to the WAI endpoint, so that the JSON parsing is also tested, or do you generate the Haskell values that the JSON is supposed to be parsed to. That is, what do you generate:
How easy is it to do this in a large-scale webapp? Does this basically mean all database accesses need to go via some sort of app-specific DB library (which is written on top of the underlying DB library, i.e. Persistent, Opaleye, or PG-Simple), which can then be swapped out with an in-memory implementation during testing? Isn't this too much work? |
I played around a bit and managed to get something that looks similar to Erlang quickcheck's state machine approach: https://gist.github.com/stevana/1595f3190c87220c59caaaf7c2aae04b . It seems to me that the generator, shrink function, and the safety property are quite generic (they only use the pre-conditions, the model transition functions and the post-conditions). So I'd like to see how much of that code we can template Haskell away. I also wonder if one can do away with the trace? I couldn't figure out how to show and shrink things like |
@saurabhnanda I don't know how much more I want to post here since I feel like I'm hijacking GitHub issues (@nick8325 how do you feel about this thread? I'm happy to take this elsewhere if you don't want to have your issues gobbled up by this thread), but I'll reply one more time here. With respect to raw string vs structured Haskell data type as the entry point for testing, I usually use the latter exclusively, but occasionally will add the former. Usually the process of going from strings to structured data is pretty robustly handled by something like aeson and I rarely find bugs in that part of the code. It's usually later on that I find some sort of mistake I've made (e.g. when normalizing strings for comparison does my normalization function work for all Unicode strings?).
Sometimes yes. QuickCheck is awesome, but not always a silver bullet. It depends on how you've structured your code. This isn't a passive-aggressive jab at code quality, there are reasonable worlds in which the pure part of your CUD pipeline is pretty trivially handled by third party libraries and the IO is the only interesting stuff you're writing (this is rarely true for the Read part; you usually have some interesting pure part of the Read pipeline that can be tested). That being said, here's some approaches to explore and keep in mind when designing your app to maximize its potential to be tested. First, I'll just note that you are always making an "app-specific DB library" in your application simply by writing your CRUD operations. Indeed way to think about making application is that they're just a series of increasingly specific libraries with a very thin layer for interaction at the very end (the "almost everything is a library" intuition is especially strong in the Haskell community from what I've seen).
vs
The name of the game when it comes to |
I'm very happy for people to have a discussion here, please carry on! |
I had another go at a pre-/post-condition specification; this time of a memory cell: https://gist.github.com/stevana/3973198ab378995691c8b1a68369c492 . The difference to my previous attempt is that there's no free monad nor trace, making the shrink function simpler and allowing us to use the standard |
Here's a version that also checks for race conditions: https://gist.github.com/stevana/a2bb4640d1552dd69abcf93a30d0814e . |
Cool! I had a go myself and came up with this: https://gist.github.com/nick8325/42a40c20be5527e22049e57a65f16aaf It only does the basic |
Nice; I like your typed commands and that the post-condition returns a property rather than a boolean. I'm not sure about using the state when generating commands though, as it makes your state transition function not be able to depend on the result... I've put a cleaned up and generalised version of my last gist in it's own repo: https://github.com/advancedtelematic/quickcheck-state-machine-model . (The library is in the src directory and the memory cell example in the test directory.) There are some of things I'd like to fix:
There's probably plenty more... In fact, please let me know if you can think of any other shortcomings! |
@saurabhnanda: The latter. |
Small update: I've stolen the well-typed commands and postconditions returning a Property rather than Bool ideas, thanks @nick8325! I've also added the above four problems and some more stuff as proper issues, as I haven't figured out how to solve them yet. I think I'll focus on the multiple references/variables problem next. |
I'm trying to write a small stateful test for my DB-backed system using the following strategy (which is taken from the quickcheck-state-machine package [1]:
i'm stuck at a very basic step. Every piece of data in the actual system's state is identified by a database/primary-key ID. If I have to compare the simplified-state with the actual-state I should be comparing data by these IDs. Does this mean that I have to execute a command on the actual system first, get the resulting IDs and then put them in the simplified-state? Is this the recommended way to do things? This would also mean that the ADT describing the actions will be different for the actual-system and the simplified-system, i.e:
[1] Not using the quickcheck-state-machine package because I do not completely understand its types and API. |
I can think of two ways to do it:
|
@saurabhnanda: You seem to have discovered one of the reasons for why the types are complicated in the To solve the problem that you describe the library uses something we call references. References are things that you only get once you execute the actual system. Using the
The If you have two types of references you'd use Behind the scenes, the library does basically the second way that @nick8325 describes above. The benefit of letting the library take care of references is that we get generation/shrinking of references for free. The downside is, as you noted, that the types become more complicated. We are still discussing if the extra complexity is worth it... |
In my approach this comes out simpler, not sure if this is something you considered @stevena:
The idea is that The
The function which executes commands is given an
|
@nick8325: I remember seeing this in your gist from a while ago! I didn't quite like how your state transition function always gets a That said, I've recently noticed that Erlang QuickCheck's state So yeah, that's something we have considered, and are reconsidering! |
Also relevant: hedgehogqa/haskell-hedgehog#89 |
It's super exciting to see so much activity on state machine testing, it's been missing from Haskell's testing toolkit for far too long. I thought it might be useful to give a quick overview of the approach I took in Hedgehog, linked by @stevana above. It certainly has a bit in common with the approaches already mentioned here. Essentially, the model state and each command are parameterised by a functor which is either newtype Var =
Var Int
data Symbolic a where
Symbolic :: Typeable a => Var -> Symbolic a
newtype Concrete a where
Concrete :: a -> Concrete a So if we look at the register command from the process registry example, it might look like this: data Pid v =
Pid (v Int)
data Register v =
Register Name (Pid v) This means that we can traverse over a command and replace the symbolic values with concrete ones, provided we have an environment with a mapping of variables to values. newtype Environment =
Environment {
unEnvironment :: Map Var Dynamic
}
reify :: HTraversable t => Environment -> t Symbolic -> Either EnvironmentError (t Concrete) In order for this to work, the commands need to implement instance HTraversable Pid where
htraverse f (Pid v) =
fmap Pid (f v)
instance HTraversable Register where
htraverse f (Register name pid) =
Register
<$> pure name
<*> htraverse f pid Within this system, the types of the functions which the user can implement for their various commands are as follows: Command Generation :: Monad m => state Symbolic -> Maybe (Gen m (input Symbolic)) Precondition :: state Symbolic -> input Symbolic -> Bool State Update (note that we use this during generation and execution) :: Ord1 v => state v -> input v -> v output -> state v Command Execution :: Monad m => input Concrete -> Test m output Postcondition :: Monad m => state Concrete -> input Concrete -> output -> Test m () Currently I can only do sequential tests, but I think the design is close enough to the Erlang one that it should be easy enough to extend when I get around to it. I'm still trying to get my head around linearisation, the paper and articles linked from @stevana's readme have been super useful for this. |
Agreed, and thanks for the explanation! I really like the I hope to add the parallel property helper to that same branch soon, meanwhile feel free to ask questions about the linearisation bit if you have any. Also, you might be interested in the following ticket, were we keep track of resources on how to improve the linearisation checker. |
In quickcheck-state-machine-0.1.0, that we just released, we use the above symbolic/concrete style references. @saurabhnanda: Your example can now be written as: data Command (v :: * -> *) :: * -> * where
CustomerSignup :: NewCustomer -> Command v CustomerId
CustomerActivation :: Reference v CustomerId -> Command v () Which, I hope you agree, is much simpler than before! Cheers @nick8325 and @jystic for the ideas! |
@saurabhnanda: I just added a CRUD webserver example to the repo. |
@stevana We've started testing our (fairly complicated) web API. Referring to this line of code in
...how would it work if the API was returning a |
You'd need to change the response type of the PostUser :: User -> Action v (Key User, User) The contact and card stuff would be similar.
Not sure I understand the question... ( |
@stevana in that case I don't really understand what |
When we use DeleteUser :: Reference v (Key User) -> Action v () The reason for this is because when we generate arbitrary actions we don't know how to generate sensible So, an action that returns multiple Apa :: Action v (Key A, Key B) While one that uses those keys is represented as: Bepa :: Reference v (Key A) -> Reference v (Key B) -> Action v () Does that make sense? |
I'll wake this discussion up with a reference to a new library that does implement Quviq-style stateful shrinking in Haskell (we built it at Quviq). https://hackage.haskell.org/package/quickcheck-dynamic |
Whoops, accidentally closed the issue. I think a tutorial on this should be implemented in |
What a coincidence, I started playing with this again yesterday (after not having thought of this for years). I'd like to see if one can design a library around @edsko 's lockstep-style where a fake is used instead of an explicit transitions function and pre- and post-conditions. I started with @nick8325 's last version from this thread, this is what I got right now (it's still work in progress). I think it's clearly better to have a fake than the transition function that both |
Discussion at Reddit: https://www.reddit.com/r/haskell/comments/5jatdg/how_to_use_quickcheck_for_stateful_testing/
I've been looking for examples of how to use Quick Check for "regular" stateful testing, where one would essentially do the following - generate a known input case, call a series of domain APIs, and finally assert the expected system state.
For example, how would one use Quick Check for testing a CRUD JSON API? Or, say a reservation system's domain API, which allows overbooking only if the admin is making the overbooking request?
Even after reading John Hughes' paper on using Quickcheck to test a circular buffer and testing at Volvo & Karla and viewing the accompanying talk I have the following confusion:
The text was updated successfully, but these errors were encountered: