I have an interesting problem, and do not know if I am overthinking something, or just missing the big picture.
I have a Rspec feature test iterating over each possible “user flow”. Each “user flow” has its own defined values in let, to determine different mock values that each “it” block needs. I have for example:
flow_scenarios:
- description: "When user is viewing after hours
stubbed_values:
- hours: "off"
- graphic: "after_hours"
...
I have a Ruby class that reads in this YAML and gives helper methods. Let’s call this UserFlowTest
. It takes the YAML and the pre-defined user.
So in my Spec file I have:
RSpec.describe "user flow", type: :feature do
let(:user) { create :user, name: "Brandon" }
let(:user_flows) { UserFlowTest.new(flow_file: Rails.root.join("after_hours.yml", user:))}
user_flow.each do |user_flow|
context "something" do
user_flow.stubbed_values.each do |stubbed_value|
stubbed_value.each do |key, value|
let(key.to_sym) { value }
end
it "will show the correct graphic" do
...
end
….
This does not work as context/describe will not allow user_flow
to be iterated over because user
is lazily loaded. Without user
going into user_flow
instantiation, this works great.
I would just like to be able to have an it block but each it block will have to have some let blocks for the setup..
What can I do?
I have an interesting problem, and do not know if I am overthinking something, or just missing the big picture.
I have a Rspec feature test iterating over each possible “user flow”. Each “user flow” has its own defined values in let, to determine different mock values that each “it” block needs. I have for example:
flow_scenarios:
- description: "When user is viewing after hours
stubbed_values:
- hours: "off"
- graphic: "after_hours"
...
I have a Ruby class that reads in this YAML and gives helper methods. Let’s call this UserFlowTest
. It takes the YAML and the pre-defined user.
So in my Spec file I have:
RSpec.describe "user flow", type: :feature do
let(:user) { create :user, name: "Brandon" }
let(:user_flows) { UserFlowTest.new(flow_file: Rails.root.join("after_hours.yml", user:))}
user_flow.each do |user_flow|
context "something" do
user_flow.stubbed_values.each do |stubbed_value|
stubbed_value.each do |key, value|
let(key.to_sym) { value }
end
it "will show the correct graphic" do
...
end
….
This does not work as context/describe will not allow user_flow
to be iterated over because user
is lazily loaded. Without user
going into user_flow
instantiation, this works great.
I would just like to be able to have an it block but each it block will have to have some let blocks for the setup..
What can I do?
Share Improve this question edited Feb 17 at 13:47 engineersmnky 29.5k2 gold badges41 silver badges63 bronze badges asked Feb 16 at 19:11 I. KhanI. Khan 2093 silver badges8 bronze badges 2 |1 Answer
Reset to default 2It'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_flow
defined? 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. – Schwern Commented Feb 17 at 18:45