-
-
Notifications
You must be signed in to change notification settings - Fork 118
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
Use before
and let
when it's beneficial
#56
Comments
I disagree with this, because quite frequently a single example (as you showed) turns into multiple examples. When # bad
describe '#filename' do
it 'returns filename' do
filename = 'input.csv'
settings.load!(filename)
expect(settings.filename).to eq('input.csv')
end
end
# good
describe Settings do
subject(:settings) { described_class.new(filename) }
let(:filename) { 'input.csv' }
describe '#filename' do
let(:filename) { 'a_different_filename.csv' }
it 'returns the filename provided to the constructor' do
expect(settings.filename).to eq('a_different_filename.csv')
end
end
end While this example is somewhat contrived, when more examples are added to the module, it's easier to understand how the Settings object is instantiated and how it acts as a whole. describe Settings do
subject(:settings) { described_class.new(filename, mode) }
let(:filename) { 'input.csv' }
let(:mode) { 'default_mode' }
describe '#filename' do
let(:filename) { 'a_different_filename.csv' }
it 'returns the filename provided to the constructor' do
expect(settings.filename).to eq('a_different_filename.csv')
end
end
describe '#special_mode?' do
subject { settings.special_mode? }
context 'when "default_mode" was specified' do
let(:mode) { 'default_mode' }
it { is_expected.to be_falsey }
end
context 'when "special_mode" was specified' do
let(:mode) { 'special_mode' }
it { is_expected.to be_truthy }
end
end
end |
I believe your example @RobinDaugherty falls into a category of specs that have a compelling benefit, while the one from description doesn't yet. There's yet another recommendation that is mentioned in the Effective testing with RSpec book, that might be used to flatten the about-to-happen combinatorial explosion. |
@RobinDaugherty I actually have a harder time understanding your example as written. There are several places in the test file where global state is being aggregated and overwritten, such that it's a little hard for me to put together a complete picture of what is being tested and what code executes before what. I generally don't shy away from code duplication in my tests, and rather avoid indirection. I'd probably write the spec above something like: describe Settings do
describe '#filename' do
it 'returns the filename passed to the constructor' do
filename = 'a_different_filename.csv'
settings = described_class.new(filename, 'default_mode')
expect(settings.filename).to eq(filename)
end
end
describe '#special_mode?' do
it 'returns false when "default_mode" is passed to the constructor' do
settings = described_class.new('file.csv', 'default_mode')
expect(settings.special_mode?).to be(false)
end
it 'returns true when "special_mode" is passed to the constructor' do
settings = described_class.new('file.csv', 'special_mode')
expect(settings.special_mode?).to be(true)
end
end
end In this way, there is nothing in the spec that wasn't explicitly called for in the spec. If I wanted to factor out a little of the detail that doesn't matter, I might add a method to instantiate the settings with defaults: describe Settings do
def make_settings(filename: 'file.csv', mode: 'default_mode')
settings = described_class.new(filename, mode)
end
describe '#filename' do
it 'returns the filename passed to the constructor' do
filename = 'a_different_filename.csv'
settings = make_settings(filename: filename)
expect(settings.filename).to eq(filename)
end
end
describe '#special_mode?' do
it 'returns false when "default_mode" is passed to the constructor' do
settings = make_settings(mode: 'default_mode')
expect(settings.special_mode?).to be(false)
end
it 'returns true when "special_mode" is passed to the constructor' do
settings = make_settings(mode: 'special_mode')
expect(settings.special_mode?).to be(true)
end
end
end I think I'd advocate for avoiding |
We're talking about a basic feature of RSpec that allows for declared variables whose values are memoized when and if needed. If you want to avoid RSpec's features and write tests that look like Minitest, then do so. However, people that use RSpec effectively understand how The RSpec Style Guide should recommend the effective use of RSpec features, not avoid using them because they are unfamiliar to some developers. |
@mockdeep I like the use of a helper method in your example. Not arguing with anyone in particular here, you both have pretty valid points. However, you seem to be sticking to one specific style, one you got used to. Both are quite good and readable. I have some concerns regarding "effective", this term is a subject for interpretation, just like "compelling benefit" that appeared at the beginning of this discussion. One approach is more concise and uses more of DSL capabilities RSpec provides. Another approach is slightly more code, is more verbose and is arguably easier to read due to less (human) lookups. The latter still has a thing to lookup, the helper method, but it's just one as opposed to several A few points (neutral, not intended to prove anyone wrong) from RSpec's codebase:
Also this note from Myron Marston:
And another one:
When picking one of the styles to pick for a spec, from my understanding several factors should be taken into consideration:
In any case, if a given spec becomes unreadable, it's a good indication that SUT has grown too big, and it's not the test code's fault that hundreds of different branches have to be covered. |
This is a little gatekeepy. I understand how
Again, it's not about familiarity, it's about the complexity it creates. It's kind of condescending to imply that people who disagree with you just don't understand the feature. |
@mockdeep I definitely didn't mean to be condescending here. But this is absolutely about familiarity. If you're writing specs in the style you gave, there's no reason to use RSpec. You can achieve the same with Minitest with RSpec-style matchers. But when it comes to an RSpec (and really this came from a discussion about default settings in the linter), recommending against using one of the basic advantages of RSpec seems like a bad idea. When there's a single example in a file calls to I feel this recommendation might be more harmful than good, especially because "when it's beneficial" is open to interpretation. In my experience, it's beneficial in the long term, almost always, because it leads to clearer setup and separation of setup and assertions. |
As mentioned, I've been writing in the
This is a little reductive. RSpec has loads of other differences (and advantages, IMO) from
You're taking it as a given that it's an advantage, but that's the point under debate. As linked by @pirj above, even the core maintainers have recommended using them sparingly if at all.
It's rare if ever that I end up needing to modify multiple test examples in this way. If anything, I end up wanting to add a new minor variation and end up needing to figure out how to finagle the existing Keep in mind that the no- |
The official RSpec style guide (not community) ticket, rspec/rspec.github.io#28, mentions that the guide should not "be used to silence discussion". So interpretation based on the project specificity is the key.
From Myron Marston:
Some quite extreme examples from popular open-source projects: I see this as over-use, as to read and understand this setup takes a while. |
Only use
before
andlet
, when there's a compelling benefit, don't use them as the default way to write specs.https://reddit.com/r/ruby/comments/73nrhk/ama_the_authors_of_effective_testing_with_rspec_3/dnsyanp/
An example I can think of:
The text was updated successfully, but these errors were encountered: