lib/lopata/scenario_builder.rb in lopata-0.1.6 vs lib/lopata/scenario_builder.rb in lopata-0.1.7

- old
+ new

@@ -1,21 +1,41 @@ +# Context for scenario creation. class Lopata::ScenarioBuilder + # @private attr_reader :title, :common_metadata, :options, :diagonals + # @private attr_accessor :shared_step, :group + # Defines one or more scenarios. + # + # @example + # Lopata.define 'scenario' do + # setup 'test user' + # action 'login' + # verify 'home page displayed' + # end + # + # Given block will be calculated in context of the ScenarioBuilder + # + # @param title [String] scenario unique title + # @param metadata [Hash] metadata to be used within the scenario + # @param block [Block] the scenario definition + # @see Lopata.define def self.define(title, metadata = {}, &block) builder = new(title, metadata) builder.instance_exec &block builder.build end + # @private def initialize(title, metadata = {}) @title, @common_metadata = title, metadata @diagonals = [] @options = [] end + # @private def build filters = Lopata.configuration.filters option_combinations.each do |option_set| metadata = common_metadata.merge(option_set.metadata) scenario = Lopata::Scenario::Execution.new(title, option_set.title, metadata) @@ -31,25 +51,108 @@ world.scenarios << scenario end end + # @!group Defining variants + + # Define option for the scenario. + # + # The scenario will be generated for all the options. + # If more then one option given, the scenarios for all options combinations will be generated. + # + # @param metadata_key [Symbol] the key to access option data via metadata. + # @param variants [Hash{String => Object}] variants for the option + # Keys are titles of the variant, values are metadata values. + # + # @example + # Lopata.define 'scenario' do + # option :one, 'one' => 1, 'two' => 2 + # option :two, 'two' => 2, 'three' => 3 + # # will generate 4 scenarios: + # # - 'scenario one two' + # # - 'scenario one three' + # # - 'scenario two two' + # # - 'scenario two three' + # end + # + # @see #diagonal + def option(metadata_key, variants) + @options << Option.new(metadata_key, variants) + end + + # Define diagonal for the scenario. + # + # The scenario will be generated for all the variants of the diagonal. + # Each variant of diagonal will be selected for at least one scenario. + # It may be included in more then one scenario when other diagonal or option has more variants. + # + # @param metadata_key [Symbol] the key to access diagonal data via metadata. + # @param variants [Hash{String => Object}] variants for the diagonal. + # Keys are titles of the variant, values are metadata values. + # + # @example + # Lopata.define 'scenario' do + # option :one, 'one' => 1, 'two' => 2 + # diagonal :two, 'two' => 2, 'three' => 3 + # diagonal :three, 'three' => 3, 'four' => 4, 'five' => 5 + # # will generate 3 scenarios: + # # - 'scenario one two three' + # # - 'scenario two three four' + # # - 'scenario one two five' + # end + # + # @see #option + def diagonal(metadata_key, variants) + @diagonals << Diagonal.new(metadata_key, variants) + end + + # Define additional metadata for the scenario + # + # @example + # Lopata.define 'scenario' do + # metadata key: 'value' + # it 'metadata available' do + # expect(metadata[:key]).to eq 'value' + # end + # end def metadata(hash) raise 'metadata expected to be a Hash' unless hash.is_a?(Hash) @common_metadata ||= {} @common_metadata.merge! hash end + # Skip scenario for given variants combination + # + # @example + # Lopata.define 'multiple options' do + # option :one, 'one' => 1, 'two' => 2 + # option :two, 'two' => 2, 'three' => 3 + # skip_when { |opt| opt.metadata[:one] == opt.metadata[:two] } + # it 'not equal' do + # expect(one).to_not eq two + # end + # end + # def skip_when(&block) @skip_when = block end + # @private def skip?(option_set) @skip_when && @skip_when.call(option_set) end - %i{ setup action it teardown verify context }.each do |name| + # @!endgroup + + # @!group Defining Steps + + # @private + # @macro [attach] define_step_method + # @!scope instance + # @method $1 + def self.define_step_method(name) name_if = "%s_if" % name name_unless = "%s_unless" % name define_method name, ->(*args, **metadata, &block) { add_step(name, *args, metadata: metadata, &block) } define_method name_if, ->(condition, *args, **metadata, &block) { add_step(name, *args, metadata: metadata, condition: Lopata::Condition.new(condition), &block) @@ -57,10 +160,128 @@ define_method name_unless, ->(condition, *args, **metadata, &block) { add_step(name, *args, condition: Lopata::Condition.new(condition, positive: false), metadata: metadata, &block) } end + # Define setup step. + # @example + # setup do + # end + # + # # setup from named shared step + # setup 'create user' + # + # # setup with both shared step and code block + # setup 'create user' do + # @user.update(admin: true) + # end + # + # Setup step used for set test data. + # @overload setup(*steps, &block) + # @param steps [Array<String, Symbol, Proc>] the steps to be runned as a part of setup. + # String - name of shared step to be called. + # Symbol - metadata key, referenced to shared step name. + # Proc - in-place step implementation. + # @param block [Block] The implementation of the step. + define_step_method :setup + + # Define action step. + # @example + # action do + # end + # + # # action from named shared step + # action 'login' + # + # # setup with both shared step and code block + # action 'login', 'go dashboard' do + # @user.update(admin: true) + # end + # + # Action step is used for emulate user or external system action + # + # @overload action(*steps, &block) + # @param steps [Array<String, Symbol, Proc>] the steps to be runned as a part of action. + # String - name of shared step to be called. + # Symbol - metadata key, referenced to shared step name. + # Proc - in-place step implementation. + # @param block [Block] The implementation of the step. + define_step_method :action + + # Define teardown step. + # @example + # setup { @user = User.create! } + # teardown { @user.destroy } + # Teardown step will be called at the end of scenario running. + # But it suggested to be decared right after setup or action step which require teardown. + # + # @overload teardown(*steps, &block) + # @param steps [Array<String, Symbol, Proc>] the steps to be runned as a part of teardown. + # String - name of shared step to be called. + # Symbol - metadata key, referenced to shared step name. + # Proc - in-place step implementation. + # @param block [Block] The implementation of the step. + define_step_method :teardown + + # Define verify steps. + # @example + # verify 'home page displayed' # call shared step. + # Usually for validation shared steps inclusion + # + # @overload verify(*steps, &block) + # @param steps [Array<String, Symbol, Proc>] the steps to be runned as a part of verification. + # String - name of shared step to be called. + # Symbol - metadata key, referenced to shared step name. + # Proc - in-place step implementation. + # @param block [Block] The implementation of the step. + define_step_method :verify + + # Define group of steps. + # The metadata for the group may be overriden + # @example + # context 'the task', task: :created do + # verify 'task setup' + # it 'created' do + # expect(metadata[:task]).to eq :created + # end + # end + # Teardown steps within group will be called at the end of the group, not scenario + # @overload context(title, **metadata, &block) + # @param title [String] context title + # @param metadata [Hash] the step additional metadata + # @param block [Block] The implementation of the step. + define_step_method :context + + # Define single validation step. + # @example + # it 'works' do + # expect(1).to eq 1 + # end + # @overload it(title, &block) + # @param title [String] validation title + # @param block [Block] The implementation of the step. + define_step_method :it + + # Define runtime method for the scenario. + # + # @note + # The method to be called via #method_missing, so it wont override already defined methods. + # + # @example + # let(:square) { |num| num * num } + # it 'calculated' do + # expect(square(4)).to eq 16 + # end + def let(method_name, &block) + steps << Lopata::Step.new(:let) do + execution.let(method_name, &block) + end + end + + # @!endgroup + + # @private def add_step(method_name, *args, condition: nil, metadata: {}, &block) step_class = case method_name when /^(setup|action|teardown|verify)/ then Lopata::ActionStep when /^(context)/ then Lopata::GroupStep @@ -69,14 +290,16 @@ step = step_class.new(method_name, *args, condition: condition, shared_step: shared_step, &block) step.metadata = metadata steps << step end + # @private def steps @steps ||= [] end + # @private def steps_with_hooks s = [] unless Lopata.configuration.before_scenario_steps.empty? s << Lopata::ActionStep.new(:setup, *Lopata.configuration.before_scenario_steps) end @@ -88,32 +311,20 @@ end s end - def let(method_name, &block) - steps << Lopata::Step.new(:let) do - execution.let(method_name, &block) - end - end - - def option(metadata_key, variants) - @options << Option.new(metadata_key, variants) - end - - def diagonal(metadata_key, variants) - @diagonals << Diagonal.new(metadata_key, variants) - end - + # @private def option_combinations combinations = combine([OptionSet.new], options + diagonals) while !diagonals.all?(&:complete?) combinations << OptionSet.new(*(options + diagonals).map(&:next_variant)) end combinations.reject { |option_set| skip?(option_set) } end + # @private def combine(source_combinations, rest_options) return source_combinations if rest_options.empty? combinations = [] current_option = rest_options.shift source_combinations.each do |source_variants| @@ -122,51 +333,62 @@ end end combine(combinations, rest_options) end + # @private def world Lopata.world end - # Набор вариантов, собранный для одного теста + # Set of options for scenario class OptionSet + # @private attr_reader :variants + + # @private def initialize(*variants) @variants = {} variants.compact.each { |v| self << v } end + # @private def +(other_set) self.class.new(*@variants.values).tap do |sum| other_set.each { |v| sum << v } end end + # @private def <<(variant) @variants[variant.key] = variant end + # @private def [](key) @variants[key] end + # @private def each(&block) @variants.values.each(&block) end + # @private def title @variants.values.map(&:title).compact.join(' ') end + # @return [Hash{Symbol => Object}] metadata for this option set def metadata @variants.values.inject({}) do |metadata, variant| metadata.merge(variant.metadata(self)) end end end + # @private class Variant attr_reader :key, :title, :value, :option def initialize(option, key, title, value) @option, @key, @title, @value = option, key, title, check_lambda_arity(value) @@ -211,20 +433,22 @@ v end end end + # @private class CalculatedValue def initialize(&block) @proc = block end def calculate(option_set) @proc.call(option_set) end end + # @private class Option attr_reader :variants, :key def initialize(key, variants) @key = key @variants = @@ -256,9 +480,10 @@ @available_metadata_keys ||= variants .map(&:value).select { |v| v.is_a?(Hash) }.flat_map(&:keys).map { |k| "#{key}_#{k}".to_sym }.uniq end end + # @private class Diagonal < Option def level_variants [next_variant] end