rspec / rspec-expectations

Provides a readable API to express expected outcomes of a code example
https://rspec.info
MIT License
1.26k stars 397 forks source link

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

Open pirj opened 3 years ago

pirj commented 3 years ago

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 commented 3 years ago

@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:

JonRowe commented 3 years ago

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

fledman commented 3 years ago

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 commented 3 years ago

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

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

semantics:

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)
benoittgt commented 3 years ago

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

pirj commented 3 years ago

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 commented 3 years ago

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.