radius-spec 0.2.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,7 @@
1
1
  inherit_mode:
2
2
  merge:
3
3
  - Exclude
4
+ - IgnoredPatterns
4
5
 
5
6
  inherit_from: common_rubocop.yml
6
7
 
@@ -26,6 +27,9 @@ Documentation:
26
27
 
27
28
  Metrics/BlockLength:
28
29
  Exclude:
30
+ - 'bin/setup'
31
+ - 'bin/update'
32
+ - 'config/routes.rb'
29
33
  - 'spec/rails_helper.rb'
30
34
 
31
35
  # Rails foreign keys and indexes can get long. We want to ignore our annotation
@@ -38,6 +42,52 @@ Metrics/LineLength:
38
42
  - '\A# fk_rails_'
39
43
  - '\A# index_'
40
44
 
45
+ # For our Rails apps several of them use the `respond_to` with `format` blocks
46
+ # to handle various mime types (mostly HTML and JSON). Given our `do` / `end`
47
+ # block style for non-functional blocks (which includes both `respond_to` and
48
+ # `format`) the general method limit of is too small. This also applies to the
49
+ # helper methods which define the allowed parameters for the action; especially
50
+ # for larger forms.
51
+ #
52
+ # Here is an example of a minimal controller `update` method which uses the
53
+ # `respond_to` style to support just the HTML and JSON formats:
54
+ #
55
+ # ```ruby
56
+ # def update
57
+ # respond_to do |format|
58
+ # if @resource.update(resource_params)
59
+ # format.html do
60
+ # redirect_to resources_url, notice: 'Resource was successfully updated.'
61
+ # end
62
+ # format.json do
63
+ # render :show, status: :ok, location: @resource
64
+ # end
65
+ # else
66
+ # format.html do
67
+ # render :edit
68
+ # end
69
+ # format.json do
70
+ # render json: @resource.errors, status: :unprocessable_entity
71
+ # end
72
+ # end
73
+ # end
74
+ # end
75
+ # ```
76
+ #
77
+ # We do believe that the default size of 10, which is what we explicitly
78
+ # configure below so there's no confusion, is a good general limit to help
79
+ # encourage a balance between terseness and procedural code. Thus we do not
80
+ # want to raise the limit, instead we just want to exclude these controllers.
81
+ #
82
+ # At this time there is no way for us to exclude just the common controller
83
+ # actions / *_params methods so we exclude the entire file.
84
+ #
85
+ # Configuration parameters: CountComments.
86
+ Metrics/MethodLength:
87
+ Max: 10
88
+ Exclude:
89
+ - 'app/controllers/**/*_controller.rb'
90
+
41
91
  # Ignore subclass parent for one off benchmarks
42
92
  #
43
93
  # Benchmarks are generally meant to be small and targeted. They often have
@@ -58,14 +108,51 @@ Rails/ApplicationRecord:
58
108
  Rails/CreateTableWithTimestamps:
59
109
  Enabled: false
60
110
 
61
- # The ActiveSupport monkey patches for `present?` are nearly all defiend as:
111
+ # Usage of `find_by` is more expressive of intent than `where.first`. We should
112
+ # check all app code, not just the models to improve intent expression.
113
+ #
114
+ # Since rake tasks often live in `lib` we also check all of lib as well.
115
+ #
116
+ # Configuration parameters: Include.
117
+ # Include: app/models/**/*.rb
118
+ Rails/FindBy:
119
+ Enabled: true
120
+ Include:
121
+ - 'app/**/*.rb'
122
+ - 'lib/**/*.rb'
123
+
124
+ # Usage of `each` for large datasets can be a performance issue; specially a
125
+ # drain on system memory. When possible it's better to use `find_each` so that
126
+ # chunks of data are evaluated at a time.
127
+ #
128
+ # We should check all app code, not just the models to help prevent this. Since
129
+ # rake tasks often live in `lib` we also check all of lib as well.
130
+ #
131
+ # Configuration parameters: Include.
132
+ # Include: app/models/**/*.rb
133
+ Rails/FindEach:
134
+ Enabled: true
135
+ Include:
136
+ - 'app/**/*.rb'
137
+ - 'lib/**/*.rb'
138
+
139
+ # We understand the trade-offs for using the through model versus a lookup
140
+ # table. As such this cop is just noise as it flags only those cases we really
141
+ # do want a lookup table.
142
+ #
143
+ # Configuration parameters: Include.
144
+ # Include: app/models/**/*.rb
145
+ Rails/HasAndBelongsToMany:
146
+ Enabled: false
147
+
148
+ # The ActiveSupport monkey patches for `present?` are nearly all defined as:
62
149
  #
63
150
  # !blank?
64
151
  #
65
- # For most of use `unless blank?` reads just as easily as `if present?`.
152
+ # For most of us `unless blank?` reads just as easily as `if present?`.
66
153
  # Sometimes contextually, it can read better depending on the branch logic and
67
154
  # surrounding context. As `if present?` requires an additional negation and
68
- # method call it is technically slower. In the general case the perf different
155
+ # method call it is technically slower. In the general case the perf difference
69
156
  # isn't much but in some cases it matters. Thus, we are not enforcing changing
70
157
  # `unless blank?` to `if present?` and are leaving it up to the context to
71
158
  # decide which is a better fit.
@@ -78,7 +165,7 @@ Rails/Present:
78
165
  # We prefer you use the attribute readers and writes. For those special cases
79
166
  # where the intent is really to interact with the raw underlying attribute we
80
167
  # prefer `read_attribute` and `write_attribute`; as this makes the intent
81
- # explict. Ideally we'd never use the hash like accessor `[:attr]`.
168
+ # explicit. Ideally we'd never use the hash like accessor `[:attr]`.
82
169
  #
83
170
  # We disable this cop because it is not configurable.
84
171
  #
@@ -87,6 +174,37 @@ Rails/Present:
87
174
  Rails/ReadWriteAttribute:
88
175
  Enabled: false
89
176
 
177
+ # This ensures we do not ignore potential validation issues in the code. Doing
178
+ # so can lead to strange and surprising bugs where records are expected to
179
+ # be created, or be modified, but are not.
180
+ #
181
+ # # If author is a new record the book may not be created since the FK is
182
+ # # invalid. Perhaps omitting other fields, maybe new required fields, is
183
+ # # an oversight in the book creation as well.
184
+ # author.save
185
+ # Book.create(author: author)
186
+ #
187
+ # Or side effects are expected to occur but they do not:
188
+ #
189
+ # # This is a condensed default Rails scaffold controller for `destroy`.
190
+ # #
191
+ # # Once a `has_many` or `has_one` associations is added which specifies
192
+ # # `dependent: :restrict_with_error` this no longer behaves as expected.
193
+ # # Given such associations are often added much later in time errors in
194
+ # # this action are an all to common oversight in Rails.
195
+ # def destroy
196
+ # @book.destroy
197
+ # respond_to do |format|
198
+ # format.html do
199
+ # redirect_to books_url, notice: 'Book was successfully destroyed.'
200
+ # end
201
+ # end
202
+ # end
203
+ #
204
+ # Configuration parameters: AllowImplicitReturn, AllowedReceivers.
205
+ Rails/SaveBang:
206
+ Enabled: true
207
+
90
208
  # According to the Rails docs while the following methods skip validations they
91
209
  # only update the specified (single) attribute reducing risks. We'd rather not
92
210
  # warn for those cases:
@@ -104,17 +222,13 @@ Rails/ReadWriteAttribute:
104
222
  # - http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html
105
223
  # - http://api.rubyonrails.org/classes/ActiveRecord/Relation.html
106
224
  #
225
+ # Configuration parameters: Blacklist, Whitelist.
107
226
  # Blacklist: decrement!, decrement_counter, increment!, increment_counter, toggle!, touch, update_all, update_attribute, update_column, update_columns, update_counters
108
227
  Rails/SkipsModelValidations:
109
- Blacklist:
110
- - 'decrement_counter'
111
- - 'increment_counter'
112
- - 'toggle!'
113
- - 'update_all'
114
- - 'update_attribute'
115
- - 'update_column'
116
- - 'update_columns'
117
- - 'update_counters'
228
+ Whitelist:
229
+ - 'decrement!'
230
+ - 'increment!'
231
+ - 'touch'
118
232
 
119
233
  # Rails uses compact style by default so we're disabling this with a :hammer:
120
234
  # for things likely to be generated by Rails (i.e. most things in app).
@@ -268,6 +268,7 @@ module Radius
268
268
  def safe_transform(value)
269
269
  return value.call if value.is_a?(Proc)
270
270
  return value if value.frozen?
271
+
271
272
  value.dup
272
273
  end
273
274
 
@@ -426,44 +427,56 @@ module Radius
426
427
 
427
428
  # Convenience wrapper for building, and persisting, a model template.
428
429
  #
429
- # This is a thin wrapper around `build(name, attrs).tap(&:save!)`. The
430
- # persistence message `save!` will only be called on objects which
431
- # respond to it.
432
- #
433
- # ### Avoid for New Code
430
+ # This is a thin wrapper around:
434
431
  #
435
- # It is strongly suggested that you avoid using `create` for new code.
436
- # Instead be explicit about when and how objects are persisted. This
437
- # allows you to have fine grain control over how your data is setup.
432
+ # ```ruby
433
+ # build(name, attrs, &block).tap(&:save!)
434
+ # ```
438
435
  #
439
- # We suggest that you create instances which need to be persisted before
440
- # your specs using the following syntax:
436
+ # The persistence message `save!` will only be called on objects which
437
+ # respond to it.
441
438
  #
442
- # ```ruby
443
- # let(:an_instance) { build("AnyClass") }
439
+ # @note It is generally suggested that you avoid using `build!` for new
440
+ # code. Instead be explicit about when and how objects are persisted.
441
+ # This allows you to have fine grain control over how your data is
442
+ # setup.
444
443
  #
445
- # before do
446
- # an_instance.save!
447
- # end
448
- # ```
444
+ # We suggest that you create instances which need to be persisted
445
+ # before your specs using the following syntax:
449
446
  #
450
- # Alternatively if you really want for the instance be lazy instantiated,
451
- # and persisted, pass the appropriate persistence method as the block:
447
+ # ```ruby
448
+ # let(:an_instance) { build("AnyClass") }
452
449
  #
453
- # ```ruby
454
- # let(:an_instance) { build("AnyClass", &:save!) }
455
- # ```
450
+ # before do
451
+ # an_instance.save!
452
+ # end
453
+ # ```
456
454
  #
457
455
  # @param (see .build)
458
456
  # @return (see .build)
459
457
  # @raise (see .build)
460
458
  # @see .build
461
459
  # @see .define_factory
462
- def create(name, custom_attrs = {}, &block)
460
+ # @since 0.5.0
461
+ def build!(name, custom_attrs = {}, &block)
463
462
  instance = build(name, custom_attrs, &block)
464
463
  instance.save! if instance.respond_to?(:save!)
465
464
  instance
466
465
  end
466
+
467
+ # Legacy helper provided for backwards compatibility support.
468
+ #
469
+ # This provides the same behavior as {.build!} and will be removed in a
470
+ # future release.
471
+ #
472
+ # @param (see .build)
473
+ # @return (see .build)
474
+ # @raise (see .build)
475
+ # @see .build
476
+ # @see .define_factory
477
+ def create(name, custom_attrs = {}, &block)
478
+ build!(name, custom_attrs, &block)
479
+ end
467
480
  end
468
481
  end
469
482
  end
@@ -7,7 +7,7 @@ require 'rspec/rails'
7
7
 
8
8
  RSpec.configure do |config|
9
9
  # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
10
- config.fixture_path = ::Rails.root.join("spec", "fixtures")
10
+ config.fixture_path = ::Rails.root.join("spec", "fixtures").to_path
11
11
 
12
12
  # If you're not using ActiveRecord, or you'd prefer not to run each of your
13
13
  # examples within a transaction, remove the following line or assign false
@@ -40,7 +40,7 @@ RSpec.configure do |config|
40
40
  # - http://guides.rubyonrails.org/v5.1.4/testing.html#implementing-a-system-test
41
41
  # - https://relishapp.com/rspec/rspec-core/v/3-7/docs/filtering/exclusion-filters
42
42
  # - http://rspec.info/documentation/3.7/rspec-core/RSpec/Core/Configuration.html#filter_run_excluding-instance_method
43
- config.filter_run_excluding type: "system"
43
+ config.filter_run_excluding type: "system" unless config.files_to_run.one?
44
44
 
45
45
  # Always clear the cache before specs to avoid state leak problems
46
46
  config.before do
@@ -109,6 +109,11 @@ RSpec.configure do |config|
109
109
  config.include Radius::Spec::ModelFactory, :model_factory, :model_factories
110
110
  end
111
111
 
112
+ config.when_first_matching_example_defined(:tempfile, :tmpfile) do
113
+ require 'radius/spec/tempfile'
114
+ config.include Radius::Spec::Tempfile, :tempfile, :tmpfile
115
+ end
116
+
112
117
  config.when_first_matching_example_defined(type: :controller) do
113
118
  require 'radius/spec/model_factory'
114
119
  config.include Radius::Spec::ModelFactory, type: :controller
