rspec-expectations icon indicating copy to clipboard operation
rspec-expectations copied to clipboard

Provide both strict (true/false) and loose (truthy/falsey) forms of the be/have matchers

Open pirj opened this issue 4 years ago • 7 comments

Maybe we can have both strict (true/false) and loose (truthy/falsey) forms of the be/have matchers?

That way, regardless of which is the "default", it is easy to do inline adjustment

some syntax possibilities:

  1. picking a symbol to append e.g. be_infinite vs be_infinite? or have_foo vs have_foo!
  2. adding an option to the matcher e.g. be_ready(strict: false)
  3. coming up with another prefix (couldn't think of one)

alternatively there could be an explicitly truthy/falsey matcher expect(number).to reply(:infinite?) expect(number).to respond_truthy(:infinite?)

Originally posted by @fledman in https://github.com/rspec/rspec-mocks/issues/1218#issuecomment-833027284

pirj avatar May 06 '21 10:05 pirj

@fledman the problem with passing a strict argument to predicate matchers is that the underlying predicate can accept parameters as well, and we pass them over to it, e.g. have_key(:foo)/be_multiple_of(3).

Context for strict_predicate_matchers and predicate matchers returning something outside of true/false:

  • https://github.com/rspec/rspec-expectations/issues/1102
  • https://bugs.ruby-lang.org/issues/9123
  • https://github.com/rspec/rspec-expectations/pull/1196
  • https://github.com/rspec/rspec-expectations/pull/1277
  • https://github.com/rubocop/rubocop-rspec/issues/919

pirj avatar May 06 '21 10:05 pirj

I'd prefer either adding a block that flips the config e.g you default to strict but run certain examples with:

around(:example) { |ex| use_truthy_predicate_matchers { ex.call } }

or setting certain predicate matcher override modes

config.set_strict_predicate_matcher_mode :be_infinite, :truthy

etc

JonRowe avatar May 06 '21 13:05 JonRowe

if a strict kwarg won't work, then my preference is a new truthy/falsey matcher

since I would prefer to impact a single assertion, rather than an example, a context, or the whole suite

fledman avatar May 06 '21 15:05 fledman

answer is another possible name, in addition to reply or respond_truthy

(although I am not in love with any of the three)

semantics:

  • expect(obj).to answer(method)
    • fails if obj does not respond to method
    • calls obj.public_send(method)
    • fails if the method call raised an exception
    • passes if the return value is truthy
  • expect(obj).not_to answer(method)
    • fails if obj does not respond to method
    • calls obj.public_send(method)
    • fails if the method call raised an exception
    • passes if the return value is falsey

examples:

# passing
expect(123).not_to answer(:infinite?)
expect(-Float::INFINITY).to answer(:infinite?)
expect([1]).to answer(:first)
expect({a:{b:{c:123}}}).to answer(:dig, :a, :b, :c)

# failing
expect(123).to answer(:infinite?)
expect(-Float::INFINITY).not_to answer(:infinite?)
expect('words').to answer(:infinite?)
expect('words').not_to answer(:infinite?)
expect([]).to answer(:first)
expect({}).to answer(:dig, :a, :b, :c)

fledman avatar May 06 '21 16:05 fledman

I love the ideas of @JonRowe or a new matcher.

benoittgt avatar May 06 '21 21:05 benoittgt

We kept be_truthy/be_falsey in 4.0, so the following is still possible:

# passes
expect(123.infinite?).to be_falsey
expect(-Float::INFINITY.infinite?).to be_truthy

However, it's not obvious which predicates can return non-boolean values in Ruby. I don't have a definitive list for core/stdlib, and can only name nonzero? and infinite? off the top of my head (broader list). And gems can have their pearls, too.

We may issue a warning when a predicate matcher is used, and the return value is neither true nor false, i.e.:

expect(123).to be_infinite
# Warning: `infitine?` predicate method called on `123` returned a non-boolean value. Consider using a less strict matcher instead `expect(123.infinite?).to be_truthy`

pirj avatar May 11 '21 20:05 pirj

Added a warning here:

expect(-Float::INFINITY).to be_infinite
`infinite?` returned neither `true` nor `false`, but rather `-1`

for easier migration to strict mode ones.

config.set_strict_predicate_matcher_mode :be_infinite, :truthy

I love the idea.

pirj avatar Jul 23 '21 19:07 pirj