Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best way to test all combinations on inputs to a function in ruby

Tags:

ruby

testing

I have found the I often repeat a similar pattern in my tests and was wondering if there's a tool that could help me DRY them up. If there isn't, I would like some feedback on whether you think this could be useful. Or maybe I'm not structuring my test properly?

Say you want to test a method on a class that takes several parameters and the method returns a boolean. For that sake of the example, the method is used to determine if a UI element should be displayed. So the definition would something like:

def should_display(controller_name, account, user)
  controller_name == 'Dashboard' && 
    account.has_feature(:secret_widget) && 
    user.can_access(:secret_feature)
end

Testing this simple method in the traditional way can be painful because you would need to test every possibility.

The a complete test would look something like:

setup do
  @my_object = MyObject.new
end

test "should display with controller name = 'DashBoard', account has feature, user can access secret feature" do
  assert @my_object.should_display 'DashBoard', make_account_with_secret_widget_feature(),  make_user_with_secret_feature_access())
end

test "should display with controller name != 'DashBoard', account has feature, user can access secret feature" do
  assert_false @my_object.should_display 'DashBoard2', make_account_with_secret_widget_feature(),  make_user_with_secret_feature_access())
end

test "should display with controller name != 'DashBoard', account does has feature, user can access secret feature" do
  assert_false @my_object.should_display 'DashBoard', make_account_without_secret_widget_feature(),  make_user_with_secret_feature_access())
end

test "... 2^3 tests total to cover every possibility" do 
end

What I would like to be able do to would to define the possible input and the expected output in a generic maner. Something like (pseudo code ahead):

controler_names = ['DashBoard', 'DashBoard2']
users = [make_user_with_secret_feature_access, make_user_without_secret_feature_access]
accounts = [make_account_with_secret_feature_access, make_account_without_secret_feature_access]

TestCombinations(controller_names, users, accounts) do
  execute do |args*|
    MyObject.new.should_display(*args) 
  end
  for_inputs('DashBoard', users[0], accounts[0]).expects(true)
  othewise.expects(false)
end

I know there are things like RubyFIT(http://fit.rubyforge.org/) that does something similar (i.e. testing business rules), but that requires an external text file which I'm not a big fan of. I would prefer to keep it to code.

If you know of anything that could help, I would appreciate it.

Thanks!

like image 236
smathieu Avatar asked Sep 14 '25 23:09

smathieu


1 Answers

Well, not sure about your testing framework, but I've (ab?)used RSpec for similar things..so I'll use this here if you don't mind. Adapting to your example:

describe MyObject do
  controller_names = ['Dashboard', 'Dashboard2']

  users =    [ User.new('Chuck', [:secret_feature, :normal_feature]), 
               User.new('Joe', :normal_feature)]

  accounts = [ Acc.new('VIP account', :secret_widget), 
               Acc.new('Normal account', :boring_widget)]

  allowed = [
             [controller_names[0], users[0], accounts[0]] #, etc, ..
            ]

  before :all do
    @my_object = MyObject.new
  end

  controller_names.product(users,accounts).each do |cn, u, a|
    expected_result = allowed.grep([cn, u, a]).size > 0
    it "should return #{expected_result} for controller:#{cn} user:#{u.name} account:#{a.name} combo" do
      result = @my_object.should_display(cn, a, u)
      result.should == expected_result
    end
  end
end

To run this, here's some fake business logic:

class MyObject;
  def should_display(controller_name, account, user)
    controller_name == 'Dashboard' && 
      account.has_feature(:secret_widget) && 
      user.can_access(:secret_feature)
  end
end
class Base < Struct.new(:name,:features); 
  def has?(feature)
    [*features].grep(feature).size > 0
  end
end
class Acc < Base; alias has_feature has? end

class User < Base; alias can_access has? end
like image 184
inger Avatar answered Sep 17 '25 19:09

inger