I would first test the non-random selection of a single string from a one-line file, then I'd test selection of a string from a multi-line file, then I'd test that the selection is random. You can't really test randomness in finite time, so the best you can do is to
- test that your method returns a value in the desired range, knowing that since your test will run many, many times over the lifetime of your app you'll probably find out if it ever returns something out of range, and
- prove that your code uses a source of randomness.
Let's say that the file doesn't exist in your test environment, or you don't know its contents, or don't want the asymmetry of having it be correct for one test and incorrect for others, so we'll need to provide a way for tests to point the class at different files.
We could write the following, writing one test at a time, making it pass and refactoring before writing the next. Here are the tests and code after the third test is written but before it's been implemented:
spec/models/thing_spec.rb
describe Thing do
describe '.random_select' do
it "returns a single line from a file with only one line" do
allow(Thing).to receive(:file) { "spec/models/thing/1" }
expect(Thing.random_select).to eq("Thing 1")
end
it "returns a single line from a file with multiple lines" do
allow(Thing).to receive(:file) { "spec/models/thing/2" }
expect(Thing.random_select).to be_in(['Thing 1', 'Thing 2'])
end
it "returns different lines at different times" do
allow(Thing).to receive(:file) { "spec/models/thing/2" }
srand 0
thing1 = Thing.random_select
srand 1
thing2 = Thing.random_select
expect(thing1).not_to eq(thing2)
end
end
end
app/models/thing.rb
class Thing
def self.random_select
"Thing 1" # this made the first two tests pass, but it'll need to change for all three to pass
end
def self.file
"lib/things"
end
end
When I wrote the second test I realized that it passed without any additional code changes, so I considered deleting it. But I deferred that decision, wrote the third test, and discovered that once the third test passes the second will have value, since the second test tests that the value comes from the file but the third test does not.
be_in
is a nicer way to test that the return value is in a known set than include
since it puts the actual value inside expect
where RSpec expects it.
There are other ways to control the randomness so you can test that it's used. For example, if you used sample
you could allow_any_instance_of(Array).to receive(:sample)
and return whatever you like. But I like using srand
since it doesn't require the implementation to use a specific method that uses the random number generator.
If the file can be missing or empty you'll need to test that too.