rast 0.18.0 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -2,33 +2,97 @@
2
2
 
3
3
  RSpec All Scenario Testing
4
4
 
5
- This library runs on top of RSpec to provide basically a parameterized unit testing pattern. It follows a specific pattern of writing unit tests, enabling a predictable, complete and easy to analyze report result.
5
+ [![Gem Version](https://badge.fury.io/rb/rast.svg)](https://badge.fury.io/rb/rast)
6
+ [![Build Status](https://travis-ci.com/roycetech/rast.svg?branch=master)](https://travis-ci.com/roycetech/rast)
7
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/280a80e7e03350b7a3d3/test_coverage)](https://codeclimate.com/github/roycetech/rast/test_coverage)
8
+ [![Maintainability](https://api.codeclimate.com/v1/badges/280a80e7e03350b7a3d3/maintainability)](https://codeclimate.com/github/roycetech/rast/maintainability)
6
9
 
10
+ This library runs on top of RSpec to provide basically a parameterized unit testing pattern. It follows a specific pattern of writing unit tests, enabling a predictable, complete and outputs a result that is simple to analyze.
7
11
 
8
- ## Definition of terms
12
+ ### A Basic Example
9
13
 
10
- `spec` - as defined in the yaml file, the individual elements under `specs`
11
- `scenario` - a specific combination of tokens from vars, it can uniquely identify a fixture.
12
- `fixture` - instance of a spec, containing a scenario, reference back to the spec, and the expected result for the given scenario.
13
- `variables` - raw list of variables to be combined into multiple fixtures.
14
- `rule` - set of outcome paired with rule clause.
15
- `exemption/exclusions` - rule defining variable combinations to be excluded from the test.
16
- `outcome` - the left portion of a rule e.g. `true: true&true`
17
- `clause` - the right portion of a rule
18
- `token` - used loosely to denote the individual variable in a rule. e.g. `true: you & me`, 'you' and 'me' are tokens.
14
+ Suppose we want to create a class that checks if a number is a positive number or not.
19
15
 
16
+ #### Create a spec file `spec/positive_spec.rb`
20
17
 
21
- ##
18
+ ```ruby
19
+ require 'rast'
20
+
21
+ rast Positive do
22
+ spec 'Is Positive Example' do
23
+ execute { |number| subject.positive?(number) }
24
+ end
25
+ end
26
+ ```
27
+
28
+ #### Create a spec configuration `spec/rast/positive_spec.yml`
29
+
30
+ ```yaml
31
+ specs:
32
+ Is Positive Exaple:
33
+ variables: {number: [-1, 0, 1]}
34
+ outcomes: {true: 1}
35
+ ```
36
+
37
+ The class to test:
38
+
39
+ ```ruby
40
+ # positive.rb
41
+ class Positive
42
+ def positive?(number)
43
+ number > 0
44
+ end
45
+ end
46
+ ```
47
+
48
+ Running the test:
49
+
50
+ `$ rspec -fd spec/examples/positive_spec.rb`
51
+
52
+ Test result:
53
+
54
+ ```
55
+ Positive: #positive?
56
+ [false]=[number: -1]
57
+ [false]=[number: 0]
58
+ [true]=[number: 1]
59
+
60
+ Finished in 0.00471 seconds (files took 0.47065 seconds to load)
61
+ 3 examples, 0 failures
62
+ ```
63
+
64
+ Read the [documentation](./Documentation.md) for more examples.
65
+
66
+ ## Contributing
67
+
68
+ ### Definition of terms
69
+
70
+ - `spec` - as defined in the yaml file, the individual elements under `specs`
71
+ - `scenario` - a specific combination of tokens from vars, it can uniquely identify a fixture.
72
+ - `token` - used loosely to denote the individual variable in a rule. e.g. `true: you & me`, `you` and `me` are tokens.
73
+ - `fixture` - a hash containing a scenario, reference back to the spec, and the expected result for the given scenario.
74
+ - `variables` - raw list of variables to be combined into multiple fixtures.
75
+ - `rule` - set of outcomes, each paired with rule clause.
76
+ - `exclusions` - rule defining variable combinations to be excluded from the test.
77
+ - `inclusions` - rule that limits the scenarios to be included. Useful for isolating test cases.
78
+ - `outcome` - the left portion `us` of a rule e.g. `us: you&me`
79
+ - `clause` - the right portion `you&me` of a rule e.g. `us: you&me`
80
+
81
+ ## Notes to author
22
82
 
23
83
  When running the tests, the execution starts at the spec file, then invoking the
24
84
  DSL. The DSL will then invoke the parameter generator to generate the scenarios.
25
85
 
26
- ## Adding new features
86
+ ### Releasing new features/bugfix
27
87
 
28
88
  - Increment the .gemspec
29
89
  - Modify the CHANGELOG.md
30
90
 
31
- ## Releasing GEM
91
+ ### Releasing GEM
92
+
93
+ - Build gem with `gem build rast.gemspec`
94
+ - Publish with `gem push <gem-filename>`
95
+
96
+ ## References
32
97
 
33
- Build gem with `gem build rast.gemspec`
34
- Publish with `gem push <gem-filename>`
98
+ [Semantic Versioning](https://semver.org)
@@ -3,13 +3,11 @@
3
3
  # Functions for detecting an enum.
4
4
  module EnumModule
5
5
  def enum?(back_card)
6
- return false unless back_card.is_a?(Array) && back_card.any?
6
+ return false unless valid?(back_card)
7
7
 
8
- return true if ordered?(back_card) || enum_with_header?(back_card)
8
+ return true if ordered_or_headered?(back_card)
9
9
 
10
- back_card.each { |element| return false unless element[/^[-+*]\s.*/] }
11
-
12
- return false unless same_prefix?(back_card)
10
+ return false if detect_non_ol(back_card) || !same_prefix?(back_card)
13
11
 
14
12
  true
15
13
  end
@@ -24,10 +22,26 @@ module EnumModule
24
22
 
25
23
  private
26
24
 
25
+ def valid?(back_card)
26
+ back_card.is_a?(Array) && back_card.any?
27
+ end
28
+
29
+ def ordered_or_headered?(back_card)
30
+ ordered?(back_card) || enum_with_header?(back_card)
31
+ end
32
+
33
+ def detect_non_ol(back_card)
34
+ back_card.each { |element| return true unless element[/^[-+*]\s.*/] }
35
+
36
+ false
37
+ end
38
+
27
39
  def enum_with_header?(back_card)
28
40
  return false if back_card.first[/^[-+*]\s.*/]
29
41
 
30
- back_card[1..back_card.size].each { |element| return false unless element[/^[-+*]\s.*/] }
42
+ back_card[1..back_card.size].each do |element|
43
+ return false unless element[/^[-+*]\s.*/]
44
+ end
31
45
 
32
46
  true
33
47
  end
@@ -2,10 +2,10 @@
2
2
 
3
3
  # Sample module
4
4
  module FactoryExample
5
- def phone_plan_name
6
- return @phone.third_name if @phone.third_name
7
- return @phone.second_name if @phone.second_name
5
+ def person_name
6
+ return @person.alias_name if @person.alias_name
7
+ return @person.last_name if @person.last_name
8
8
 
9
- @phone.first_name
9
+ @person.first_name
10
10
  end
11
11
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Example
4
+ class HotelFinder
5
+ def aircon; end
6
+
7
+ def security; end
8
+
9
+ def applicable?
10
+ return false if aircon.nil? || security.nil?
11
+
12
+ aircon.operational? && security.grade == :diplomat
13
+ end
14
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ class Person
5
+ attr_accessor :first_name, :last_name, :alias_name
6
+ end
@@ -3,17 +3,22 @@
3
3
  # Example that returns true if the number passed is a prime number
4
4
  class PrimeNumber
5
5
  def prime?(number)
6
- is_prime = true
7
6
  raise "Invalid number: #{number}" if number <= 0
8
7
 
9
- if number == 1
10
- is_prime = false
11
- elsif number > 2
12
- (2...number).each do |i|
13
- is_prime = number % i != 0
14
- break unless is_prime
15
- end
8
+ return false if number == 1
9
+
10
+ compute_prime(number)
11
+ end
12
+
13
+ private
14
+
15
+ def compute_prime(number)
16
+ is_prime = true
17
+ (2...number).each do |i|
18
+ is_prime = number % i != 0
19
+ break unless is_prime
16
20
  end
21
+
17
22
  is_prime
18
23
  end
19
24
  end
@@ -21,18 +21,22 @@ class ParameterGenerator
21
21
 
22
22
  spec_config = @specs_config[spec_id]
23
23
 
24
+ raise "Spec not found for: #{spec_id}. Check yaml file." if spec_config.nil?
25
+
24
26
  spec_config[:description] = spec_id
25
27
 
26
28
  # Keep, for backwards compatibility
27
29
  spec_config['rules'] ||= spec_config['outcomes']
28
30
  spec_config['default'] ||= spec_config['else']
29
31
 
30
- spec = instantiate_spec(spec_config)
32
+ spec_config['variables'] = expand_variables(spec_config['variables'])
33
+
34
+ spec = build_spec(spec_config)
31
35
 
32
36
  list = []
33
37
 
34
38
  variables = spec.variables
35
- var_first = spec.variables.first
39
+ var_first = variables.first
36
40
  multipliers = []
37
41
 
38
42
  (1...variables.size).each { |i| multipliers << variables.values[i].dup }
@@ -44,26 +48,52 @@ class ParameterGenerator
44
48
 
45
49
  private
46
50
 
47
- def valid_case?(scenario, spec)
48
- return true if spec.exclude_clause.nil? && spec.include_clause.nil?
51
+ def expand_variables(variables)
52
+ return nil unless variables
49
53
 
50
- rule_evaluator = RuleEvaluator.new(converters: spec.converters)
54
+ expanded_variables = {}
55
+ variables.each do |key, tokens|
56
+ expanded_variables[key] = if tokens == 'boolean'
57
+ [false, true]
58
+ else
59
+ tokens
60
+ end
61
+ end
62
+ expanded_variables
63
+ end
64
+
65
+ def valid_case?(scenario, spec)
66
+ return true unless with_optional_clause?(spec)
51
67
 
52
68
  include_result = true
53
69
  unless spec.exclude_clause.nil?
54
- exclude_clause = Rule.sanitize(clause: spec.exclude_clause)
55
- rule_evaluator.parse(expression: exclude_clause)
56
- evaluate_result = rule_evaluator.evaluate(scenario: scenario, rule_token_convert: spec.token_converter)
57
- include_result = evaluate_result == 'false'
70
+ include_result = qualify_secario?(spec, scenario, false)
58
71
  end
59
72
 
60
- return include_result if spec.include_clause.nil? || !include_result
73
+ return include_result if no_include_or_dont_include?(spec, include_result)
61
74
 
62
- include_clause = Rule.sanitize(clause: spec.include_clause)
63
- rule_evaluator.parse(expression: include_clause)
64
- include_result = rule_evaluator.evaluate(scenario: scenario, rule_token_convert: spec.token_converter) == "true"
75
+ qualify_secario?(spec, scenario, true)
76
+ end
65
77
 
66
- include_result
78
+ # blech!
79
+ def no_include_or_dont_include?(spec, include_result)
80
+ spec.include_clause.nil? || !include_result
81
+ end
82
+
83
+ def qualify_secario?(spec, scenario, is_included)
84
+ action = is_included ? 'include' : 'exclude'
85
+ rule_evaluator = RuleEvaluator.new(converters: spec.converters)
86
+ clause = Rule.sanitize(clause: spec.send("#{action}_clause"))
87
+ rule_evaluator.parse(expression: clause)
88
+ rule_evaluator.evaluate(
89
+ scenario: scenario,
90
+ rule_token_convert: spec.token_converter
91
+ ) == is_included.to_s
92
+ end
93
+
94
+ # Has an exclude or include clause
95
+ def with_optional_clause?(spec)
96
+ !spec.exclude_clause.nil? || !spec.include_clause.nil?
67
97
  end
68
98
 
69
99
  # add all fixtures to the list.
@@ -89,18 +119,15 @@ class ParameterGenerator
89
119
  param[:scenario][var_name] = var_value
90
120
  end
91
121
 
92
- param[:expected_outcome] = validator.validate(
93
- scenario: scenario,
94
- fixture: param
95
- )
122
+ param[:expected] = validator.validate(scenario: scenario, fixture: param)
96
123
 
97
124
  param
98
125
  end
99
126
 
100
127
  # Detects if rule config has one outcome to one token mapping.
101
128
  def one_to_one(outcome_to_clause)
102
- outcome_to_clause.each do |outcome, clause|
103
- next if clause.is_a?(Array) && clause.size == 1
129
+ outcome_to_clause.each do |_outcome, clause|
130
+ next if outcome_to_one_array?(clause)
104
131
 
105
132
  return false if RuleEvaluator.tokenize(clause: clause).size > 1
106
133
  end
@@ -108,53 +135,69 @@ class ParameterGenerator
108
135
  true
109
136
  end
110
137
 
111
- # Used to optimize by detecting the variables if rules config is a 1 outcome to 1 rule token.
138
+ def outcome_to_one_array?(clause)
139
+ clause.is_a?(Array) && clause.size == 1
140
+ end
141
+
142
+ # Used to optimize by detecting the variables if rules config is a 1 outcome
143
+ # to 1 rule token.
112
144
  def detect_variables(spec_config)
113
145
  return nil unless one_to_one(spec_config['rules'])
114
146
 
115
147
  tokens = spec_config['rules'].values
116
- return { vars: tokens.map(&:first) } if tokens.first.is_a?(Array) && tokens.first.size == 1
148
+ if tokens.first.is_a?(Array) && tokens.first.size == 1
149
+ return { vars: tokens.map(&:first) }
150
+ end
117
151
 
118
152
  { vars: spec_config['rules'].values }
119
153
  end
120
154
 
121
155
  def instantiate_spec(spec_config)
122
- if spec_config['variables'].nil?
123
- spec_config['variables'] = detect_variables(spec_config)
124
- end
125
-
126
- spec = RastSpec.new(
156
+ RastSpec.new(
127
157
  description: spec_config[:description],
128
158
  variables: spec_config['variables'],
129
159
  rule: Rule.new(rules: spec_config['rules']),
130
160
  default_outcome: spec_config['default'] || spec_config['else']
131
161
  )
162
+ end
132
163
 
164
+ def build_spec(spec_config)
165
+ if spec_config['variables'].nil?
166
+ spec_config['variables'] = detect_variables(spec_config)
167
+ end
168
+
169
+ spec = instantiate_spec(spec_config)
133
170
  pair_config = calculate_pair(spec_config)
134
171
  spec.init_pair(pair_config: pair_config) unless pair_config.nil?
135
172
 
136
- unless spec_config['exclude'].nil?
137
- spec.init_exclusion(spec_config['exclude'])
173
+ configure_include_exclude(spec, spec_config)
174
+ spec.init_converters(converters: generate_converters(spec_config))
175
+ end
176
+
177
+ def generate_converters(spec_config)
178
+ converters_config = spec_config['converters']
179
+ return converters unless converters_config.nil?
180
+
181
+ # when no converters defined, we detect if type is consistent, otherwise
182
+ # assume it's string.
183
+ default_converter = DefaultConverter.new
184
+ spec_config['variables'].map do |_key, array|
185
+ if same_data_type(array)
186
+ RuleEvaluator::DEFAULT_CONVERT_HASH[array.first.class]
187
+ else
188
+ default_converter
189
+ end
138
190
  end
191
+ end
139
192
 
140
- unless spec_config['include'].nil?
141
- spec.init_inclusion(spec_config['include'])
193
+ def configure_include_exclude(spec, spec_config)
194
+ unless spec_config['exclude'].nil?
195
+ spec.init_exclusion(spec_config['exclude'])
142
196
  end
143
197
 
144
- converters_config = spec_config['converters']
145
- converters = if converters_config.nil?
146
- # when no converters defined, we detect if type is consistent, otherwise assume it's string.
147
- default_converter = DefaultConverter.new
148
- spec_config['variables'].map do |_key, array|
149
- if same_data_type(array)
150
- RuleEvaluator::DEFAULT_CONVERT_HASH[array.first.class]
151
- else
152
- default_converter
153
- end
154
- end
155
- end
198
+ return if spec_config['include'].nil?
156
199
 
157
- spec.init_converters(converters: converters)
200
+ spec.init_inclusion(spec_config['include'])
158
201
  end
159
202
 
160
203
  def calculate_pair(spec_config)
@@ -162,19 +205,30 @@ class ParameterGenerator
162
205
  return pair_config unless pair_config.nil?
163
206
 
164
207
  outcomes = spec_config['rules'].keys
165
- if outcomes.size == 1
166
- if [TrueClass, FalseClass].include?(outcomes.first.class)
167
- return { outcomes.first => !outcomes.first }
168
- end
208
+ return {} unless outcomes.size == 1
169
209
 
170
- if %w[true false].include?(outcomes.first)
171
- return { outcomes.first => outcomes.first == 'true' ? 'false' : 'true' }
172
- end
210
+ boolean_pair = boolean_pair(outcomes, spec_config)
211
+ return boolean_pair if boolean_pair
212
+
213
+ default_pair(spec_config)
214
+ end
215
+
216
+ def default_pair(spec_config)
217
+ outcomes = spec_config['rules'].keys
218
+ { outcomes.first => spec_config['default'] } if spec_config['default']
219
+ end
220
+
221
+ # refactored out of calculate_pair.
222
+ def boolean_pair(outcomes, spec_config)
223
+ return false if spec_config['default']
173
224
 
174
- return { outcomes.first => spec_config['default'] } if spec_config['default']
225
+ if [TrueClass, FalseClass].include?(outcomes.first.class)
226
+ return { outcomes.first => !outcomes.first }
175
227
  end
176
228
 
177
- {}
229
+ return unless %w[true false].include?(outcomes.first)
230
+
231
+ { outcomes.first => (outcomes.first != 'true').to_s }
178
232
  end
179
233
 
180
234
  def same_data_type(array)
@@ -12,7 +12,12 @@ class RastSpec
12
12
 
13
13
  attr_accessor :exclude
14
14
 
15
- def initialize(description: '', variables: [][], rule: nil, default_outcome: '')
15
+ def initialize(
16
+ description: '',
17
+ variables: [][],
18
+ rule: nil,
19
+ default_outcome: ''
20
+ )
16
21
  @description = description
17
22
  @variables = variables
18
23
  @pair = {}
@@ -37,7 +42,8 @@ class RastSpec
37
42
 
38
43
  @variables.keys.each_with_index do |key, index|
39
44
  @variables[key].each do |element|
40
- converter = RuleEvaluator::DEFAULT_CONVERT_HASH[element.class] || converters[index]
45
+ converter = RuleEvaluator::DEFAULT_CONVERT_HASH[element.class] ||
46
+ converters[index]
41
47
  @token_converter[element.to_s] = converter
42
48
  end
43
49
  end