test_spec 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e5f6724300184d3600f08828c91a0d61a21208aa
4
- data.tar.gz: 896de6158af5a26a3a288d9297867e03db771a19
3
+ metadata.gz: e2a3e183e6720aca0feee09a37aa7c46523b4108
4
+ data.tar.gz: 6d71391ab4677b7ece78333417a8a12f6aa857c4
5
5
  SHA512:
6
- metadata.gz: fec3edcf80e74b76bb0d262df4aadd56a5470790fa32653bb176022675fa01736553dd34d7f5079f9ab188ecf1c603588259c091aa23340168912c1aa41906bf
7
- data.tar.gz: cdc157ed2065d4b9f58ccfb186128c88f1bdb28c819801f0a149d1d277b16f375f45a87d022327d71169b5621a29e01f58581268b03489f63d3a22dc78778d31
6
+ metadata.gz: b4602a79d42b6b133d49d3d5a1751760299b87dcdba3f9ddbf604cfab6a90580d6c170dd7daf8b2513d54ed410050365408366452743435dc01ca810280cb391
7
+ data.tar.gz: 8d576bdf2e726b3f2cd6f8b502d9502867955bb8fca1ab1235be912864fa49551472e04a6419d9565edab526d56d759d1df1fcf902ab26fc2e259b587170de4f
data/.gitignore CHANGED
@@ -11,6 +11,9 @@
11
11
  /spec/coverage/
12
12
  /tmp/
13
13
 
14
+ # Generated Reports
15
+ reports/
16
+
14
17
  # Rspec Failure Tracking
15
18
 
16
19
  .rspec_status
data/.hound.yml CHANGED
@@ -67,3 +67,13 @@ Metrics/ParameterLists:
67
67
  # Remove execute permissions check.
68
68
  Lint/ScriptPermission:
69
69
  Enabled: false
70
+
71
+ # Allow methods with has_ for predicates.
72
+ Naming/PredicateName:
73
+ NameWhitelist:
74
+ - has_failed_screenshot?
75
+ - has_exception?
76
+ - has_screenshots?
77
+ - has_screenrecord?
78
+ - has_spec?
79
+ - has_comment?
data/README.md CHANGED
@@ -60,7 +60,7 @@ Because TestSpec uses a custom formatter, you should have an `.rspec` file with
60
60
  --format RSpec::TestSpec::Formatter
61
61
  ```
62
62
 
63
- You can use RSpec constructs within Specify constructs although there are some things to be aware of. Here is an example:
63
+ You can use RSpec constructs within TestSpec constructs although there are some things to be aware of. Here is an example:
64
64
 
65
65
  ```ruby
66
66
  Feature 'Bank Accounts' do
@@ -68,7 +68,7 @@ Feature 'Bank Accounts' do
68
68
  subject { Account.new(valid_account_number) }
69
69
 
70
70
  Scenario 'starting a new account' do
71
- Test 'will have a starting balance of 0' do
71
+ test 'will have a starting balance of 0' do
72
72
  expect(subject.balance).to eq(0)
73
73
  end
74
74
 
@@ -79,7 +79,7 @@ Feature 'Bank Accounts' do
79
79
  end
