rast 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +15 -0
- data/CHANGELOG.md +14 -0
- data/Documentation.md +297 -0
- data/Gemfile +0 -2
- data/Getting-Started-Detailed.md +122 -0
- data/Getting-Started.md +19 -102
- data/README.md +80 -16
- data/examples/enum_module.rb +20 -6
- data/examples/factory_example.rb +4 -4
- data/examples/hotel_finder.rb +14 -0
- data/examples/person.rb +6 -0
- data/examples/prime_number.rb +13 -8
- data/lib/rast/parameter_generator.rb +107 -53
- data/lib/rast/rast_spec.rb +8 -2
- data/lib/rast/rules/logic_helper.rb +76 -95
- data/lib/rast/rules/rule_evaluator.rb +100 -98
- data/lib/rast/rules/rule_validator.rb +14 -7
- data/lib/rast/rules/token_util.rb +17 -0
- data/lib/rast/spec_dsl.rb +57 -35
- data/lib/rast.rb +5 -1
- data/lib/template_spec.yml +5 -7
- data/rast.gemspec +1 -1
- metadata +8 -9
- data/examples/arithmetic_module.rb +0 -8
- data/examples/double_example.rb +0 -14
- data/examples/logic_four.rb +0 -15
- data/examples/lohika.rb +0 -27
- data/examples/phone.rb +0 -6
- data/examples/quiz_module.rb +0 -34
- data/examples/triple.rb +0 -15
data/README.md
CHANGED
@@ -2,33 +2,97 @@
|
|
2
2
|
|
3
3
|
RSpec All Scenario Testing
|
4
4
|
|
5
|
-
|
5
|
+
[](https://badge.fury.io/rb/rast)
|
6
|
+
[](https://travis-ci.com/roycetech/rast)
|
7
|
+
[](https://codeclimate.com/github/roycetech/rast/test_coverage)
|
8
|
+
[](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
|
-
|
12
|
+
### A Basic Example
|
9
13
|
|
10
|
-
|
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
|
-
|
86
|
+
### Releasing new features/bugfix
|
27
87
|
|
28
88
|
- Increment the .gemspec
|
29
89
|
- Modify the CHANGELOG.md
|
30
90
|
|
31
|
-
|
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
|
-
|
34
|
-
Publish with `gem push <gem-filename>`
|
98
|
+
[Semantic Versioning](https://semver.org)
|
data/examples/enum_module.rb
CHANGED
@@ -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
|
6
|
+
return false unless valid?(back_card)
|
7
7
|
|
8
|
-
return true if
|
8
|
+
return true if ordered_or_headered?(back_card)
|
9
9
|
|
10
|
-
|
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
|
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
|
data/examples/factory_example.rb
CHANGED
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
# Sample module
|
4
4
|
module FactoryExample
|
5
|
-
def
|
6
|
-
return @
|
7
|
-
return @
|
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
|
-
@
|
9
|
+
@person.first_name
|
10
10
|
end
|
11
11
|
end
|
data/examples/person.rb
ADDED
data/examples/prime_number.rb
CHANGED
@@ -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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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 =
|
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
|
48
|
-
return
|
51
|
+
def expand_variables(variables)
|
52
|
+
return nil unless variables
|
49
53
|
|
50
|
-
|
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
|
-
|
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
|
73
|
+
return include_result if no_include_or_dont_include?(spec, include_result)
|
61
74
|
|
62
|
-
|
63
|
-
|
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
|
-
|
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[:
|
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 |
|
103
|
-
next if
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
137
|
-
|
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
|
-
|
141
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
171
|
-
|
172
|
-
|
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
|
-
|
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)
|
data/lib/rast/rast_spec.rb
CHANGED
@@ -12,7 +12,12 @@ class RastSpec
|
|
12
12
|
|
13
13
|
attr_accessor :exclude
|
14
14
|
|
15
|
-
def initialize(
|
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] ||
|
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
|