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
lets block for defining multiple let blocks at once
#1650
Comments
|
I've thought about this a bit. Unfortunately, this isn't something that I'd want to pull into core; I'll let the rest of the team chime in on their thoughts. I have several reasons for this, the most concerning is what we would need to do for all the edge cases. What is the behavior when the defined helper names don't match directly to the splatted return value? As in your gist, some people may want an early failure. Others may want the assignments to be What if I want to slurp up the values in another position? Does that mean we now need to support parsing each name to check for a splat? # Not to mention this requires you to use a string b/c Ruby
# doesn't like :*ios due to parsing rules
lets(:'*ios', :status) { call "args" }
# What if I don't care about the values? Do we support this?
# Or some alternative?
lets(:stdout, :*) { call "args" }This also brings up the complexity about what if I don't want one of the splatted values? Is the syntax then: lets(:stdout, :_, :status) { call "args" }What if I do the following? lets(:stdout, :_stderr, :status) { call "args" }Depending on coding style that could either mean "I don't intend to use Splats are not the simplest to understand in Ruby (I often have to recheck syntax and try it in |
|
As for an alternative, I think it really depends on usage. I'm having a hard time thinking of an instance when I actually wanted to do this. I would not suggest "context" local variables, which it seems you tried to use. I know others on the core team use them, but I only use them to define things like scoped classes, modules, to "constants" (which I aggressively call As you found out, "context" locals are not cleaned up (one of their major downsides) and can mutate between specs. This is simply due to Ruby block-scoping rules. Below is a code sample that shows this: describe "defining the context" do
this_is_a_context_local = []
it "can mutate state" do
this_is_a_context_local << :modified
end
it "will fail or pass depending on run order" do
expect(this_is_a_context_local).to eq []
end
it "this may be confusing for some" do
# Due to block scoping rules I just changed
# the actual variable and now it means 123
# for subsequent specs that run
this_is_a_context_local = 123
end
it "if this runs after the above spec it passes" do
expect(this_is_a_context_local).to eq 123
end
endI always tell newer Ruby-ist to start with spec local variables. Often they try to abstract out into
If not, maybe a For your specific use case, it's hard to say without more concrete example specs. Honestly, I probably would just use "spec" local variables. Though, since it seems this was very much related to running a process, I would probably move the "action" into either a shared helper method, or just re-call it in each spec. I can then use the new RSpec Another alternative, since RSpec just creates classes in the background and all describe "a context" do
attr_reader :stdout, :stderr, :status
def run_the_process(args)
@stdout, @stderr, @status = call(args)
end
it "exits successfully" do
run_the_process("args")
expect(status).to be_success
end
end |
|
I saw your tweet to your gist yesterday, @JoshCheek, and seeing That said...I can't think of a single time I've ever wanted something like this (then again, maybe I've had situations that could have benefitted from this but didn't realize it at the time). Given that this is intended to be used in a similar fashion to destructuring assignment, I think I'd expect it to have the same semantics...but as @cupakromer has pointed out, there are definitely some complexities with this, particularly with how splats work. I also think that Historically, I think RSpec has been too eager to add new features/APIs suggested by users. Over time, we've learned that some of those features are a better fit for external gems (like So...I'd encourage you to package this in an extension gem and if the community finds it widely useful and the corner cases get worked out, we can consider pulling it in the future...but for now, I don't think this belongs. |
class Program
# ...
end
if $0 !~ /rspec/
# the out here is visible below, so gets shadowed, and they share state
out, err, status = Program.call ARGV
$stdout.print out
# ...
else
RSpec.describe Program do
# ...
context 'one' do
out, err, status = Program.call [...] # => ["out1", ...]
# this will fail, because the context below updated what out points to
it('is out1') { expect(out).to eq 'out1' }
# ...
context 'two' do
out, err, status = Program.call [...] # => ["out2", ...]
# ... |
|
I personally ran into this issue when trying to refactor away from I thought this was really tricky/fragile, as it relied on laziness, and because I couldn't understand the test itself without looking at all of the "parent" contexts and simulating in my brain how all of the different setup code would interact. So, I thought to myself, I know how to make things explicit: using arguments. I created a helper method which accepts an 'age' argument and returns the The approach I took was to just write code like: The main frustration I have is that, if I have more setup code, and the setup code requires access to As for the semantics of how you would implement this, I feel like you would want |
|
👋 As per the original comments at the time, this is still something we'd be reticent to bring into core, there is nothing stopping you from implementing this as an extension in the mean time. Thats overriding let and using the original let interface as its a public api, you could do away with the meta let if you didn't need to delay the block call etc |
|
@JonRowe Thanks for the tip!! |
The code is here.
I had a method that returned three values (stdout, stderr, exit status), and setting these values with let blocks was quite obnoxious:
So I wound up using local variables, because then I could just do:
This had drawbacks, though:
So I decided I'd like to be able to have a
letsblock, which is the same as aletblock, except it sets multiple names at once so that it works more like the local variable example. Now I can do:Figured I'd post it here, and see if it fit with what y'all are trying to do, before trying for a PR.
The text was updated successfully, but these errors were encountered: