Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RSpec/NoExpectationExample #1321

Merged
merged 8 commits into from
Jul 27, 2022
Merged

Conversation

r7kamura
Copy link
Contributor

@r7kamura r7kamura commented Jul 8, 2022

This cop checks that each example has any expectation.

fixes #1319

Example

# bad
it do
  a?
end

# good
it do
  expect(a?).to be_truthy
end

Background

Trying it out on my Rails project, I found the following bad cases by this Cop:

it do
end

it 'succeeds' do
  subject
end

xit 'returns 200' do
end

then I corrected the code like this:

- it do
- end

it 'succeeds' do
-   subject
+   expect { subject }.not_to raise_error
end

- xit 'returns 200' do
- end
+ xit 'returns 200'

Before submitting the PR make sure the following are checked:

  • Feature branch is up-to-date with master (if not - rebase it).
  • Squashed related commits together.
  • Added tests.
  • Updated documentation.
  • Added an entry to the CHANGELOG.md if the new code introduces user-observable changes.
  • The build (bundle exec rake) passes (be sure to run this locally, since it may produce updated documentation that you will need to commit).

If you have created a new cop:

  • Added the new cop to config/default.yml.
  • The cop is configured as Enabled: pending in config/default.yml.
  • The cop is configured as Enabled: true in .rubocop.yml.
  • The cop documents examples of good and bad code.
  • The tests assert both that bad code is reported and that good code is not reported.
  • Set VersionAdded in default/config.yml to the next minor version.

If you have modified an existing cop's configuration options:

  • Set VersionChanged in config/default.yml to the next major version.

@r7kamura
Copy link
Contributor Author

r7kamura commented Jul 8, 2022

Sorry I haven't read the related issues at all. I see that we had the same discussion a few days ago and even 10 years ago. It's just a coincidence that I created this pull request today.

config/default.yml Outdated Show resolved Hide resolved
@r7kamura r7kamura force-pushed the feature/empty-example branch 2 times, most recently from f9c5b23 to 1432383 Compare July 8, 2022 23:05
@r7kamura
Copy link
Contributor Author

r7kamura commented Jul 8, 2022

Thanks for the review comments! I learned a lot of information I didn't know 👍
I have made some improvements and hope you will take another look.

@r7kamura r7kamura requested review from pirj, Darhazer and koic July 9, 2022 11:56
@r7kamura r7kamura force-pushed the feature/empty-example branch 2 times, most recently from b05886d to 4db2c25 Compare July 10, 2022 06:26
Copy link
Member

@pirj pirj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel that this cop should implement autocorrection. WDYT?

.rubocop.yml Show resolved Hide resolved
# @param [RuboCop::AST::Node] node
# @return [Boolean]
def including_any_expectation?(node)
if !node.is_a?(::RuboCop::AST::Node)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What else can it be?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, if the parent node is s(:send, nil, :it), its children are [nil, :it], so it can be nil or a Symbol.

lib/rubocop/cop/rspec/no_expectation_example.rb Outdated Show resolved Hide resolved
lib/rubocop/cop/rspec/no_expectation_example.rb Outdated Show resolved Hide resolved
@pirj pirj requested a review from bquorning July 10, 2022 11:34
@r7kamura
Copy link
Contributor Author

I don't feel that this cop should implement autocorrection. WDYT?

I'm on the fence about that.

RSpec/EmptyExampleGroup, which has a similar concept, implements autocorrection, so I thought it might be a good idea for this cop to do the same. Sometimes this autocorrection is useful, sometimes it is not. This cop is marked as Safe: false, so it will not be unintentionally autocorrected unless users force it. But thoughtlessly erasing the code is indeed dangerous...

I actually used this cop's autocorrection in my Rails app because I had to change a large amount of code there, but I am thinking that maybe this autocorrection should be removed.

@pirj
Copy link
Member

pirj commented Jul 10, 2022

RSpec/EmptyExampleGroup, which has a similar concept, implements autocorrection, so I thought it might be a good idea for this cop to do the same

I think an empty example group has more chances to be a deliberate choice than the no expectations one. The latter is most likely a mistake.
For me, removing the empty example group doesn't seem to be an ideal autocorrection. But for the no expectations, it isn't for sure. Taking example from the NoExpectations ticket:

it { subject } # Just checks if calling `subject` doesn't raise any exceptions
it { validates_presence_of(:attribute) } # Oops, missing `is_expected.to`

It would be quite odd to remove those examples.

This cop is marked as Safe: false

I have to agree with this, unfortunately, with the Safe definition as "indicates whether the cop can yield false positives (by design) or not". It is unfortunate, though, that this cop would in most cases won't be run, and won't detect legitimate offences. Sadly, I don't have an idea how to overcome this.

that maybe this autocorrection should be removed

I fully support that.

Let me quickly run the cop against several codebases in real-world-rspec to see how severe and frequent false positives are.

@pirj
Copy link
Member

pirj commented Jul 10, 2022

57497 files inspected, 24233 offenses detected

I briefly went through, and tried categorizing cases that look like false positive/suspicious:

  1. Legacy RSpec expectation notation:
# /Users/pirj/source/real-world-rspec/loomio/spec/models/user_spec.rb
    it "returns the stored name if deactivated_at is nil" do
      user.name.should_not == 'Deleted user'
    end
# /Users/pirj/source/real-world-rspec/rspec-mocks/spec/rspec/mocks/configuration_spec.rb
          it "does not warn about the should syntax" do
            RSpec.should_not_receive(:deprecate)
            Object.new.should_not_receive(:bees)
          end
  1. Might be legitimate offences
