even_better_specs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +17 -0
- data/README.md +50 -0
- data/config/default.yml +116 -0
- data/exe/even_better_specs +7 -0
- data/lib/even_better_specs/version.rb +5 -0
- data/lib/even_better_specs.rb +11 -0
- data/lib/rubocop/cop/even_better_specs/all_possible_cases.rb +37 -0
- data/lib/rubocop/cop/even_better_specs/create_only_needed_data.rb +30 -0
- data/lib/rubocop/cop/even_better_specs/describe_method.rb +32 -0
- data/lib/rubocop/cop/even_better_specs/expect_syntax.rb +26 -0
- data/lib/rubocop/cop/even_better_specs/factories_not_fixtures.rb +24 -0
- data/lib/rubocop/cop/even_better_specs/group_expectations.rb +56 -0
- data/lib/rubocop/cop/even_better_specs/no_hooks.rb +28 -0
- data/lib/rubocop/cop/even_better_specs/no_let.rb +26 -0
- data/lib/rubocop/cop/even_better_specs/no_shared_examples.rb +34 -0
- data/lib/rubocop/cop/even_better_specs/redundant_require.rb +39 -0
- data/lib/rubocop/cop/even_better_specs/request_specs.rb +25 -0
- data/lib/rubocop/cop/even_better_specs/short_description.rb +41 -0
- data/lib/rubocop/cop/even_better_specs/stub_env.rb +26 -0
- data/lib/rubocop/cop/even_better_specs/use_subject.rb +27 -0
- metadata +97 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1654cb014ec4bfbe5ac31e7f75259ff4f161041cd1fb15e2658a279b4ceb1f01
|
|
4
|
+
data.tar.gz: f5f45ee95234cf0631612cb9a4f48946fff2df2415fcd80aceb5b295b619dba3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 672498e77b41332d9f1d8510505b5edde0bfb227cd5168fc858beaa21a0d506d2e6dd536d06fce90d24c2399e52fed993ac84e6a46250a7e5d7894878e9887c8
|
|
7
|
+
data.tar.gz: 6c38c8e78159a86f6358dae83faacd41fe246c70104ba0700b7d2a5b9ccfd3db36cbd90acf1c4ca636e5f86a43ec4f4ab447f1c69d0f23dace9873a938adc8b4
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
|
+
and this project follows [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-10-30
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `even_better_specs` CLI that runs with its own config, so it works in projects without rubocop in their Gemfile.
|
|
14
|
+
- Cops for the Even Better Specs guidelines: `DescribeMethod`, `FactoriesNotFixtures`, `UseSubject`,
|
|
15
|
+
`ShortDescription`, `GroupExpectations`, `AllPossibleCases` (info severity), `RequestSpecs`, `ExpectSyntax`,
|
|
16
|
+
`RedundantRequire` (autocorrectable), `NoLet`, `NoHooks`, `NoSharedExamples`, `StubEnv`, `CreateOnlyNeededData`.
|
|
17
|
+
- rubocop-rspec cops enabled: `DescribeClass`, `ContextMethod`, `ContextWording`, `DescribedClass`, `VerifiedDoubles`.
|
data/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Even Better Specs
|
|
2
|
+
|
|
3
|
+
RuboCop cops for the [Even Better Specs](https://evenbetterspecs.github.io) guidelines, with a CLI
|
|
4
|
+
that runs in any project, including Rails apps that do not have `rubocop` in their Gemfile.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
Install the gem globally (do **not** add it to the Gemfile) and run it from the project root:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
gem install even_better_specs
|
|
12
|
+
even_better_specs # checks every *_spec.rb
|
|
13
|
+
even_better_specs spec/models
|
|
14
|
+
even_better_specs -a # autocorrect what can be autocorrected
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Do not use `bundle exec`, because the gem is not in the Gemfile. The CLI accepts all `rubocop` options.
|
|
18
|
+
It runs with its own config only, so the project's `.rubocop.yml` (if any) is ignored.
|
|
19
|
+
|
|
20
|
+
## Rules
|
|
21
|
+
|
|
22
|
+
| Guideline | Cop |
|
|
23
|
+
|---|---|
|
|
24
|
+
| Describe what you are testing | `RSpec/DescribeClass`, `EvenBetterSpecs/DescribeMethod` |
|
|
25
|
+
| Use contexts | `RSpec/ContextMethod`, `RSpec/ContextWording` (`when`) |
|
|
26
|
+
| Factories, not fixtures | `EvenBetterSpecs/FactoriesNotFixtures` |
|
|
27
|
+
| Leverage described class | `RSpec/DescribedClass` |
|
|
28
|
+
| Use subject | `EvenBetterSpecs/UseSubject` |
|
|
29
|
+
| Short description | `EvenBetterSpecs/ShortDescription` (`Max: 100`) |
|
|
30
|
+
| Group expectations | `EvenBetterSpecs/GroupExpectations` |
|
|
31
|
+
| All possible cases | `EvenBetterSpecs/AllPossibleCases` (flags a `describe` with a single `context`; reported as `info`, so it does not fail the run) |
|
|
32
|
+
| Request vs controller specs | `EvenBetterSpecs/RequestSpecs` |
|
|
33
|
+
| Expect vs should syntax | `EvenBetterSpecs/ExpectSyntax` |
|
|
34
|
+
| Redundant require | `EvenBetterSpecs/RedundantRequire` (autocorrectable) |
|
|
35
|
+
| Instance double over double | `RSpec/VerifiedDoubles` |
|
|
36
|
+
| Let's not | `EvenBetterSpecs/NoLet` |
|
|
37
|
+
| Avoid hooks | `EvenBetterSpecs/NoHooks` |
|
|
38
|
+
| Don't use shared examples | `EvenBetterSpecs/NoSharedExamples` |
|
|
39
|
+
| Stub environment variables | `EvenBetterSpecs/StubEnv` (flags writes to `ENV`) |
|
|
40
|
+
| Create only the data you need | `EvenBetterSpecs/CreateOnlyNeededData` (`*_list` above `Max: 2`) |
|
|
41
|
+
|
|
42
|
+
"Mock external dependencies" and "Stub HTTP requests" cannot be detected statically. For HTTP, use
|
|
43
|
+
`WebMock.disable_net_connect!` in `spec_helper.rb` so unstubbed requests fail.
|
|
44
|
+
|
|
45
|
+
## Development
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
bundle install
|
|
49
|
+
bundle exec rspec
|
|
50
|
+
```
|
data/config/default.yml
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Even Better Specs: https://evenbetterspecs.github.io
|
|
2
|
+
plugins:
|
|
3
|
+
- rubocop-rspec
|
|
4
|
+
|
|
5
|
+
AllCops:
|
|
6
|
+
DisabledByDefault: true
|
|
7
|
+
NewCops: disable
|
|
8
|
+
SuggestExtensions: false
|
|
9
|
+
Include:
|
|
10
|
+
- "**/*_spec.rb"
|
|
11
|
+
|
|
12
|
+
# Describe what you are testing
|
|
13
|
+
RSpec/DescribeClass:
|
|
14
|
+
Enabled: true
|
|
15
|
+
# Unlike rubocop-rspec's default, request specs must describe the controller constant.
|
|
16
|
+
Exclude:
|
|
17
|
+
- "**/spec/features/**/*"
|
|
18
|
+
- "**/spec/routing/**/*"
|
|
19
|
+
- "**/spec/system/**/*"
|
|
20
|
+
- "**/spec/views/**/*"
|
|
21
|
+
IgnoredMetadata:
|
|
22
|
+
type:
|
|
23
|
+
- routing
|
|
24
|
+
- view
|
|
25
|
+
- feature
|
|
26
|
+
- system
|
|
27
|
+
- aruba
|
|
28
|
+
- task
|
|
29
|
+
EvenBetterSpecs/DescribeMethod:
|
|
30
|
+
Description: Nested `describe` blocks name a method using `.` or `#`.
|
|
31
|
+
Enabled: true
|
|
32
|
+
|
|
33
|
+
# Use contexts
|
|
34
|
+
RSpec/ContextMethod:
|
|
35
|
+
Enabled: true
|
|
36
|
+
RSpec/ContextWording:
|
|
37
|
+
Enabled: true
|
|
38
|
+
Prefixes:
|
|
39
|
+
- when
|
|
40
|
+
|
|
41
|
+
# Factories, not fixtures
|
|
42
|
+
EvenBetterSpecs/FactoriesNotFixtures:
|
|
43
|
+
Description: Use factories instead of fixtures.
|
|
44
|
+
Enabled: true
|
|
45
|
+
|
|
46
|
+
# Leverage described class
|
|
47
|
+
RSpec/DescribedClass:
|
|
48
|
+
Enabled: true
|
|
49
|
+
|
|
50
|
+
# Use subject
|
|
51
|
+
EvenBetterSpecs/UseSubject:
|
|
52
|
+
Description: Use `subject` instead of `described_class.new` without arguments.
|
|
53
|
+
Enabled: true
|
|
54
|
+
|
|
55
|
+
# Short description
|
|
56
|
+
EvenBetterSpecs/ShortDescription:
|
|
57
|
+
Description: Descriptions are short and on a single line.
|
|
58
|
+
Enabled: true
|
|
59
|
+
Max: 100
|
|
60
|
+
|
|
61
|
+
# Group expectations
|
|
62
|
+
EvenBetterSpecs/GroupExpectations:
|
|
63
|
+
Description: Sibling examples with the same setup are merged into one.
|
|
64
|
+
Enabled: true
|
|
65
|
+
|
|
66
|
+
# All possible cases
|
|
67
|
+
EvenBetterSpecs/AllPossibleCases:
|
|
68
|
+
Description: A `describe` has more than one `context`.
|
|
69
|
+
Enabled: true
|
|
70
|
+
# Heuristic: it reports but does not fail the run.
|
|
71
|
+
Severity: info
|
|
72
|
+
|
|
73
|
+
# Request vs controller specs
|
|
74
|
+
EvenBetterSpecs/RequestSpecs:
|
|
75
|
+
Description: Write request specs instead of controller specs.
|
|
76
|
+
Enabled: true
|
|
77
|
+
|
|
78
|
+
# Expect vs should syntax
|
|
79
|
+
EvenBetterSpecs/ExpectSyntax:
|
|
80
|
+
Description: Use `expect` instead of `should`.
|
|
81
|
+
Enabled: true
|
|
82
|
+
|
|
83
|
+
# Redundant require
|
|
84
|
+
EvenBetterSpecs/RedundantRequire:
|
|
85
|
+
Description: Require helpers from `.rspec`, not from each spec file.
|
|
86
|
+
Enabled: true
|
|
87
|
+
|
|
88
|
+
# Instance double over double
|
|
89
|
+
RSpec/VerifiedDoubles:
|
|
90
|
+
Enabled: true
|
|
91
|
+
|
|
92
|
+
# Let's not
|
|
93
|
+
EvenBetterSpecs/NoLet:
|
|
94
|
+
Description: Do not use `let` / `let!`.
|
|
95
|
+
Enabled: true
|
|
96
|
+
|
|
97
|
+
# Avoid hooks
|
|
98
|
+
EvenBetterSpecs/NoHooks:
|
|
99
|
+
Description: Avoid hooks.
|
|
100
|
+
Enabled: true
|
|
101
|
+
|
|
102
|
+
# Don't use shared examples
|
|
103
|
+
EvenBetterSpecs/NoSharedExamples:
|
|
104
|
+
Description: Do not use shared examples or shared contexts.
|
|
105
|
+
Enabled: true
|
|
106
|
+
|
|
107
|
+
# Stub environment variables
|
|
108
|
+
EvenBetterSpecs/StubEnv:
|
|
109
|
+
Description: Stub environment variables instead of changing `ENV`.
|
|
110
|
+
Enabled: true
|
|
111
|
+
|
|
112
|
+
# Create only the data you need
|
|
113
|
+
EvenBetterSpecs/CreateOnlyNeededData:
|
|
114
|
+
Description: Do not create more records than the example needs.
|
|
115
|
+
Enabled: true
|
|
116
|
+
Max: 3
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubocop"
|
|
4
|
+
require "rubocop-rspec"
|
|
5
|
+
require_relative "even_better_specs/version"
|
|
6
|
+
|
|
7
|
+
module EvenBetterSpecs
|
|
8
|
+
CONFIG = File.expand_path("../config/default.yml", __dir__)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Dir[File.join(__dir__, "rubocop/cop/even_better_specs/*.rb")].each { |file| require file }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module EvenBetterSpecs
|
|
6
|
+
# A `describe` with a single `context` most likely misses the edge and invalid cases.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# describe '#destroy' do
|
|
11
|
+
# context 'when the product exists' do
|
|
12
|
+
# end
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# # good
|
|
16
|
+
# describe '#destroy' do
|
|
17
|
+
# context 'when the product exists' do
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# context 'when the product does not exist' do
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
class AllPossibleCases < RuboCop::Cop::RSpec::Base
|
|
24
|
+
MSG = "Only one `context` here. Test valid, edge and invalid cases."
|
|
25
|
+
|
|
26
|
+
def_node_matcher :describe?, "(block (send #rspec? :describe ...) ...)"
|
|
27
|
+
def_node_matcher :context?, "(block (send nil? :context ...) ...)"
|
|
28
|
+
|
|
29
|
+
def on_block(node)
|
|
30
|
+
return unless describe?(node) && node.body && context?(node.body)
|
|
31
|
+
|
|
32
|
+
add_offense(node.send_node)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module EvenBetterSpecs
|
|
6
|
+
# Create only the data you need. Flags `*_list` factory calls above `Max` records.
|
|
7
|
+
#
|
|
8
|
+
# @example Max: 2 (default)
|
|
9
|
+
# # bad
|
|
10
|
+
# create_list(:product, 5)
|
|
11
|
+
#
|
|
12
|
+
# # good
|
|
13
|
+
# create(:product, featured: false)
|
|
14
|
+
class CreateOnlyNeededData < Base
|
|
15
|
+
MSG = "Create only the data you need (%<count>d records, max %<max>d)."
|
|
16
|
+
RESTRICT_ON_SEND = %i[create_list build_list build_stubbed_list attributes_for_list].freeze
|
|
17
|
+
|
|
18
|
+
def_node_matcher :list_count, "(send {nil? (const {nil? cbase} :FactoryBot)} _ _ (int $_) ...)"
|
|
19
|
+
|
|
20
|
+
def on_send(node)
|
|
21
|
+
count = list_count(node)
|
|
22
|
+
max = cop_config.fetch("Max", 3)
|
|
23
|
+
return unless count && count > max
|
|
24
|
+
|
|
25
|
+
add_offense(node, message: format(MSG, count: count, max: max))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module EvenBetterSpecs
|
|
6
|
+
# Nested `describe` blocks name a method: `.class_method` or `#instance_method`.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# describe 'the authenticate method for User' do
|
|
11
|
+
# describe 'GET #index' do
|
|
12
|
+
#
|
|
13
|
+
# # good
|
|
14
|
+
# describe '.authenticate' do
|
|
15
|
+
# describe '#index' do
|
|
16
|
+
class DescribeMethod < RuboCop::Cop::RSpec::Base
|
|
17
|
+
MSG = "Describe a method using `.class_method` or `#instance_method`, or use `context`."
|
|
18
|
+
|
|
19
|
+
def_node_matcher :describe_with_string, "(block (send #rspec? :describe $str ...) ...)"
|
|
20
|
+
|
|
21
|
+
def on_block(node)
|
|
22
|
+
description = describe_with_string(node)
|
|
23
|
+
return unless description
|
|
24
|
+
return if description.value.start_with?(".", "#")
|
|
25
|
+
return unless node.each_ancestor(:block).any? { |ancestor| example_group?(ancestor) }
|
|
26
|
+
|
|
27
|
+
add_offense(description)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module EvenBetterSpecs
|
|
6
|
+
# Always use `expect` instead of `should`.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# response.code.should eq('200')
|
|
11
|
+
# it { should eq('200') }
|
|
12
|
+
#
|
|
13
|
+
# # good
|
|
14
|
+
# expect(response.code).to eq('200')
|
|
15
|
+
# it { is_expected.to eq('200') }
|
|
16
|
+
class ExpectSyntax < Base
|
|
17
|
+
MSG = "Use `expect` instead of `%<method>s`."
|
|
18
|
+
RESTRICT_ON_SEND = %i[should should_not].freeze
|
|
19
|
+
|
|
20
|
+
def on_send(node)
|
|
21
|
+
add_offense(node.loc.selector, message: format(MSG, method: node.method_name))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module EvenBetterSpecs
|
|
6
|
+
# Use factories instead of fixtures.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# fixtures :users
|
|
11
|
+
#
|
|
12
|
+
# # good
|
|
13
|
+
# user = create(:user)
|
|
14
|
+
class FactoriesNotFixtures < Base
|
|
15
|
+
MSG = "Use factories instead of fixtures."
|
|
16
|
+
RESTRICT_ON_SEND = %i[fixtures].freeze
|
|
17
|
+
|
|
18
|
+
def on_send(node)
|
|
19
|
+
add_offense(node) if node.receiver.nil?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module EvenBetterSpecs
|
|
6
|
+
# Sibling examples with the same arrange and act steps should be one example
|
|
7
|
+
# with several expectations.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad
|
|
11
|
+
# it 'returns 200' do
|
|
12
|
+
# get user_path(user)
|
|
13
|
+
# expect(response.code).to eq('200')
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# it 'returns JSON content type' do
|
|
17
|
+
# get user_path(user)
|
|
18
|
+
# expect(response.content_type).to eq('application/json')
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# # good
|
|
22
|
+
# it 'responds with 200 http status and a JSON content type' do
|
|
23
|
+
# get user_path(user)
|
|
24
|
+
# expect(response.code).to eq('200')
|
|
25
|
+
# expect(response.content_type).to eq('application/json')
|
|
26
|
+
# end
|
|
27
|
+
class GroupExpectations < RuboCop::Cop::RSpec::Base
|
|
28
|
+
MSG = "Same setup as a sibling example. Group the expectations in a single example."
|
|
29
|
+
EXPECTATIONS = %i[expect expect_any_instance_of is_expected are_expected should should_not].freeze
|
|
30
|
+
|
|
31
|
+
def on_block(node)
|
|
32
|
+
return unless example_group_with_body?(node)
|
|
33
|
+
|
|
34
|
+
seen = {}
|
|
35
|
+
node.body.each_child_node(:block).select { |child| example?(child) }.each do |example|
|
|
36
|
+
setup = setup_source(example)
|
|
37
|
+
next if setup.empty?
|
|
38
|
+
|
|
39
|
+
seen.key?(setup) ? add_offense(example.send_node) : seen[setup] = true
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def setup_source(example)
|
|
46
|
+
statements = example.body&.begin_type? ? example.body.children : [example.body].compact
|
|
47
|
+
statements.reject { |statement| expectation?(statement) }.map(&:source).join("\n")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def expectation?(statement)
|
|
51
|
+
statement.each_node(:send).any? { |send| EXPECTATIONS.include?(send.method_name) }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module EvenBetterSpecs
|
|
6
|
+
# Avoid hooks. Arrange everything inside each example.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# before do
|
|
11
|
+
# sign_in user
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# # good
|
|
15
|
+
# it 'returns 200' do
|
|
16
|
+
# sign_in user
|
|
17
|
+
# end
|
|
18
|
+
class NoHooks < RuboCop::Cop::RSpec::Base
|
|
19
|
+
MSG = "Avoid hooks. Arrange everything inside the example."
|
|
20
|
+
|
|
21
|
+
def on_block(node)
|
|
22
|
+
add_offense(node.send_node) if hook?(node)
|
|
23
|
+
end
|
|
24
|
+
alias on_numblock on_block
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module EvenBetterSpecs
|
|
6
|
+
# Do not use `let` / `let!`. Define the data inside each example.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# let(:user) { build(:user) }
|
|
11
|
+
#
|
|
12
|
+
# # good
|
|
13
|
+
# it 'returns the full name' do
|
|
14
|
+
# user = build(:user)
|
|
15
|
+
# end
|
|
16
|
+
class NoLet < Base
|
|
17
|
+
MSG = "Do not use `%<method>s`. Define the data inside the example."
|
|
18
|
+
RESTRICT_ON_SEND = %i[let let!].freeze
|
|
19
|
+
|
|
20
|
+
def on_send(node)
|
|
21
|
+
add_offense(node, message: format(MSG, method: node.method_name)) if node.receiver.nil?
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module EvenBetterSpecs
|
|
6
|
+
# Do not use shared examples or shared contexts.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# shared_examples 'a barking animal' do
|
|
11
|
+
# it_behaves_like 'a barking animal'
|
|
12
|
+
#
|
|
13
|
+
# # good
|
|
14
|
+
# it 'barks' do
|
|
15
|
+
# expect(subject.able_to_bark?).to eq(true)
|
|
16
|
+
# end
|
|
17
|
+
class NoSharedExamples < Base
|
|
18
|
+
MSG = "Do not use `%<method>s`. Write self-contained examples."
|
|
19
|
+
RESTRICT_ON_SEND = %i[
|
|
20
|
+
shared_examples shared_examples_for shared_context
|
|
21
|
+
it_behaves_like it_should_behave_like include_examples include_context
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
def_node_matcher :rspec_receiver?, "{nil? (const {nil? cbase} :RSpec)}"
|
|
25
|
+
|
|
26
|
+
def on_send(node)
|
|
27
|
+
return unless rspec_receiver?(node.receiver)
|
|
28
|
+
|
|
29
|
+
add_offense(node, message: format(MSG, method: node.method_name))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module EvenBetterSpecs
|
|
6
|
+
# Remove `require 'rails_helper'` / `require 'spec_helper'` from spec files.
|
|
7
|
+
# Use `--require rails_helper` in `.rspec` instead.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad
|
|
11
|
+
# require 'rails_helper'
|
|
12
|
+
#
|
|
13
|
+
# describe User do
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# # good
|
|
17
|
+
# describe User do
|
|
18
|
+
# end
|
|
19
|
+
class RedundantRequire < Base
|
|
20
|
+
extend AutoCorrector
|
|
21
|
+
include RangeHelp
|
|
22
|
+
|
|
23
|
+
MSG = "Remove redundant `require`. Use `--require %<helper>s` in `.rspec` instead."
|
|
24
|
+
RESTRICT_ON_SEND = %i[require].freeze
|
|
25
|
+
|
|
26
|
+
def_node_matcher :helper_require, "(send nil? :require (str ${\"rails_helper\" \"spec_helper\"}))"
|
|
27
|
+
|
|
28
|
+
def on_send(node)
|
|
29
|
+
helper = helper_require(node)
|
|
30
|
+
return unless helper
|
|
31
|
+
|
|
32
|
+
add_offense(node, message: format(MSG, helper: helper)) do |corrector|
|
|
33
|
+
corrector.remove(range_with_surrounding_space(node.source_range, side: :right))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module EvenBetterSpecs
|
|
6
|
+
# Write request specs instead of controller specs.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# describe UsersController, type: :controller do
|
|
11
|
+
#
|
|
12
|
+
# # good
|
|
13
|
+
# describe UsersController, type: :request do
|
|
14
|
+
class RequestSpecs < Base
|
|
15
|
+
MSG = "Write request specs instead of controller specs."
|
|
16
|
+
|
|
17
|
+
def_node_matcher :controller_type?, "(pair (sym :type) (sym :controller))"
|
|
18
|
+
|
|
19
|
+
def on_pair(node)
|
|
20
|
+
add_offense(node) if controller_type?(node)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module EvenBetterSpecs
|
|
6
|
+
# Descriptions must not be longer than `Max` characters nor span many lines.
|
|
7
|
+
# Split them using `context`.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad
|
|
11
|
+
# it 'returns 422 when user first_name is missing from params and when user last_name is missing' do
|
|
12
|
+
#
|
|
13
|
+
# # good
|
|
14
|
+
# context 'when user first_name is missing from params' do
|
|
15
|
+
# it 'returns 422' do
|
|
16
|
+
class ShortDescription < RuboCop::Cop::RSpec::Base
|
|
17
|
+
MSG = "Keep descriptions under %<max>d characters and on a single line. Split them using `context`."
|
|
18
|
+
|
|
19
|
+
def_node_matcher :description, "(block (send #rspec? _ ${str dstr} ...) ...)"
|
|
20
|
+
|
|
21
|
+
def on_block(node)
|
|
22
|
+
return unless example?(node) || example_group?(node)
|
|
23
|
+
|
|
24
|
+
text = description(node)
|
|
25
|
+
return unless text
|
|
26
|
+
|
|
27
|
+
length = text.str_type? ? text.value.length : text.source.length
|
|
28
|
+
return unless text.multiline? || length > max
|
|
29
|
+
|
|
30
|
+
add_offense(text, message: format(MSG, max: max))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def max
|
|
36
|
+
cop_config.fetch("Max", 100)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module EvenBetterSpecs
|
|
6
|
+
# Stub environment variables instead of changing `ENV`.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# ENV['SALES_TEAM_EMAIL'] = 'sales-group@company.com'
|
|
11
|
+
#
|
|
12
|
+
# # good
|
|
13
|
+
# stub_env('SALES_TEAM_EMAIL', 'sales-group@company.com')
|
|
14
|
+
class StubEnv < Base
|
|
15
|
+
MSG = "Stub environment variables (e.g. `stub_env`) instead of changing `ENV`."
|
|
16
|
+
RESTRICT_ON_SEND = %i[[]= store update merge! delete replace clear].freeze
|
|
17
|
+
|
|
18
|
+
def_node_matcher :env?, "(const {nil? cbase} :ENV)"
|
|
19
|
+
|
|
20
|
+
def on_send(node)
|
|
21
|
+
add_offense(node) if env?(node.receiver)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module EvenBetterSpecs
|
|
6
|
+
# Use `subject` instead of `described_class.new` when no argument is given.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# calculator = described_class.new
|
|
11
|
+
#
|
|
12
|
+
# # good
|
|
13
|
+
# subject.add(1, 2)
|
|
14
|
+
# subject = described_class.new(5)
|
|
15
|
+
class UseSubject < Base
|
|
16
|
+
MSG = "Use `subject` instead of `described_class.new` without arguments."
|
|
17
|
+
RESTRICT_ON_SEND = %i[new].freeze
|
|
18
|
+
|
|
19
|
+
def_node_matcher :described_class_new?, "(send (send nil? :described_class) :new)"
|
|
20
|
+
|
|
21
|
+
def on_send(node)
|
|
22
|
+
add_offense(node) if described_class_new?(node)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: even_better_specs
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Glauco Custodio
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-30 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rubocop
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.72'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.72'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rubocop-rspec
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.5'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.5'
|
|
41
|
+
description: Enforces https://evenbetterspecs.github.io with RuboCop. Ships its own
|
|
42
|
+
CLI so it runs in projects that do not have rubocop in their Gemfile.
|
|
43
|
+
email:
|
|
44
|
+
- glauco.custodio@gmail.com
|
|
45
|
+
executables:
|
|
46
|
+
- even_better_specs
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- CHANGELOG.md
|
|
51
|
+
- README.md
|
|
52
|
+
- config/default.yml
|
|
53
|
+
- exe/even_better_specs
|
|
54
|
+
- lib/even_better_specs.rb
|
|
55
|
+
- lib/even_better_specs/version.rb
|
|
56
|
+
- lib/rubocop/cop/even_better_specs/all_possible_cases.rb
|
|
57
|
+
- lib/rubocop/cop/even_better_specs/create_only_needed_data.rb
|
|
58
|
+
- lib/rubocop/cop/even_better_specs/describe_method.rb
|
|
59
|
+
- lib/rubocop/cop/even_better_specs/expect_syntax.rb
|
|
60
|
+
- lib/rubocop/cop/even_better_specs/factories_not_fixtures.rb
|
|
61
|
+
- lib/rubocop/cop/even_better_specs/group_expectations.rb
|
|
62
|
+
- lib/rubocop/cop/even_better_specs/no_hooks.rb
|
|
63
|
+
- lib/rubocop/cop/even_better_specs/no_let.rb
|
|
64
|
+
- lib/rubocop/cop/even_better_specs/no_shared_examples.rb
|
|
65
|
+
- lib/rubocop/cop/even_better_specs/redundant_require.rb
|
|
66
|
+
- lib/rubocop/cop/even_better_specs/request_specs.rb
|
|
67
|
+
- lib/rubocop/cop/even_better_specs/short_description.rb
|
|
68
|
+
- lib/rubocop/cop/even_better_specs/stub_env.rb
|
|
69
|
+
- lib/rubocop/cop/even_better_specs/use_subject.rb
|
|
70
|
+
homepage: https://evenbetterspecs.github.io
|
|
71
|
+
licenses:
|
|
72
|
+
- MIT
|
|
73
|
+
metadata:
|
|
74
|
+
source_code_uri: https://github.com/evenbetterspecs/even_better_specs
|
|
75
|
+
changelog_uri: https://github.com/evenbetterspecs/even_better_specs/blob/main/CHANGELOG.md
|
|
76
|
+
bug_tracker_uri: https://github.com/evenbetterspecs/even_better_specs/issues
|
|
77
|
+
rubygems_mfa_required: 'true'
|
|
78
|
+
post_install_message:
|
|
79
|
+
rdoc_options: []
|
|
80
|
+
require_paths:
|
|
81
|
+
- lib
|
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - ">="
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '3.1'
|
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '0'
|
|
92
|
+
requirements: []
|
|
93
|
+
rubygems_version: 3.5.16
|
|
94
|
+
signing_key:
|
|
95
|
+
specification_version: 4
|
|
96
|
+
summary: RuboCop cops for the Even Better Specs guidelines
|
|
97
|
+
test_files: []
|