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