# /Users/pirj/source/real-world-rspec/mastodon/spec/controllers/auth/registrations_controller_spec.rb
    it 'does nothing if user already exists' do
      Fabricate(:account, username: 'test')
      subject # Frankly, I'm struggling to understand where `subject` is defined
    end
# /Users/pirj/source/real-world-rspec/refinerycms/pages/spec/models/refinery/page_meta_data_spec.rb
      it 'meta_description' do
        page.respond_to?(:meta_description) # should be `expect(page).to respond_to(:meta_description)` ?
      end
  1. Expectation helper method
    it 'tracks when sign in is nil' do
      user.update(current_sign_in_at: nil)
      sign_in user, scope: :user
      get :show

      expect_updated_sign_in_at(user)
    end
  1. Skipped examples
      it "updates the requested expense" do
        expense = Expense.create! valid_attributes
        put :update, {:id => expense.to_param, :expense => new_attributes}, valid_session
        expense.reload
        skip("Add assertions for updated state")
      end
  1. Rails/Minitest-style assertions
# /Users/pirj/source/real-world-rspec/publify/publify_core/spec/controllers/admin/articles_controller_spec.rb
      it "sends notifications on create" do
        u = create(:user, notify_via_email: true, notify_on_new_articles: true)
        u.save!
        ActionMailer::Base.deliveries.clear
        emails = ActionMailer::Base.deliveries

        post :create, params: { "article" => base_article }

        assert_equal(1, emails.size)
        assert_equal(u.email, emails.first.to[0])
      end
  1. Alternative mocking library (e.g. Mocha)
# /Users/pirj/source/real-world-rspec/puppet/acceptance/lib/puppet/acceptance/classifier_utils_spec.rb
  it "provides a handle to the classifier service" do
    handle.expects(:perform_request).with(Net::HTTP::Get, '/hi', {})
    handle.get('/hi')
  end
  1. No explicit expect { ... }.not_to raise_error
# /Users/pirj/source/real-world-rspec/rspec-core/spec/rspec/core/backtrace_formatter_spec.rb
      it "deals gracefully with a security error" do
        Metadata.instance_eval { @relative_path_regex = nil }
        with_safe_set_to_level_that_triggers_security_errors do
          self.formatter.__send__(:backtrace_line, __FILE__)
          # on some rubies, this doesn't raise a SecurityError; this test just
          # assures that if it *does* raise an error, the error is caught inside
        end
      end
  1. Esoteric - definition of examples inside examples
# /Users/pirj/source/real-world-rspec/rspec-core/spec/rspec/core/configuration_spec.rb
      it 'successfully invokes the block' do
        RSpec.describe("group") { it "example 1" do; end}
        example = RSpec.world.example_groups.first.examples.first
        expect(example.metadata[:new_key]).to eq(:new_value)
      end
  1. Missing configuration for well-known expectations
    it 'does not register any offenses' do
      expect_no_offenses(<<~RUBY, 'foo.rb')
        # cop will not read these contents
        gem('rubocop')
        gem('rubocop')
      RUBY
    end

Can we put this:

RSpec:
  Language:
    Expectations:
      - expect_correction
      - expect_no_offenses
      - expect_offense

into some additional YAML file so rubocop and other extensions could just include this, and this would be used in both this cop and MultipleExpectations?

It looks like an immense effort to fix this all. With Safe: false, I believe it's ok to keep the cop as it is.
I wouldn't even care about the long-deprecated monkey-patching object.should RSpec syntax and Minitest-style assertions.

Copy link
Member

@pirj pirj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks!

Copy link
Member

@Darhazer Darhazer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cop itself looks fine. I'm worried about extending the Language though

@@ -61,9 +61,14 @@ RSpec:
Pending:
- pending
Expectations:
- are_expected
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change in Language potentially affects multiple cops (ExpectInHook, MultipleExpectations. StubbedMock). Perhaps it's worth mentioning it in the changelog. Also I'm not sure we should support the should expectations at all

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bquorning what do you think about extending the default language?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven’t personally used the ‘should’ syntax since RSpec 3 was released, but from the RSpec documentation it sounds like it’s not being removed, even in v4. So I guess we should support it?

@pirj I imagine you might know about official RSpec plans?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All forms of should will be removed from RSpec 4 with no replacement.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I would not keep the burden of supporting should, as it was soft-deprecated nine years ago and was disabled in the generated config since then:

For RSpec 3, we considered the idea of disabling the old syntax by default, forcing users to opt-in to use it.

But I still see people use it, even those who joined software development less than nine years ago 😄

it 'registers no offenses' do
expect_no_offenses(<<~RUBY)
RSpec.describe Foo do
pending 'not implemented'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps we should skip checking of pending or skipped examples in the first place.

In response to the review comments, I added the following changes later:

- Prefer x_type? to is_a?
- Use RSpec.Language.Examples
- Add are_expected to expectation method names
- Prefer on_block to on_send
- Add more methods to RSpec.Language.Expectations
- Use RSpec.Language.Expectations
- Replace with clearer custom method names
- Add some missing \@return comments
@pirj pirj requested a review from Darhazer July 26, 2022 20:55
@pirj pirj merged commit 291f1fd into rubocop:master Jul 27, 2022
@pirj
Copy link
Member

pirj commented Jul 27, 2022

Thank you, @r7kamura !

@r7kamura r7kamura deleted the feature/empty-example branch July 27, 2022 11:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cop idea: NoExpectations
5 participants