Is it necessary, or useful, to call `:rand.seed` in tests?

I’m writing a test that generates a lot of ‘random’ cases. I’m using Enum.random/1 and Enum.take_random/2 and it occurred to me that I should see them, and use the ExUnit seed too, so that I could reproduce the random cases as well.

Per the docs, both of the Enum functions can be seeded by calling :rand.seed.

But is it necessary or useful to seed myself, either in individual tests, or in test setup callbacks?

I would think the seed that mix test displays is already being used to, effectively, call :rand.seed. And that’s what it looks like the ExUnit runner code is doing:

  defp generate_test_seed(seed, %ExUnit.Test{module: module, name: name}) do
    :rand.seed(@rand_algorithm, {:erlang.phash2(module), :erlang.phash2(name), seed})
  end

And yet there’s also this related Stack Overflow question:

Interestingly, that question states:

I have a test case that needs to use random integer, so I have:

test "test with random integer" do      
  IO.inspect :random.uniform(10)
  assert true
end

This always prints 4 when I run it, even though I can see different seeds in console output:

Randomized with seed 197796
...
Randomized with seed 124069

So their problem isn’t that the randomness isn’t reproducible, it’s that it’s not very random.

I tried running the test quoted above. I ran it via mix test some_file.exs:123 three (3) times and every time I saw the same 5 output to the console – weird!

I ran mix test – i.e. all of the tests in the same (pre-existing) project to which I added the ‘test test’ – and saw the same output again, and then each time after several more runs.

I think maybe I was confused – coming from other programming languages – because :rand.seed does NOT update ‘global state’, or at least not the state of the entire ‘run test’ execution. From the :rand docs:

The functions with implicit state use the process dictionary variable rand_seed to remember the current state.

If a process calls uniform/0, uniform/1 or uniform_real/0 without setting a seed first, seed/1 is called automatically with the default algorithm and creates a non-constant seed.

The functions with explicit state never use the process dictionary.

I’m guessing that it is necessary (and thus useful) to call :rand.seed in individual tests, as I suspect they all (mostly) run in their own (Elixir/Erlang/OTP) process anyways.

Am I right? Wrong? What am I missing?

Aaargh – I’m still confused.

The ‘test test’ now looks like this:

  test "test with random integer" do
    ex_unit_seed = ExUnit.configuration()[:seed]
    :rand.seed(:exsss, {ex_unit_seed, ex_unit_seed, ex_unit_seed})
    IO.inspect "seed: #{ex_unit_seed}"

    IO.inspect "random integer: #{:random.uniform(10)}"
    assert true
  end

Some example console output:

$mix test some_file.exs:123
...
"seed: 516976"
"random integer: 5"
...
$mix test some_file.exs:123
...
"seed: 241779"
"random integer: 5"
...
$mix test some_file.exs:123
...
"seed: 128789"
"random integer: 5"
...
$mix test some_file.exs:123
...
"seed: 996907"
"random integer: 5"

Maybe the problem is that the ‘test test’ code is using :random instead of :rand? From the docs for :random:

Note

The improved rand module is to be used instead of this module.

I replaced :random.uniform(10) with :rand.uniform(10) and – finally!:

$mix test some_file.exs:123
...
"seed: 699274"
"random integer: 1"

Whew!

The problem was :random – this ‘test test’ works as expected (i.e. output changes with different seeds):

  test "test with random integer" do
    ex_unit_seed = ExUnit.configuration()[:seed]
    #:rand.seed(:exsss, {ex_unit_seed, ex_unit_seed, ex_unit_seed}) # Do NOT set the seed!
    IO.inspect "seed: #{ex_unit_seed}"

    IO.inspect "random integer: #{:rand.uniform(10)}"
    assert true
  end

The other behavior makes it seem :random is just broken; not just deprecated.

1 Like

:random has its own seed, separate from :rand. That’s why it’s not affected by your seed calls.

Yeah – I missed that at first.

But I’m still surprised that :random.uniform/1, even without being explicitly seeded, seems to be ‘seeded’ by some kind of constant for the entire system.