@@ -124,6 +129,11 @@ RSpec.configure do |config|
124
129
  config.include Radius::Spec::ModelFactory, type: :job
125
130
  end
126
131
 
132
+ config.when_first_matching_example_defined(type: :mailer) do
133
+ require 'radius/spec/model_factory'
134
+ config.include Radius::Spec::ModelFactory, type: :mailer
135
+ end
136
+
127
137
  config.when_first_matching_example_defined(type: :model) do
128
138
  require 'radius/spec/model_factory'
129
139
  config.include Radius::Spec::ModelFactory, type: :model
@@ -138,4 +148,14 @@ RSpec.configure do |config|
138
148
  require 'radius/spec/model_factory'
139
149
  config.include Radius::Spec::ModelFactory, type: :system
140
150
  end
151
+
152
+ config.when_first_matching_example_defined(:webmock) do
153
+ require 'webmock/rspec'
154
+ end
155
+
156
+ config.when_first_matching_example_defined(:vcr, :vcr_record, :vcr_record_new) do
157
+ require 'radius/spec/vcr'
158
+ end
141
159
  end
160
+
161
+ require 'radius/spec/rspec/negated_matchers'
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Define negated matchers for use with composable matchers and compound
4
+ # expectations.
5
+ #
6
+ # @see https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/composing-matchers
7
+ # @see https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/compound-expectations
8
+ RSpec::Matchers.define_negated_matcher :exclude, :include
9
+ RSpec::Matchers.define_negated_matcher :excluding, :including
10
+ RSpec::Matchers.define_negated_matcher :not_eq, :eq
11
+
12
+ # Allows us to check that a block doesn't raise an exception while also
13
+ # checking for other changes using compound expectations; since you can't chain
14
+ # a negative (`not_to`) with any other matchers
15
+ #
16
+ # @see https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/compound-expectations
17
+ RSpec::Matchers.define_negated_matcher :not_change, :change
18
+ RSpec::Matchers.define_negated_matcher :not_raise_error, :raise_error
19
+ RSpec::Matchers.define_negated_matcher :not_raise_exception, :raise_exception
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'tempfile'
5
+
6
+ module Radius
7
+ module Spec
8
+ # Temporary file helpers
9
+ #
10
+ # These helpers are meant to ease the creation of temporary files to either
11
+ # stub the data out or provide a location for data to be saved then
12
+ # verified.
13
+ #
14
+ # In the case of file stubs, using these helpers allows you to co-locate
15
+ # the file data with the specs. This makes it easy for someone to read the
16
+ # spec and understand the test case; instead of having to find a fixture
17
+ # file and look at its data. This also makes it easy to change the data
18
+ # between specs, allowing them to focus on just what they need.
19
+ #
20
+ # To make these helpers available require them after the gem:
21
+ #
22
+ # ```ruby
23
+ # require 'radius/spec'
24
+ # require 'radius/spec/tempfile'
25
+ # ```
26
+ #
27
+ # ### Including Helpers in Specs
28
+ #
29
+ # There are multiple ways you can use these helpers. Which method you
30
+ # choose depends on how much perceived magic/syntactic sugar you want:
31
+ #
32
+ # - call the helpers directly on the module
33
+ # - manually include the helper methods in the specs
34
+ # - use metadata to auto load this feature and include it in the specs
35
+ #
36
+ # When using the metadata option you do not need to explicitly require the
37
+ # module. This gem registers metadata with the RSpec configuration when it
38
+ # loads and `RSpec` is defined. When the matching metadata is first used it
39
+ # will automatically require and include the helpers.
40
+ #
41
+ # Any of following metadata will include the factory helpers:
42
+ #
43
+ # - `:tempfile`
44
+ # - `:tmpfile`
45
+ #
46
+ # @example use a helper directly in specs
47
+ # require 'radius/spec/tempfile'
48
+ #
49
+ # RSpec.describe AnyClass do
50
+ # it "includes the file helpers" do
51
+ # Radius::Spec::Tempfile.using_tempfile do |pathname|
52
+ # code_under_test pathname
53
+ # expect(pathname.read).to eq "Any written data"
54
+ # end
55
+ # end
56
+ # end
57
+ # @example manually include the helpers
58
+ # require 'radius/spec/tempfile'
59
+ #
60
+ # RSpec.describe AnyClass do
61
+ # include Radius::Spec::Tempfile
62
+ # it "includes the file helpers" do
63
+ # using_tempfile do |pathname|
64
+ # code_under_test pathname
65
+ # expect(pathname.read).to eq "Any written data"
66
+ # end
67
+ # end
68
+ # end
69
+ # @example use metadata to auto include the helpers
70
+ # RSpec.describe AnyClass do
71
+ # it "includes the file helpers", :tempfile do
72
+ # using_tempfile do |pathname|
73
+ # code_under_test pathname
74
+ # expect(pathname.read).to eq "Any written data"
75
+ # end
76
+ # end
77
+ # end
78
+ # @since 0.5.0
79
+ module Tempfile
80
+ module_function
81
+
82
+ # Convenience wrapper for managaing temporary files.
83
+ #
84
+ # This creates a temporary file and yields its path to the provided
85
+ # block. When the block returns the temporary file is deleted.
86
+ #
87
+ # ### Optional Parameters
88
+ #
89
+ # The block is required. All other parameters are optional. All
90
+ # parameters except `data` are Ruby version dependent and will be
91
+ # forwarded directly to the stdlib's
92
+ # {https://ruby-doc.org/stdlib/libdoc/tempfile/rdoc/Tempfile.html#method-c-create
93
+ # `Tempfile.create`}. The when the `data` parameter is provided it's
94
+ # contents will be written to the temporary file prior to yielding to the
95
+ # block.
96
+ #
97
+ # @example creating a tempfile to pass to code
98
+ # def write_hello_world(filepath)
99
+ # File.write filepath, "Hello World"
100
+ # end
101
+ #
102
+ # Radius::Spec::Tempfile.using_tempfile do |pathname|
103
+ # write_hello_world pathname
104
+ # end
105
+ # @example creating a file stub
106
+ # stub_data = "Any file stub data text."
107
+ # Radius::Spec::Tempfile.using_tempfile(data: stub_data) do |stubpath|
108
+ # # File.read(stubpath)
109
+ # # => "Any file stub data text."
110
+ # code_under_test stubpath
111
+ # end
112
+ # @example creating a file stub inline
113
+ # Radius::Spec::Tempfile.using_tempfile(data: <<~TEXT) do |stubpath|
114
+ # Any file stub data text.
115
+ # TEXT
116
+ # # File.read(stubpath)
117
+ # # => "Any file stub data text.\n"
118
+ # code_under_test stubpath
119
+ # end
120
+ # @example creating a file stub inline without trailing newline
121
+ # Radius::Spec::Tempfile.using_tempfile(data: <<~TEXT.chomp) do |stubpath|
122
+ # Any file stub data text.
123
+ # TEXT
124
+ # # File.read(stubpath)
125
+ # # => "Any file stub data text."
126
+ # code_under_test stubpath
127
+ # end
128
+ # @example writing binary data inline
129
+ # Radius::Spec::Tempfile.using_tempfile(encoding: Encoding::BINARY, data: <<~BIN.chomp) do |binpath|
130
+ # \xC8\x90\xC5\x9D\xE1\xB9\x95\xC4\x93\xC4\x89
131
+ # BIN
132
+ # # File.read(binpath)
133
+ # # => "Ȑŝṕēĉ"
134
+ # code_under_test binpath
135
+ # end
136
+ # @param args [Object] addition file creation options
137
+ #
138
+ # Passed directly to {https://ruby-doc.org/stdlib/libdoc/tempfile/rdoc/Tempfile.html#method-c-create
139
+ # `Tempfile.create`}; see the stdlib docs for details on available
140
+ # options.
141
+ # @param data [String] stub data to write to the file before yielding
142
+ # @param kwargs [Hash{Symbol => Object}] addition file creation options
143
+ #
144
+ # Passed directly to {https://ruby-doc.org/stdlib/libdoc/tempfile/rdoc/Tempfile.html#method-c-create
145
+ # `Tempfile.create`}; see the stdlib docs for details on available
146
+ # options.
147
+ # @yieldparam pathname [Pathname] {https://ruby-doc.org/stdlib/libdoc/pathname/rdoc/Pathname.html path}
148
+ # of the created temporary file
149
+ # @note A block must be provided
150
+ # @see https://ruby-doc.org/stdlib/libdoc/pathname/rdoc/Pathname.html Pathname
151
+ # @see https://ruby-doc.org/stdlib/libdoc/tempfile/rdoc/Tempfile.html Tempfile
152
+ def using_tempfile(*args, data: nil, **kwargs)
153
+ args << 'tmpfile' if args.empty?
154
+ ::Tempfile.create(*args, **kwargs) do |f|
155
+ f.write(data)
156
+ f.close
157
+ yield Pathname(f.path)
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end