coconup
coconup

Reputation: 1005

Rails ActiveSupport::TestCase - How do I dynamically define tests together with their helper methods?

I am building some abstraction into my Rails (5.2) tests as I want to run the same tests multiple times with different parameter sets.

I can successfully create a helper to generate test classes on the fly. This looks like the following, within my test_helper.rb:

class << self
    def test_configs(configs: DEFAULT_CONFIGS, &block)
        configs.each do |c|
            Class.new(ActiveSupport::TestCase) { yield(c) }
        end
    end
end

I can use this helper in any given test file as follows:

require 'test_helper'

class SampleTest < ActiveSupport::TestCase
  test_configs do |c|
    test "#{c[:name]} - something is true" do
      puts "#{c[:name]}" => # Correctly outputs c[:name]
    end
  end
end

"#{c[:name]}" is correctly interpolated for each iteration according to what "config" is passed from the helper. So far so good.

I am having though a problem creating some helper methods that also make use of the variable c, either within the test_configs method itself or within single test files.

None of the following works in giving a consistent match between the c variable that is passed to the test titles and what happens within the tests themselves:

# Approach 1
class SampleTest < ActiveSupport::TestCase
  test_configs do |c|
    def config_name
      "#{c[:name]}" # => undefined local variable or method `c'
    end

    test "#{c[:name]} - something is true" do
      puts "#{config_name}"
    end
  end
end

# Approach 2
class SampleTest < ActiveSupport::TestCase
  test_configs do |c|
    define_method("config_name") {
      "#{c[:name]}"
    }

    test "#{c[:name]} - something is true" do
      puts "#{config_name}" # => Uses only the last definition 
    end
  end
end

# Approach 3
class << self
    def test_configs(configs: DEFAULT_CONFIGS, &block)
        configs.each do |c|
            define_method "config_name" do # (same with def config_name)
                "#{c[:name]}"
            end
            Class.new(ActiveSupport::TestCase) { yield(c) }
        end
    end
end
# => undefined local variable or method `config_name'

How do I get to correctly "inject" methods which make use of the passed variable?

Upvotes: 2

Views: 1133

Answers (1)

coconup
coconup

Reputation: 1005

The right approach was similar to the 3rd one in my question, but yield must be replaced by instance_eval in order for the code within the block to inherit the context of the new class I am creating:

# test_helper.rb
class ActiveSupport::TestCase
  class << self
    def test_configs(configs: DEFAULT_CONFIGS, &block)
      configs.each do |c|
        Class.new(ActiveSupport::TestCase) {
          define_singleton_method "config_title" do
            "#{c[:name]}".capitalize
          end

          define_method "config_name" do
            "#{c[:name]}"
          end

          instance_eval(&block)
        end
      end
    end
  end
end


# sample_test.rb
require 'test_helper'

class SampleTest < ActiveSupport::TestCase
  test_configs do
    test "#{config_title} - something is true" do # => Correctly invokes the class method defined above
      puts "#{config_name}" # => Correctly invokes the instance method defined above
    end
  end
end

As you can see, there is also a difference in that class methods must be defined in order to be used within test titles, while instance methods must be defined for being used within tests.

In case c must be passed to the block (e.g. for defining additional methods within the block itself), instance_exec should be the way to go: docs.

Upvotes: 1

Related Questions