80
80
  ```
81
81
 
82
- You can see that within the Feature construct I have let and subject elements. Within the Scenario you can see I use a Specify method (Test) and an RSpec method (it).
82
+ You can see that within the Feature construct I have let and subject elements. Within the Scenario you can see I use a TestSpec method (Test) and an RSpec method (it).
83
83
 
84
84
  ## Documentation
85
85
 
@@ -183,6 +183,171 @@ You can also see here that multiple **Scenario** blocks can be included within a
183
183
 
184
184
  This should give a rough idea of how TestSpec provides an internal DSL.
185
185
 
186
+ ### TestSpec API
187
+
188
+ The [unit tests](https://github.com/jeffnyman/test_spec/tree/master/spec) will give you some idea of how TestSpec works.
189
+
190
+ To use TestSpec as an RSpec overlay, you have **descriptive containers** with the following keywords:
191
+
192
+ * Feature, Ability, Story, Component, Workflow
193
+
194
+ These are defined in the [spec.rb](https://github.com/jeffnyman/test_spec/blob/master/lib/test_spec/spec.rb) file.
195
+
196
+ You have **example group sequences** with the following keywords:
197
+
198
+ * Scenario, Condition, Behavior
199
+ * Step, Test, Rule, Fact
200
+ * steps, tests, rules, facts
201
+
202
+ These are defined in the [test_spec.rb](https://github.com/jeffnyman/test_spec/blob/master/lib/test_spec.rb) file.
203
+
204
+ You have **example steps** with the following keywords:
205
+
206
+ * (Gherkin steps) Given, When, Then, And, But
207
+ * (RSpec steps) it, specify, example
208
+ * (Specify steps) step, test, rule, fact
209
+
210
+ These are defined in the [example_group.rb](https://github.com/jeffnyman/test_spec/blob/master/lib/test_spec/rspec/example_group.rb) file.
211
+
212
+ The API is basically this: **Containers contain example groups that contain example steps.** Everything must essentially be nested in that order. This allows you to follow just about every xSpec or xBehave pattern out there.
213
+
214
+ Some representative examples:
215
+
216
+ ```ruby
217
+ Feature 'some feature description' do
218
+ Scenario 'some test condition' do
219
+ Given 'some context' do
220
+ end
221
+
222
+ When 'some action' do
223
+ end
224
+
225
+ Then 'some observable' do
226
+ end
227
+ end
228
+ end
229
+ ```
230
+
231
+ ```ruby
232
+ Workflow 'some workflow description' do
233
+ Behavior 'some behavior description' do
234
+ example 'some test condition' do
235
+ end
236
+
237
+ example 'some other test condition' do
238
+ end
239
+ end
240
+ end
241
+ ```
242
+
243
+ ```ruby
244
+ Component 'some component name' do
245
+ rules 'some high-level rules condition' do
246
+ rule 'some specific rule test condition' do
247
+ end
248
+
249
+ rule 'some other specific rule test condition' do
250
+ end
251
+ end
252
+ end
253
+ ```
254
+
255
+ A few notes on this.
256
+
257
+ Example group sequences and example steps cannot be top level. Only descriptive containers can be the top level artifact in a test spec. Also, example steps cannot be directly under descriptive containers. For example, you can't do this:
258
+
259
+ ```ruby
260
+ Ability 'ability keyword' do
261
+ When 'when keyword' do
262
+ end
263
+ end
264
+ ```
265
+
266
+ Here the `When` (an example step) is under a descriptive container (`Ability`). But it needs to be within an example group (such as `Scenario`).
267
+
268
+ It's also worth noting that descriptive containers cannot be used inside example groups. For example, you can't do this:
269
+
270
+ ```ruby
271
+ Ability 'ability keyword' do
272
+ Scenario 'scenario keyword' do
273
+ Story 'story keyword' do
274
+ end
275
+ end
276
+ end
277
+ ```
278
+
279
+ Here the `Story` (a descriptive container) is used under a example group (`Scenario`). Descriptive containers, however, are top-level constructs. They provide a grouping method for one or more example groups.
280
+
281
+ You can nest descriptive containers if you feel that provides better understanding of your test spec. For example:
282
+
283
+ ```ruby
284
+ Feature 'feature keyword' do
285
+ Workflow 'workflow keyword' do
286
+ Component 'component #1' do
287
+ Scenario 'scenario' do
288
+ end
289
+ end
290
+
291
+ Component 'component #2' do
292
+ Scenario 'scenario' do
293
+ end
294
+ end
295
+ end
296
+ end
297
+ ```
298
+
299
+ Here notice that the containers `Feature`, `Workflow` and `Component` are used together to indicate a relationship about what is being tested. Each of the innermost containers then has some example groups (`Scenario` in this case) which can contain specific tests.
300
+
301
+ ## Design Rationale
302
+
303
+ TestSpec is a micro-framework.
304
+
305
+ A micro-framework provides a focused solution, which means it does one thing and one thing only, instead of trying to solve each and every problem. While doing that one thing it does well, the micro-framework should do it while being expressive and concise. Further, it should be able to serve as one component of your own custom modularized framework, allowing you to compose solutions.
306
+
307
+ ### Frameworks are Effective and Efficient
308
+
309
+ I believe that test solution frameworks require effective functionality, an efficient user experience, and demonstrable business impact. This all has to be delivered, deployed, and supported quickly and cost-effectively. You want the equivalent of a lower total cost of ownership and a faster time-to-market for whatever you produce (such as test artifacts).
310
+
311
+ ### Frameworks are Comfortable
312
+
313
+ Like a programming language, a framework needs to be something you're comfortable with -- something that reflects your personal style and mode of working. That's what TestSpec is for me.
314
+
315
+ TestSpec allows me to continue working with a Gherkin-like structure but putting my test specs closer to the code. Just as in RSpec, everything is in one place: the Ruby file. The benefit here is that you don't have to change the text in two places -- feature files and step definition files -- every time you change something. Further, there are no more matchers to sync up with natural language. TestSpec uses plain Ruby helper methods coupled with various patterns.
316
+
317
+ ### Frameworks Provide
318
+
319
+ A framework provides a few key aspects.
320
+
321
+ A place for everything: Structure and convention drive a good framework. Everything should have a proper place within the system; this eliminates guesswork and increases productivity.
322
+
323
+ A culture and aesthetic to help inform programming decisions: Rather than seeing the structure imposed by a framework as constraining, see it as liberating. A good framework encodes its opinions, gently guiding you. Often, difficult decisions are made for you by virtue of convention. The culture of the framework helps you make fewer menial decisions and helps you focus on what matters most.
324
+
325
+ ### Frameworks Have Qualities
326
+
327
+ I believe a test framework with good qualities will ...
328
+
329
+ * Encode opinions.
330
+ * Have an elegant, concise syntax.
331
+ * Have powerful metaprogramming features.
332
+ * Be well suited as a host language for creating DSLs.
333
+ * Allow for an open ecosystem.
334
+
335
+ ### TestSpec (Conceptually) Compared
336
+
337
+ Even though their domains are different, consider TestSpec in light of Rails. Rails is more than a programming framework for creating web applications. It's also a framework for thinking about web applications.
338
+
339
+ Rails ships not as a blank slate equally tolerant of every kind of expression. On the contrary, Rails trades that flexibility for the convenience of "what most people need most of the time to do most things."
340
+
341
+ You could argue that Rails is a designer straightjacket. Yet this straightjacket sets you free from focusing on the things that just don't matter and focuses your attention on the stuff that does. To be able to accept that trade, you need to understand not just how to do something in Rails, but also why it's done like that. Only by understanding the why will you be able to consistently work with the framework instead of against it.
342
+
343
+ It doesn't mean that you'll always have to agree with a certain choice, but you will need to agree to the overachieving principle of conventions. You have to learn to relax and let go of your attachment to personal idiosyncrasies when the productivity rewards are right.
344
+
345
+ So it is with TestSpec.
346
+
347
+ A framework goal is to solve 80% of the problems that occur in your testing domain, assuming that the remaining 20% are problems that are unique to the application's domain. This implies that 80% of the code in an application is infrastructure. So here instead of focusing on the details of knitting an application together, you get to focus on the 20% that really matters.
348
+
349
+ The framework lets you start right away by encompassing a set of intelligent decisions about how your logic should work and alleviating the amount of low-level decision making you need to do up front. As a result, you can focus on the problems you're trying to solve and get the job done more quickly.
350
+
186
351
  ## Development
187
352
 
188
353
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake spec:all` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -0,0 +1,97 @@
1
+ require "test_spec/rspec/problem"
2
+
3
+ class Example
4
+ attr_reader :run_time, :status, :duration, :exception, :description,
5
+ :full_description, :example_group, :failed_screenshot,
6
+ :screenshots, :screenrecord, :file_path, :metadata, :spec
7
+
8
+ # rubocop:disable Metrics/AbcSize
9
+ def initialize(example)
10
+ @execution_result = example.execution_result
11
+ @run_time = @execution_result.run_time.round(5)
12
+ @status = @execution_result.status.to_s
13
+ @example_group = example.example_group.to_s
14
+ @description = example.description
15
+ @full_description = example.full_description
16
+ @metadata = example.metadata
17
+ @duration = @execution_result.run_time.to_s(:rounded, precision: 5)
18
+ @screenrecord = @metadata[:screenrecord]
19
+ @screenshots = @metadata[:screenshots]
20
+ @spec = nil
21
+ @file_path = @metadata[:file_path]
22
+ @exception = Problem.new(example, @file_path)
23
+ @failed_screenshot = @metadata[:failed_screenshot]
24
+ @comment = @metadata[:comment]
25
+ end
26
+ # rubocop:enable Metrics/AbcSize
27
+
28
+ def example_title
29
+ title_arr = @example_group.to_s.split('::') - %w[RSpec ExampleGroups]
30
+ title_arr.push @description
31
+ title_arr.join(' → ')
32
+ end
33
+
34
+ def comment
35
+ ERB::Util.html_escape(@comment)
36
+ end
37
+
38
+ def has_spec?
39
+ !@spec.nil?
40
+ end
41
+
42
+ def has_comment?
43
+ !@comment.nil?
44
+ end
45
+
46
+ def has_screenrecord?
47
+ !@screenrecord.nil?
48
+ end
49
+
50
+ def has_screenshots?
51
+ !@screenshots.nil? && !@screenshots.empty?
52
+ end
53
+
54
+ def has_failed_screenshot?
55
+ !@failed_screenshot.nil?
56
+ end
57
+
58
+ def has_exception?
59
+ !@exception.klass.nil?
60
+ end
61
+
62
+ def klass(prefix = 'label-')
63
+ class_map = {
64
+ passed: "#{prefix}success",
65
+ failed: "#{prefix}danger",
66
+ pending: "#{prefix}warning"
67
+ }
68
+
69
+ class_map[@status.to_sym]
70
+ end
71
+
72
+ def create_spec_line(spec_text)
73
+ formatter = Rouge::Formatters::HTML.new(css_class: 'highlight')
74
+ lexer = Rouge::Lexers::Gherkin.new
75
+ @spec = formatter.format(lexer.lex(spec_text.gsub('#->', '')))
76
+ end
77
+
78
+ # rubocop:disable Metrics/LineLength
79
+ # rubocop:disable Metrics/AbcSize
80
+ def self.load_spec_comments!(examples)
81
+ examples.group_by(&:file_path).each do |file_path, file_examples|
82
+ lines = File.readlines(file_path)
83
+
84
+ file_examples.zip(file_examples.rotate).each do |ex, next_ex|
85
+ lexically_next = next_ex &&
86
+ next_ex.file_path == ex.file_path &&
87
+ next_ex.metadata[:line_number] > ex.metadata[:line_number]
88
+ start_line_idx = ex.metadata[:line_number] - 1
89
+ next_start_idx = (lexically_next ? next_ex.metadata[:line_number] : lines.size) - 1
90
+ spec_lines = lines[start_line_idx...next_start_idx].select { |l| l.match(/#->/) }
91
+ ex.create_spec_line(spec_lines.join) unless spec_lines.empty?
92
+ end
93
+ end
94
+ end
95
+ # rubocop:enable Metrics/LineLength
96
+ # rubocop:enable Metrics/AbcSize
97
+ end
@@ -1,24 +1,75 @@
1
+ require "test_spec/rspec/example"
2
+ require "erb"
3
+ require "rouge"
4
+ require "fileutils"
5
+ require "active_support"
6
+ require "active_support/inflector"
7
+ require "active_support/core_ext/numeric"
1
8
  require "rspec/core/formatters/documentation_formatter"
2
9
 
3
10
  module RSpec
4
11
  module TestSpec
12
+ # rubocop:disable Metrics/ClassLength
5
13
  class Formatter < ::RSpec::Core::Formatters::DocumentationFormatter
6
14
  ::RSpec::Core::Formatters.register(
7
15
  self,
8
16
  :example_started, :example_passed, :example_step_passed,
9
- :example_step_pending, :example_step_failed
17
+ :example_step_pending, :example_pending, :example_step_failed,
18
+ :example_group_finished, :example_group_started
10
19
  )
11
20
 
21
+ DEFAULT_REPORT_PATH = File.join(
22
+ '.', 'reports', Time.now.strftime('%Y%m%d-%H%M%S')
23
+ )
24
+
25
+ REPORT_PATH = ENV['REPORT_PATH'] || DEFAULT_REPORT_PATH
26
+ SCREENRECORD_DIR = File.join(REPORT_PATH, 'screenrecords')
27
+ SCREENSHOT_DIR = File.join(REPORT_PATH, 'screenshots')
28
+
29
+ def initialize(_output)
30
+ super
31
+ create_report_directory
32
+ create_screenshots_directory
33
+ create_screenrecords_directory
34
+ provide_resources
35
+
36
+ @all_groups = {}
37
+
38
+ # REPEATED FROM THE SUPER
39
+ # @group_level = 0
40
+ end
41
+
12
42
  # rubocop:disable Metrics/LineLength
13
43
  def example_started(notification)
14
44
  return unless notification.example.metadata[:with_steps]
15
45
 
16
46
  full_message = "#{current_indentation}#{notification.example.description}"
17
47
  output.puts Core::Formatters::ConsoleCodes.wrap(full_message, :default)
48
+
49
+ # For reporter
50
+ @group_example_count += 1
18
51
  end
19
52
 
53
+ # This comes from the DocumentationFormatter.
20
54
  def example_passed(notification)
21
55
  super unless notification.example.metadata[:with_steps]
56
+
57
+ # For reporter
58
+ @group_example_success_count += 1
59
+ @examples << Example.new(notification.example)
60
+ end
61
+
62
+ # This comes from the DocumentationFormatter.
63
+ def example_failed(notification)
64
+ @group_example_failure_count += 1
65
+ @examples << Example.new(notification.example)
66
+ end
67
+
68
+ # This comes from the DocumentationFormatter.
69
+ # Needed for Reporter.
70
+ def example_pending(notification)
71
+ @group_example_pending_count += 1
72
+ @examples << Example.new(notification.example)
22
73
  end
23
74
 
24
75
  def example_step_passed(notification)
@@ -48,6 +99,151 @@ module RSpec
48
99
  output.puts Core::Formatters::ConsoleCodes.wrap(full_message, :failure)
49
100
  end
50
101
  # rubocop:enable Metrics/LineLength
102
+
103
+ # ADDED FOR REPORTING
104
+
105
+ def example_group_started(_notification)
106
+ if @group_level.zero?
107
+ @examples = []
108
+ @group_example_count = 0
109
+ @group_example_success_count = 0
110
+ @group_example_failure_count = 0
111
+ @group_example_pending_count = 0
112
+ end
113
+
114
+ super
115
+
116
+ # REPEATED FROM THE SUPER
117
+ # @group_level += 1
118
+ end
119
+
120
+ # rubocop:disable Metrics/LineLength
121
+ # rubocop:disable Metrics/MethodLength
122
+ # rubocop:disable Metrics/AbcSize
123
+ def example_group_finished(notification)
124
+ super
125
+
126
+ return unless @group_level.zero?
127
+
128
+ # rubocop:disable Metrics/BlockLength
129
+ File.open("#{REPORT_PATH}/#{notification.group.description.parameterize}.html", "w") do |f|
130
+ @passed = @group_example_success_count
131
+ @failed = @group_example_failure_count
132
+ @pending = @group_example_pending_count
133
+
134
+ duration_values = @examples.map(&:run_time)
135
+ duration_keys = duration_values.size.times.to_a
136
+
137
+ if duration_values.size < 2 && !duration_values.empty?
138
+ duration_values.unshift(duration_values.first)
139
+ duration_keys = duration_keys << 1
140
+ end
141
+
142
+ @title = notification.group.description
143
+ @durations = duration_keys.zip(duration_values)
144
+ @summary_duration = duration_values.inject(0) { |sum, i| sum + i }.to_s(:rounded, precision: 5)
145
+
146
+ Example.load_spec_comments!(@examples)
147
+
148
+ class_map = {
149
+ passed: 'success',
150
+ failed: 'danger',
151
+ pending: 'warning'
152
+ }
153
+
154
+ statuses = @examples.map(&:status)
155
+
156
+ status =
157
+ if statuses.include?('failed')
158
+ 'failed'
159
+ elsif statuses.include?('passed')
160
+ 'passed'
161
+ else
162
+ 'pending'
163
+ end
164
+
165
+ @all_groups[notification.group.description.parameterize] = {
166
+ group: notification.group.description,
167
+ examples: @examples.size,
168
+ status: status,
169
+ klass: class_map[status.to_sym],
170
+ passed: statuses.select { |s| s == 'passed' },
171
+ failed: statuses.select { |s| s == 'failed' },
172
+ pending: statuses.select { |s| s == 'pending' },
173
+ duration: @summary_duration
174
+ }
175
+
176
+ template_file = File.read(
177
+ File.dirname(__FILE__) + "/../../../templates/report.erb"
178
+ )
179
+
180
+ f.puts ERB.new(template_file).result(binding)
181
+ end
182
+ # rubocop:enable Metrics/BlockLength
183
+
184
+ # THIS ONE IS FROM THE SUPER
185
+ # @group_level -= 1 if @group_level > 0
186
+ end
187
+ # rubocop:enable Metrics/MethodLength
188
+ # rubocop:enable Metrics/AbcSize
189
+
190
+ # This is from BaseTextFormatter.
191
+ # rubocop:disable Metrics/MethodLength
192
+ # rubocop:disable Metrics/AbcSize
193
+ def close(notification)
194
+ File.open("#{REPORT_PATH}/overview.html", "w") do |f|
195
+ @overview = @all_groups
196
+
197
+ @passed = @overview.values.map { |g| g[:passed].size }.inject(0) { |sum, i| sum + i }
198
+ @failed = @overview.values.map { |g| g[:failed].size }.inject(0) { |sum, i| sum + i }
199
+ @pending = @overview.values.map { |g| g[:pending].size }.inject(0) { |sum, i| sum + i }
200
+
201
+ duration_values = @overview.values.map { |e| e[:duration] }
202
+ duration_keys = duration_values.size.times.to_a
203
+
204
+ if duration_values.size < 2
205
+ duration_values.unshift(duration_values.first)
206
+ duration_keys = duration_keys << 1
207
+ end
208
+
209
+ @durations = duration_keys.zip(duration_values.map { |d| d.to_f.round(5) })
210
+ @summary_duration = duration_values.map { |d| d.to_f.round(5) }.inject(0) { |sum, i| sum + i }.to_s(:rounded, precision: 5)
211
+ @total_examples = @passed + @failed + @pending
212
+
213
+ template_file = File.read(
214
+ File.dirname(__FILE__) + "/../../../templates/overview.erb"
215
+ )
216
+
217
+ f.puts ERB.new(template_file).result(binding)
218
+ end
219
+
220
+ super
221
+ end
222
+ # rubocop:enable Metrics/AbcSize
223
+ # rubocop:enable Metrics/MethodLength
224
+ # rubocop:enable Metrics/LineLength
225
+
226
+ private
227
+
228
+ def create_report_directory
229
+ FileUtils.rm_rf(REPORT_PATH) if File.exist?(REPORT_PATH)
230
+ FileUtils.mkpath(REPORT_PATH)
231
+ end
232
+
233
+ def create_screenshots_directory
234
+ FileUtils.mkdir_p SCREENSHOT_DIR unless File.exist?(SCREENSHOT_DIR)
235
+ end
236
+
237
+ def create_screenrecords_directory
238
+ FileUtils.mkdir_p SCREENRECORD_DIR unless File.exist?(SCREENRECORD_DIR)
239
+ end
240
+
241
+ def provide_resources
242
+ FileUtils.cp_r(
243
+ File.dirname(__FILE__) + "/../../../resources", REPORT_PATH
244
+ )
245
+ end
51
246
  end
247
+ # rubocop:enable Metrics/ClassLength
52
248
  end
53
249
  end