It's easy to get fixated on declaring everything with let, but you don't have to. Use let to take advantage of its lazy-evaluation, but let is most useful with shared examples as we'll see below.
If you're solving a problem by breaking up a hash into dynamically named variables, now you have two problems. That's code which is difficult to understand and maintain.
We can implement your code without using let at all. Just normal variables. And we can use the hash directly.
require 'rspec'
RSpec.describe "user flow" do
user_flows = [
{ name: "This", foo: :bar },
{ name: "That", in: :out, foo: :bar }
]
user_flows.each do |user_flow|
context "something #{user_flow[:name]}" do
it "has foo set to bar" do
expect(user_flow[:foo]).to eq :bar
end
end
end
end
But it's probably better to get rid of your flow scenarios file entirely and instead write it as contexts using shared examples. Now we can take full advantage of let.
require 'rspec'
RSpec.describe "user flow" do
shared_examples "it is a user flow" do
it "has foo set to bar" do
expect(foo).to eq :bar
end
it "has a name" do
expect(name).not_to be_empty
end
end
context "this user flow" do
let(:foo) { :bar }
let(:name) { "This" }
it_behaves_like "it is a user flow"
end
context "that user flow" do
let(:foo) { :bar }
let(:name) { "That" }
it_behaves_like "it is a user flow"
end
end
Here is a more practical example of using shared examples in multiple contexts.
require 'rspec'
class Secure
def self.something
42
end
end
class User
attr_accessor :name
def initialize(name, admin)
@name = name
@admin = admin
end
def some_admin_function
raise "Not an admin" unless @admin
Secure.something
end
def logged_in?
@logged_in
end
def login
@logged_in = true
end
def logout
@logged_in = false
end
end
RSpec.describe User do
shared_examples "it is a User" do
it 'has a username' do
expect(user.name).not_to be_empty
end
it "can log in and log out" do
user.login
expect(user).to be_logged_in
user.logout
expect(user).not_to be_logged_in
end
end
context "regular user" do
let(:user) { User.new("Regular", false) }
it_behaves_like "it is a User"
it "cannot change admin settings" do
expect { user.some_admin_function }.to raise_error "Not an admin"
end
end
context "admin user" do
let(:user) { User.new("Admin", true) }
it_behaves_like "it is a User"
it "can change admin settings" do
expect( user.some_admin_function ).to eq 42
end
end
end
Both contexts share the examples about being a user, then they have their own examples specific to their contexts.
Finally, sometimes you do have complex test data that would be too much clutter for a single test file. Rather than reading data from a YAML file, write test factories using factory_bot.
factory :user do
name { Faker::Name.name }
admin { false }
trait :admin do
admin { true }
end
initialize_with { new(name, admin) }
end
And then we can use that to create all sorts of semi-random test data.
context "regular user" do
let(:user) { build(:user) }
...
end
context "admin user" do
let(:user) { build(:user, :admin) }
...
end
user_flowdefined? 2) What is the error message? 3) You don't have to uselet, nor make individual variables for each key/value pair. Try using one hash.