######################################################################## # The Idea: # # This is supposed to get us thinking about the various dimensions our # testing should address. If there are states orthogonal to each other # (eg. readable vs unreadable, logged in vs not logged in) each of # those states should comprise a dimension in the matrix. By # addressing it this way, we should be able to minimize the amount of # setup/teardown code and get full coverage across our actions for all # these edge cases and as a result have extremely clear tests. # ######################################################################## # Example Test Matrix Specification: # # matrix :example, :edge1, :edge2, :edge3, ... # action :action1, :OK, :e_NF, :mod, ... # action :action2, :OK, :e_RO, :na, ... # action ... # ######################################################################## # Matrix: # # I envision the setups being a code that combines the different # dimensions of edge case state. # # Something for a CMS might look like: `[df]_[ugo]_[rRwW]` where: # # + `[df]` for dir/file. # + and the rest is in the style of symbolic args to chmod: # + u/g/o = user, group, or other # + lowercase `X` == `X`able, uppercase `X` == un`X`able, where `X` # is read/write. # ######################################################################## # Action: # # :new/:err/:del are just examples, they should have semantic info # attached to them. # # Use :na to specify an inapplicable edge case for that action. # # Use :OK to specify the standard positive state. It is equivalent to # a result with the same name as the action. (eg # matrix_test_index). This cleans up the matrix a lot and allows for # narrower and more readable columns. # # Edge cases specific to an action that fall outside the matrix are # regular tests. # ######################################################################## # Matrix Methods (the legos): # # Everything else basically equates to lego pieces: # # + To init some context # def setup # init(:logined){ login_as :dongbin} # end # + There is one "setup" method per action: matrix_setup_#{action}(setup, expect) # + There is one "test" method per result: matrix_test_#{result}(setup) # # Thus, for the matrix "example" above, the top left-most test will # end up calling: # login_as :dongbin # matrix_setup_action1(:edge1, :new) # matrix_test_new(:edge1) # # Read the action method for exact details. ######################################################################## module FunctionalTestMatrix def self.included(klass) super klass.extend ClassMethods end def init(setup, &action) @init_action ||= { } @init_action[setup] = action end module ClassMethods def matrix(name, *setups) @@matrix, @@setups, @@after_filter = name, setups, { } end def after_filter(action, *expected) @@after_filter ||= { } expected.each do |expect| @@after_filter[expect] ||= [] @@after_filter[expect] << action end end def action(action, *results) testcases = @@setups.zip(results).reject { |a,b| b == :na } testcases = Hash[*testcases.flatten] matrix = @@matrix # bind to local scope for define_method closure testcases.each do |setup, expected| expected_action = expected == :OK ? action : expected define_method "test_#{matrix}_#{action}_#{setup}" do @action = action @setup = setup setup_array = setup.to_s.split(/_/).map {|c| c.intern } setup_array.each do |s| @init_action[s].call if @init_action[s] end send "matrix_setup_#{action}" send "matrix_test_#{expected_action}" after_filter = @@after_filter[expected_action] || [] after_filter.each do |after| send "matrix_test_#{after}" end end end end module_function :matrix, :action end end