Question from Elixir land. I have a GenServer that receives a cast message and performs a side effect (a database update). In a test, I want to send the cast, wait “long enough”, then confirm that the side effect happened.
Some options for waiting “long enough”:
Process.sleep/1 - bad because if it’s too short, the test will not consistently pass, but if it’s too long, the test will be slow. Also, the tests run slower in CI, which makes picking the right value tricky. A variant of this would be repeated, short “sleep and check” cycles, up to a timeout.
Modify the GenServer for the sake of the test to notify when it’s finished (eg via telemetry)
Modify the GenServer for the sake of the test to have a handle_call(:ping; when it replies, I’d know the prior message in the mailbox had been processed too
Use a standard, mailbox-dependent call like :sys.get_state/1 or :sys.get_status/1 - same idea as item 3 but no modification of the GenServer is needed. Minor downsides: it’s a bit wasteful since I don’t actually need the return value, and it requires code comments to explain why I’m calling it and ignoring the return value.
Modify the side effect function for the sake of the test to notify that it’s complete
I don’t love modifying code to emit events or handle messages solely for the sake of testing. Option 4 is my current choice, but it would be nice if there were a :sys.ping(pid) that would do the same thing - similar to what we have in Node.ping/1 (although I know that has the side effect of establishing clustering if it’s not already established).
I thought that something like :sys.ping(pid) would probably already exist in Erlang, but if it does, I haven’t found it. Does it exist? If not, should it?
I’d use the variant of the first version: “sleep and check the side effect” in a cycle. If the environment is fast, only a few checks are necessary and the test finishes fast. If the environment is slow, more checks are done, but it’s still robust.
Why not use (the equivalent of) gen_server:call/3 instead? Using polling in this case feels like a kludge.
I.e., I’d go with option 5. Perhaps pass in an optional From in the cast, and when done with the side-effect do a gen_server:reply (or equivalent) if the From is present.
I don’t love modifying code to emit events or handle messages solely for the sake of testing.
…But it is a powerful testing technique otherwise. For example, it allows to correlate effects from different nodes. If you print debug logs after completion of event processing, then you may as well replace them with the snabbkaffe tracepoints and use ?block_until macro to synchronize testcase with the SUT.