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

Raise an error when calling custom matcher without using `.and` #1426

Open AlecksJohannes opened 1 year ago

AlecksJohannes commented 1 year ago

Subject of the issue

Hi there folks 👋 ,

First of all, thank you for this great gem! I created this issue here just to offer a small idea, I encountered a small scenario when using custom matchers.

Let's say we have this custom matcher

RSpec::Matchers.define(:create_some_model) do
  chain(:method1) {}
  chain(:method2) {}
  chain(:method3) {}
  ...
end

describe do
  it do
    expect { some_action }.to create_some_model(Model).method1.method2.method3.create_some_model(Model2)
  end
end

As you can see, it is easy to make mistake as we sometime might forget to add .and between create_some_model and this could create false-positive test result as it will only run create_some_model(Model2). I understand that this is more human error but I would love to prevent it.

My idea is in RSpec::Matchers::DSL::Matcher, we also define

class RSpec::Matchers::DSL::Matcher
  define_method(method) do
    raise "You did not use .and between your 'create_some_model' test expectation"
  end
end

So calling create_some_model twice will resulting in error like that. There is one catch with this approach is you cannot call the same create_some_model inside the matcher itself but IMHO, I don't think it is a good idea to do such

RSpec::Matchers.define(:create_some_model) do
  chain(:method1) {}
  chain(:method2) {}
  chain(:method3) {}

 def something
   expect { }.to create_some_model <-- this doesn't work
 end
  ...
end

I can create a PR to apply this idea, please let me know what do you guys think. Thank you for reading!

P/S:

There is 1 more approach besides that is to get rid of the defined custom matcher method from RSpec::Matchers::DSL::Matcher but if we do that we also have to get rid of the method_missing as well which I don't think it is a good idea.

Your environment

pirj commented 1 year ago

Thanks for reporting! It’s certainly accidental behaviour. Wondering if it’s possible to play with access levels to disallow explicit receiver. I’m pretty sure there were cases where yhe matcher was called recursively. If it would be possible to keep this and issue a warning, that would be perfect. A PR to fix this is welcome!

AlecksJohannes commented 1 year ago

Thanks for reporting! It’s certainly accidental behaviour. Wondering if it’s possible to play with access levels to disallow explicit receiver. I’m pretty sure there were cases where yhe matcher was called recursively. If it would be possible to keep this and issue a warning, that would be perfect. A PR to fix this is welcome!

Hello there! I hope you are enjoying your weekend ☺️

Surely, I had the same idea I will play around with access levels to see if I can somehow do that, otherwise if it is too complicated then I believe we can raise it as a warning instead of error (will also create a PR for that)

P/S (12/8/2023): I believe I have the solution, I will open a PR soon.

AlecksJohannes commented 1 year ago

Hello there @pirj 👋 I hope you are doing well, I have opened a PR for this issue. Thank you!

JonRowe commented 1 year ago

The method should not even be available on the matcher for what its worth, e.g .eq(thing).eq will raise a NoMethodError

AlecksJohannes commented 1 year ago

Hey folks, I hope you are all well!

I have left a comment here https://github.com/rspec/rspec-expectations/pull/1428#issuecomment-1681781748, could you guys have a look ? Thank you