README.md in radius-spec-0.4.0 vs README.md in radius-spec-0.5.0
- old
+ new
@@ -247,11 +247,11 @@
##### Template Attribute Keys
Attribute keys may be defined using either strings or symbols. However, they
will be stored internally as symbols. This means that when an object instance
-is create using the factory the attribute hash will be provided to `new` with
+is created using the factory the attribute hash will be provided to `new` with
symbol keys.
##### Dynamic Attribute Values (i.e. Generators)
We try to keep the special cases / rules to a minimum. To support dynamic
@@ -416,30 +416,30 @@
`Proc` as an attribute value it will be sent to new directly without
receiving `call`.
##### Optional Block
-Both `build` and `create` support providing an optional block. This block is
+Both `build` and `build!` support providing an optional block. This block is
passed directly to `new` when creating the object. This is to support the
common Ruby idiom of yielding `self` within initialize:
- ```ruby
- class AnyClass
- def initialize(attrs = {})
- # setup attrs
- yield self if block_given?
- end
+```ruby
+class AnyClass
+ def initialize(attrs = {})
+ # setup attrs
+ yield self if block_given?
end
+end
- RSpec.describe AnyClass, :model_factory do
- it "passes the block to the object initializer" do
- block_capture = nil
- an_object = build("AnyClass") { |instance| block_capture = instance }
- expect(block_capture).to be an_object
- end
+RSpec.describe AnyClass, :model_factory do
+ it "passes the block to the object initializer" do
+ block_capture = nil
+ an_object = build("AnyClass") { |instance| block_capture = instance }
+ expect(block_capture).to be an_object
end
- ```
+end
+```
Since Ruby always supports passing a block to a method, even if the method does
not use the block, it's possible the block will not run if the class being
instantiated does not do anything with it.
@@ -449,32 +449,331 @@
##### "Creating" Instances
We suggest that you create instances using the following syntax:
- ```ruby
- created_instance = build("AnyClass").tap(&:save!)
- ```
+```ruby
+let(:an_instance) { build("AnyClass") }
+before do
+ an_instance.save!
+end
+```
+
Or alternatively:
- ```ruby
- let(:an_instance) { build("AnyClass") }
+```ruby
+created_instance = build("AnyClass")
+created_instance.save!
+```
- before do
- an_instance.save!
- end
- ```
-
This way it is explicit what objects need to be persisted and in what order.
-However, many of our existing projects use a legacy `create` helper. This is
-simply a wrapper around `build.tap(&:save!)`, but it supports omitting the
-`save!` call for objects which do not support it.
+This can get tedious at times, especially for those who only need to create an
+object to embed as an attribute of another object:
- ```ruby
- created_instance = create("AnyClass")
- ```
+```ruby
+collaborator = build("AnotherClass")
+collaborator.save!
+
+# collaborator is not used against directly after this line
+created_instance = build("AnyClass", collaborator: collaborator)
+created_instance.save!
+```
+
+For these cases the `build!` helper is available. This is simply an alias for
+`build.tap(&:save!)`, but it supports omitting the `save!` call for objects
+which do not support it. While it provides a safety guarantee that `save!` will
+be called (instead of potentially `save`) it is less explicit.
+
+```ruby
+created_instance = build("AnyClass", collaborator: build!("AnotherClass"))
+created_instance.save!
+```
+
+We still discourage the use of `build!` directly in `let` blocks for all of the
+above mentioned reasons.
+
+##### Legacy "Creating" Instances
+
+Many of our existing projects use a legacy `create` helper. This is simply an
+alias for `build!`. It is provided only for backwards compatibility support and
+will be removed in a future release. New code should not use this method.
+
+```ruby
+created_instance = create("AnyClass")
+```
+
+### Negated Matchers
+
+This gem defines the following [negated matchers](https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/define-negated-matcher)
+to allow for use [composing matchers](https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/composing-matchers)
+and with [compound expectations](https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/compound-expectations).
+
+| Matcher | Inverse Of |
+|-----------------------|------------|
+| `exclude` | [`include`](https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/include-matcher) |
+| `excluding` | [`including`](https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/include-matcher) |
+| `not_eq` | [`eq`](https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/equality-matchers#compare-using-eq-(==)) |
+| `not_change` | [`change`](https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/change-matcher) |
+| `not_raise_error` | [`raise_error`](https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/raise-error-matcher) |
+| `not_raise_exception` | [`raise_exception`](https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/raise-error-matcher) |
+
+#### Composing Matchers
+
+There is no equivalent of `not_to` for composed matchers when only a subset of
+the values needs to be negated. The negated matchers allow this type of fine
+grain comparison:
+
+```ruby
+x = [1, 2, :value]
+expect(x).to contain_exactly(be_odd, be_even, not_eq(:target))
+```
+
+This also works for verifying / stubbing a message with argument constraints:
+
+```ruby
+allow(obj).to receive(:meth).with(1, 2, not_eq(5))
+obj.meth(1, 2, 3)
+expect(obj).to have_received(:meth).with(not_eq(2), 2, 3)
+```
+
+This is great for verifying option hashes:
+
+```ruby
+expect(obj).to have_received(:meth).with(
+ some_value,
+ excluding(:some_opt, :another_opt),
+)
+```
+
+#### Compound Negated Matchers
+
+Normally it's not possible to chain to a negative match:
+
+```ruby
+a = b = 0
+expect {
+ a = 1
+}.not_to change {
+ b
+}.from(0).and change {
+ a
+}.to(1)
+```
+
+Fails with:
+
+ NotImplementedError:
+ `expect(...).not_to matcher.and matcher` is not supported, since it creates
+ a bit of an ambiguity. Instead, define negated versions of whatever
+ matchers you wish to negate with `RSpec::Matchers.define_negated_matcher`
+ and use `expect(...).to matcher.and matcher`.
+
+Per the error the negated matcher allows for the following:
+
+```ruby
+a = b = 0
+expect {
+ a = 1
+}.to change {
+ a
+}.to(1).and not_change {
+ b
+}.from(0)
+```
+
+Similarly, complex expectations can be set on lists:
+
+```ruby
+a = %i[red blue green]
+expect(a).to include(:red).and exclude(:yellow)
+expect(a).to exclude(:yellow).and include(:red)
+```
+
+### Working with Temp Files
+
+These helpers are meant to ease the creation of temporary files to either stub
+the data out or provide a location for data to be saved then verified.
+
+In the case of file stubs, using these helpers allows you to co-locate the file
+data with the specs. This makes it easy for someone to read the spec and
+understand the test case; instead of having to find a fixture file and look at
+its data. This also makes it easy to change the data between specs, allowing
+them to focus on just what they need.
+
+#### Usage
+
+There are multiple ways you can use these helpers. Which method you choose
+depends on how much perceived magic/syntactic sugar you want:
+
+ - Call the helpers directly on the module:
+
+ ```ruby
+ require 'radius/spec/tempfile'
+
+ def write_hello_world(filepath)
+ File.write filepath, "Hello World"
+ end
+
+ Radius::Spec::Tempfile.using_tempfile do |pathname|
+ write_hello_world pathname
+ File.read(pathname)
+ # => "Hello World"
+ end
+ ```
+ - Include the helper methods explicitly:
+
+ ```ruby
+ require 'radius/spec/tempfile'
+
+ RSpec.describe AnyClass do
+ include Radius::Spec::Tempfile
+
+ it "includes the file helpers" do
+ using_tempfile do |pathname|
+ code_under_test pathname
+ expect(pathname.read).to eq "Any written data"
+ end
+ end
+ end
+ ```
+ - Include the helper methods via metadata:
+
+ ```ruby
+ RSpec.describe AnyClass do
+ it "includes the file helpers", :tempfile do
+ using_tempfile do |pathname|
+ code_under_test pathname
+ expect(pathname.read).to eq "Any written data"
+ end
+ end
+ end
+ ```
+
+ When using this metadata option you do not need to explicitly require the
+ tempfile feature. This gem registers metadata with the RSpec configuration
+ when it loads and `RSpec` is defined. When the metadata is first used it
+ will automatically require the tempfile feature and include the helpers.
+
+ Any of following metadata will include the factory helpers:
+
+ - `:tempfile`
+ - `:tmpfile`
+
+There are a few additional behaviors to note:
+
+ - Data can be stubbed by the helper through the `data` keyword arg:
+
+ ```ruby
+ stub_data = "Any file stub data text."
+ Radius::Spec::Tempfile.using_tempfile(data: stub_data) do |stubpath|
+ File.read(stubpath)
+ # => "Any file stub data text."
+ end
+ ```
+
+ It can even be inlined using heredocs:
+
+ ```ruby
+ Radius::Spec::Tempfile.using_tempfile(data: <<~TEXT) do |stubpath|
+ Any file stub data text.
+ TEXT
+ # Yard formats heredoc args oddly
+ File.read(stubpath)
+ # => "Any file stub data text.\n"
+ end
+ ```
+
+ > NOTE: That when inlining like this heredocs add an extra new line. To
+ > remove it use `.chomp` on the kwarg:
+ >
+ > ```ruby
+ > using_tempfile(data: <<~TEXT.chomp) do |pathname|
+ > This has no newline.
+ > TEXT
+ > # ...
+ > end
+ > ```
+
+ - Additional arguments and options are forwarded directly to
+ [Tempfile.create](https://ruby-doc.org/stdlib/libdoc/tempfile/rdoc/Tempfile.html#method-c-create)
+
+ This allows you to set custom file extensions:
+
+ ```ruby
+ Radius::Spec::Tempfile.using_tempfile(%w[custom_name .myext]) do |pathname|
+ pathname.extname
+ # => ".myext"
+ end
+ ```
+
+ Or change the file encoding:
+
+ ```ruby
+ Radius::Spec::Tempfile.using_tempfile(encoding: "ISO-8859-1", data: <<~DATA) do |pathname|
+ Résumé
+ DATA
+ # Yard formats heredoc args oddly
+ File.read(pathname)
+ # => "R\xE9sum\xE9\n"
+ end
+ ```
+
+### Common VCR Configuration
+
+A project must include both [`vcr`](https://rubygems.org/gems/vcr) and
+[`webmock`](https://rubygems.org/gems/webmock) to use this configuration.
+Neither of those gems will be installed as dependencies of this gem. This is
+intended to give projects more flexibility in choosing which additional features
+they will use.
+
+The main `radius/spec/rspec` setup will load the common VCR configuration
+automatically when a spec is tagged with the `:vcr` metadata. This will
+configure VCR to:
+
+ - save specs to `/spec/cassettes`
+
+ - use record mode `once` when a single spec or spec file is run
+
+ This helps ease the development of new specs without requiring any
+ configuration / setting changes.
+
+ - uses record mode `none` otherwise, along setting VCR to fail when unused
+ interactions remain in a cassette
+
+ This is intended to better alert developers to unexpected side effects of
+ changes as any addition or removal of a request will cause a failure.
+
+ - all `Authorization` HTTP headers are filtered by default
+
+ This is a common oversight when recording specs. Often token based
+ authentication is picked up by the other filtered environment settings, but
+ basic authentication is not. Additionally, certain types of digest
+ authentication may cause specs to leak state. This filtering guards all of
+ these cases from accidental credential leak.
+
+ - the following common sensitive, or often environment variable, settings are
+ filtered
+
+ Those settings which often change between developer machines, and even the
+ CI server, can cause for flaky specs. It may also be frustrating for
+ developers to have to adjust their local systems to match others just to
+ get a few specs to pass. This is intended to help mitigate those issues:
+
+ - `AWS_ACCESS_KEY_ID`
+ - `AWS_SECRET_ACCESS_KEY`
+ - `GOOGLE_CLIENT_ID`
+ - `GOOGLE_CLIENT_SECRET`
+ - `RADIUS_OAUTH_PROVIDER_APP_ID`
+ - `RADIUS_OAUTH_PROVIDER_APP_SECRET`
+ - `RADIUS_OAUTH_PROVIDER_URL`
+
+ - a project's local `support/vcr.rb` file will be loaded after the common
+ VCR configuration loads; if it's available
+
+ This allows projects to overwrite common settings if they need to, as well,
+ as add on addition settings or filtering of data.
## Development
After checking out the repo, run `bin/setup` to install dependencies. Then, run
`rake spec` to run the tests. You can also run `bin/console` for an interactive