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