Generate Rake test tasks dynamically (based on existing test files) in a Rakefile
Asked Answered
H

3

5

I'm generating test tasks dynamically based on existing test files in a Rakefile. Consider you have various unit test files named after the pattern test_<name>.rb. So what I'm doing is creating a task named after the file name inside the 'test' namespace. With the code below I can then call all the tests with rake test:<name>

require 'rake/testtask'

task :default => 'test:all'

namespace :test do

  desc "Run all tests"
  Rake::TestTask.new(:all) do |t|
    t.test_files = FileList['test_*.rb']
  end

  FileList['test_*.rb'].each do |task|
    name = task.gsub(/test_|\.rb\z/, '')
    desc "Run #{name} tests"
    Rake::TestTask.new(:"#{name}") do |t|
      t.pattern = task
    end
  end

end

The above code works, it just seems too much code for simple task generation. And I still haven't figured out a way to print some description text to the console like puts "Running #{name} tests:"

Is there a more elegant way than the above method?

EDIT: What I really expected to get was an alternative to the loop to define the tasks dynamically but I guess the rake lib doesn't provide any helper to that so I'm stuck with the loop.

Hamburg answered 2/3, 2012 at 19:37 Comment(2)
Edited the code from the tip of @ScottJSheaHamburg
For those who don't know, Jim Weirich is the author of the Rake project, I thank his time to answer my question below.Hamburg
E
13

Here's another way to solve the problem using rules in Rake.

A rake rule kicks in whenever rake wants to build "X", and it finds a rules that says "to build X, use Y". We will setup a rule that triggers when someone specifies a target in the format "test:XXX", it will attempt to use a file named "test/test_XXX.rb".

require 'rake/testtask'

task :default => 'test:all'

TEST_FILES = FileList['test/test_*.rb']

namespace :test do
  desc "Run all tests"
  Rake::TestTask.new(:all) do |t|
    t.test_files = TEST_FILES
  end

  rule /^test:/ => lambda { |tn| "test/test_%s.rb" % tn.gsub(/^test:/,'') } do |rule|
    ruby rule.source
  end
end

Suppose you have a test file named "test/test_my_code.rb". To execute that test file, just type:

rake test:my_code

The rule is triggered whenever there is a target beginning with "test:" that cannot matched by any other task. It then looks for a file given by the lamdba function. The lambda transforms the target name "test:XXX" into a filename "test/test_XXX.rb". If the filename exists, the body of the rule is executed.

The body of the rule just runs the test file as an executable. This is generally enough to run the tests of a single file. If you need to add library paths (e.g. "lib") to the load path for the tests, you can change the rule body to be something like

ruby "-Ilib", rule.source

Another difference between this and the explicit loop solution is that rake will not print out descriptions for rules, so the "rake -T" output won't include the individual tests in its output.

I don't know if this is better than the the original, but it does give you some options.

Exodus answered 3/3, 2012 at 21:7 Comment(1)
Thanks for accepting my request to shed some light upon us. I had already seen the rule alternative but only now I fully understood it. I accepted your answer since it was the only one to show an alternative to the loop solution. As you said, it has, in my opinion, a major draw back since it doesn't generate a list for rake -T and the TestTask class doesn't allow one to customize the description. I intend to fork the rake repo on github and take a look on that. Meanwhile, is there any reason for such limitations in Rake::TestTask? Once again, thank you a lot for accepting my challenge.Hamburg
S
1

My idea:

namespace :test do

  FileList['test_*.rb'].each do |rakefile|
    name = rakefile.gsub(/test_|\.rb\z/, '')

    desc "Run #{name} tests"
    task name do 
      require_relative rakefile
    end
    #Define default task for :test
    task :default => name
  end

end

desc "Run all tests"
task :test => 'test:default'
task :default => 'test'

But I'm not sure if it is a good idea to replace Rake::TestTask.new with require_relative.

My solution contains another change: I replace tasktest:allwithtest:defaultand define a new tasktest`.

So you get the follwoing result with rake -T:

rake test    # Run all tests
rake test:1  # Run 1 tests
rake test:2  # Run 2 tests

If you want to run all tests, you need rake test, specific tests can be done with rake test:<name>


You may make it also via Rake::TestTask.new

require 'rake/testtask'
namespace :test do

  FileList['test_*.rb'].each do |rakefile|
    name = rakefile.gsub(/test_|\.rb\z/, '')

    Rake::TestTask.new(:"#{name}") do |t|
      t.pattern = rakefile
    end
    #Define default task for :test
    task :default => name
  end

end

desc "Run all tests"
task :test => 'test:default'
task :default => 'test'

With rake -T I get:

rake test    # Run all tests
rake test:1  # Run tests for 1
rake test:2  # Run tests for 2

The description is generated.

you may add a:

    desc 'Alternative description'
    task name

Then you get:

rake test    # Run all tests
rake test:1  # Run tests for 1 / Alternative description
rake test:2  # Run tests for 2 / Alternative description

If you want to change the text you may add

    #replace description
    Rake.application[name].comment.replace("Run #{name} tests")

after the end of Rake::TestTask.new. That's ugly code, but Rake::TestTask doesn't allow to change the description (it would be possible, but it wold be a modification of the class).

Shreveport answered 2/3, 2012 at 21:25 Comment(3)
Thanks for your contribution, I don't really like the idea of using require_relative instead of Rake::TestTask.new since TestTask adds further functionality that may be needed in the future (see rake.rubyforge.org/classes/Rake/TestTask.html). However I appreciated the suggestion of the rake test instead of rake test:all. Plese check my question edit for further considerations.Hamburg
I extended my answer. There is still a loop with FileList['test_*.rb'], but only one. The 2nd is done via the default-task.Shreveport
Notice that you are achieving the objective of running all the tests by adding each individual test as a requirement to the default task. +1 for showing different ways of running all the tests, either by running them all together or separately. ThanksHamburg
R
0

Hmmm... the one change I made was about all I could think of. Not sure it is worthy of a full answer but I wanted to make sure I did not corrupt anything. You might also try posting it on Stack Exchange's Code Review

require 'rake/testtask'

task :default => 'test:all'

namespace :test do

  desc "Run all tests"
  Rake::TestTask.new(:all) do |t|
    t.test_files = FileList['test_*.rb']
  end

  FileList['test_*.rb'].each do |task|
    name = task.gsub(/test_|\.rb\z/, '')
    desc "Run #{name} tests"
    Rake::TestTask.new(:"#{name}") do |t|
      t.pattern = task
    end
  end

end
Rive answered 2/3, 2012 at 19:50 Comment(1)
I appreciate the tip although it wasn't that kind of improvement that I was looking for.Hamburg

© 2022 - 2024 — McMap. All rights reserved.