Generate your own types in fast-check
- 1. TLDR
- 2. A slow start to a fast-check
- 3. Show me da code!
- 4. Generating data
- 5. Expressing the property
- 6. In conclusion
TLDR
If your custom type is:
1 | class ImaCustom { |
… and if you’re just getting started with this (like me) and thinking: “ok, now how do I randomly generate my ImaCustom
instances?”.
Try generating random sets and arrays which you can then use to create your ImaCustom
s.
e.g.
1 | import fc from "fast-check"; |
That’s basically what I wanted to write about 😊
A slow start to a fast-check
I guess you already know what property based testing is if you’re here. Recently, I was testing something… which tbh, I didn’t really know how I wanted to work. But in my mind, I could think of certain properties; certain things which had to hold on the datastructure in question when this operation happens. So - I figured I’d reach for that testing approach I know about; think is amazing; but almost never actually use 🤔
Anyway - it didn’t take long to find fast-check. I had used jsverify before, but I’ve forgotten the API and who cares anyway - I just want to code a few properties and get on with my app… maybe actually get it to a usable state 😅
So… I’m searching online:
Ye, I know more or less what property based testing is. Great - first few examples show how to generate basic types. Cool, found the list of built-in… oh “arbitraries” they’re called… hmm fancy that - and I thought I had an arbitrary name. Now if I could only get to the “how to generate your own flippin’ types” in the documentation and give these stressed eyeballs a break.
Far as I can tell - that section doesn’t exist in the docs. I did eventually have a 🤦♂️ moment and realised that any “custom” datastructure must be made up of more basic types.
Show me da code!
This is basically the data I’m working with here (Immutable.js Set / OrderedSet):
1 | export interface ContentProps { |
… which is passed in to Content
‘s constructor to create an instance. But the constructor has some constraints:
1 | if (include.has("") || exclude.has("")) { |
So basically, no empty string in the sets and they must be mutually exclusive.
Finally, a Content
can sync
given a list of file paths string[]
… and this is what I came up with for what I had in mind. NOTE: you do not need to understand or even read this code:
1 | public sync(filePaths: string[] = []): Content { |
One property I can think of is this:
In
result = content.sync(filePaths: string[])
- any filePath infilePaths
which is also incontent.exclude
should be inresult.exclude
and not inresult.include
This is the property being demoed below, but if you want to get a feel of what sync
is about, here are a couple more properties which come to mind:
- Any existing values in
content.exclude
which are not present infilePaths
should not be inresult.exclude
- Any existing values in
content.include
which are not present infilePaths
should not be inresult.include
- Any existing values in
content.include
which are infilePaths
are kept in the same order inresult.include
Generating data
To express this property, I first want to express how the “ingredients” are generated, i.e. the arbitrary data:
1 | const arbitrarySyncTestProps = fc.tuple( |
So - generate me a tuple of 2 sets of strings with a max size of 25 and whose strings are no more than 100 characters in length. Also, throw in an array of strings for good measure. Super simple - but now I want to get more specific… I should have no empty strings in either Set, nor in the array come to think of it - as those elements are meant to end up in a Set which doesn’t want empty strings. Also, I want the two Sets to be mutually exclusive… and while we’re at it - I think it won’t do to just have random strings. Taking another look at the property:
any filePath in
filePaths
which is also incontent.exclude
So, maybe I should ensure I get some strings which are in both filePaths
and exclude
:
1 | const arbitrarySyncTestProps = fc |
Expressing the property
Finally, the property can be expressed with a few simple assertions:
1 | ([include, exclude, filePaths]: [ |
Putting it all together
1 | import fc from "fast-check"; |
In conclusion
It was fun to revisit property based testing. One issue I know will come up is the matter of performance. This is taking circa 5 seconds to run so I know its going to be problem. Maybe I’ll only run the property based tests when an env var is set, or configure fast-check
to “be more fast!” … somehow (e.g. generate less tests). In any case, it’s still useful and I have already found a couple of issues with my implementation.
Any feedback on this is more than welcome 👇
🍻