最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

ruby on rails - Iterating to create contexts from a class - Stack Overflow

programmeradmin4浏览0评论

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
  • It feels like there are some anti-patterns there. Creating a context within a loop seems like a smell. Maybe you could be better off using RSpec shared examples instead, but it's hard to go deeper on this idea without seeing the real code (the one covered by the tests) – lmtaq Commented Feb 16 at 19:41
  • 1) Where is user_flow defined? 2) What is the error message? 3) You don't have to use let, nor make individual variables for each key/value pair. Try using one hash. – Schwern Commented Feb 17 at 18:45
Add a comment  | 

1 Answer 1

Reset to default 2

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
发布评论

评论列表(0)

  1. 暂无评论