igalic
How to create a custom StreamData Generator
Hi folks,
I’m trying to transform our tests for AuthToken (a wrapper around JOSE) to ExUnitProperties. But I’m kinda failing at the most basic task of transforming a simple function into a generator. What I’ve done is this:
defp stream_authtoken_keys(_seed, _range) do
Stream.repeatedly(&AuthToken.generate_key/0)
end
defp gen_authtoken_key() do
%StreamData{generator: &stream_authtoken_keys/2}
end
However, when trying to use it…
describe "keys" do
property "generate_key/0 returns a valid AES128 key" do
check all authtoken_key <- gen_authtoken_key() do
{:ok, key} = authtoken_key
assert byte_size(key) == 16
end
end
end
I get the following error:
1) property keys generate_key/0 returns a valid AES128 key (AuthTokenTest)
test/authtoken_test.exs:23
** (MatchError) no match of right hand side value: #Function<51.91433161/2 in Stream.repeatedly/1>
code: check all authtoken_key <- gen_authtoken_key() do
stacktrace:
(stream_data) lib/stream_data.ex:203: StreamData.bind_filter/5
(stream_data) lib/stream_data.ex:346: anonymous fn/5 in StreamData.bind_filter/3
(stream_data) lib/stream_data.ex:203: StreamData.check_all/6
test/authtoken_test.exs:24: (test)
Marked As Solved
igalic
After posting this in elixir (on irc & discord) I got a very decent reply that works out pretty fine, so I’m gonna close this topic for now with it!
First off, trying to generate %StreamData{} myself, is kinda bad form, since it’s basically a private struct…
We looked for alternatives, and found StreamData.constant/0. However, on its own, it always generates the same value.
Which makes a certain amount of sense when you think about how it’s called:
iex(1)> x = StreamData.constant(AuthToken.generate_key) # this is now evaluated, we're done here
%StreamData{generator: #Function<16.32994346/2 in StreamData.constant/1>}
iex(2)> x |> Enum.take(2)
[
ok: <<109, 179, 255, 84, 16, 160, 239, 132, 198, 157, 218, 254, 13, 160, 196, 137>>,
ok: <<109, 179, 255, 84, 16, 160, 239, 132, 198, 157, 218, 254, 13, 160, 196, 137>>
]
iex(3)>
The clue came from @michalmuskala:
You usually construct the opaque type one way or the other, you rarely construct it directly. For example the MapSet struct is opaque, but you can generate it by doing something like map(list(), &MapSet.new/1)
and the code we now have from @ericmj
defp gen_authtoken_key() do
StreamData.map(StreamData.list_of(StreamData.constant(:unused_tick)), fn _ ->
AuthToken.generate_key()
end)
end
what we’re doing here is using StreamData.constant/1 as a sort of clock generator, that drives the call of AuthToken.generate_key/0… and it’s nice start, but we’re needlessly generating a list_of which we then map
So let’s update this with all responses below!
defp gen_authtoken_key() do
StreamData.unshrinkable(
StreamData.bind(StreamData.constant(:unused), fn _ ->
StreamData.constant(AuthToken.generate_key())
end)
)
end
This is much cleaner! Since we use bind/2, which is essentially made for this… better see what’s going on, we can use pipes to extract the flow:
defp gen_authtoken_key() do
gen_ticker = fn _ ->
StreamData.constant(AuthToken.generate_key())
end
StreamData.constant(:unused)
|> StreamData.bind(gen_ticker)
|> StreamData.unshrinkable()
end
We feed a stream of :unused into gen_ticker lambda, just so it will be constantly called. Since our call to AuthToken.generate_key/0 is wrapped in said lambda, it will be executed on every call, rather than on ge_ticker’s declaration. Finally, we pass the so generated %StreamData{} into unshrinkable/0 (because it is) before returning it.
Also Liked
whatyouhide
Seeing this now after Add StreamData.repeatedly to pass in a function as your generator by aforward · Pull Request #104 · whatyouhide/stream_data · GitHub has been opened. Just wanted to mention here too that I also think that a generic way to convert streams to generators would be nice but there is a big limitation: generators are stateless while streams aren’t. Streams are enumerated in order and there is no way for generators to know at which index in the stream it’s at.
@ericmj also your solution can be even simpler as bind + constant is just map:
def repeatedly(fun) do
StreamData.map(StreamData.constant(nil), fn _ -> fun.() end)
end
ericmj
I found a (maybe) cleaner way of doing it that doesn’t involve generating a list:
defp gen_authtoken_key() do
StreamData.bind(StreamData.constant(:unused), fn _ ->
StreamData.constant(AuthToken.generate_key())
end)
end
I haven’t tested this so I can’t verify that it works.
EDIT: @whatyouhide has a more concise solution below: How to create a custom StreamData Generator - #12 by whatyouhide
LostKobrakai
It might also be good to make the generator unshrinkable, because shrinking doesn’t seem to make sense in that context.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #javascript
- #code-sync
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








