rast 0.18.0 → 0.19.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![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
|
-
|
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
|