-
Notifications
You must be signed in to change notification settings - Fork 286
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 let and let! #8
Comments
I'm a big fan of the |
Also, one other thing: I've found that people are often initially confused by the semantics of # this:
let(:foo) { Foo.new }
# ...is very nearly equivalent to this:
def foo
@foo ||= Foo.new
end |
In this example:
this bit:
is not a good practice. The reason is that Sure, it kind of works in this case. But when you're talking about best practices, you really want to use the best tool for the job, and not just a tool that kind of works in this case. In this case, a method would be best:
The full example using a method would be:
|
i think in this case, exactly nothing would be best: context 'when updates a not existing property value' do
let(:properties) { { id: Settings.resource_id, value: 'on'} }
it 'raises a not found error' do
expect { resource.properties = properties }.to raise_error Mongoid::Errors::DocumentNotFound
end
end |
@nclark That's true in this particular case only because this particular case is so simple. But this particular case really stands in as an example for many like cases, some of which are simple and some of which are much more complex. In some such complex cases, the action may be a very long line of code or may be two, three, or ten lines of code. Or maybe there are twenty In these complex cases, depending on the details of the cases, you may want to wrap up that complexity in a normal Ruby method. But, as from my previous comment, you would not want to wrap it up in a |
The declarative nature of
... looks awkward because you've dropped into imperative semantics.
... while it looks more verbose, fits better because you are declaring what I think @nclark is right in embedding it. You are trying to test the assignment of properties and testing for a raised error. You would want to declare it in |
@hosh Remember that "the assignment of properties" is a very simple example whose purpose is to illustrate a whole class of examples, many of which are very complex. And remember that the text right before the example (at http://betterspecs.org/#let) is: "Use let to initialize actions that are lazy loaded to test your specs." So my point is not really about this particular simple example alone. My point is really about the whole class of both simple and complex examples which this particular simple example represents: the class of all examples relating to deferring possibly-complex actions so that we can test the actions directly for failure, rather than just inspecting the results or side-effects of these actions. |
@yfeldblum why would you be writing a complex series of actions for your test stimulus, though? Setup might be complex but the stimulus should surely never be more than a single method call. In the example, the actual thing being tested is the method Even assuming for the sake of argument that we might want to make assertions about complex series of actions that for some reason aren't modelled in our application's code, I still struggle to see an advantage in putting a layer of indirection between the thing we're making assertions about and the assertion being made. Can you give an example of a more complex test that would benefit from this use of |
|
Maybe this is personal preference, but it's highly opaque to me what is being tested in those examples. Is it the method
Only two of these statements are actually about errors (and IMO the first statement is not a valuable assertion). That leaves two statements, 3 and 4, that are being indirectly tested using repetition of statements 1 and 2. Surely it's far better to directly test them: let(:thing) { described_class.new }
describe "#run_and_clear_props" do
it "raises an error if no properties are set" do
expect { thing.run_and_clear_props }.to raise_error
end
it "clears the properties" do
thing.prop1 = val1
thing.prop2 = val2
thing.prop3 = val3
thing.run_and_clear_props
# make assertions about properties here
end
end
describe "#reset" do
it "resets the object to some condition" do
# test whatever it is that #reset actually does
end
end It also seems that Test complexity is design feedback - we should eliminate it by improving our design, not by disguising it in our test suite. |
In the example, think of the prop-setting like passing multiple named arguments to It's important not to get bogged down in the simplistic details of a stylized example. In any particular example, of course, there will be many ways to improve the code (from at least some people's viewpoints on code goodness). It's also important not to force the principles of good unit tests onto tests that may not be unit tests. In non-unit tests, we may find it desirable to test a sequence of steps as one, rather than just each step individually. In the particular example, |
Right, but why should the reader of these tests have to infer I agree that obsessing over the details of simplified examples can be counterproductive, but if the only example we can find where we might want to put stimuli in a test method is illustrative of both bad API design and bad testing, then it hardly recommends the approach as a "best practice" of any sort. On the contrary, it illustrates precisely my objection: that if your required stimulus is complex, then you're trying to test too much at once. |
@yfeldblum Sure, I understand that. I also point out that often times, there is needlessly complex side effects. Some things really do need a lot of operations jammed together, but not everything. My main use for methods is not complex/simple distinction. It is whether I want to write a macro or not. I tend to use Ruby methods to set up As far as betterspecs goes and your argument that this example is a simple case: this means that we should write a better example. |
The data introduced by |
I've updated the main example, added a simple example of what |
We should give an example of One can miss this tiny comment that is in guideline and get some pain. |
Is there a way to force-load/instantiate a If I have a lot of specs that use let in the way it was designed, i.e. lazy loading, but then have one spec that needs the variable to be loaded immediately I will begin with the line |
@johnrees can you give an example? let(:special) { 'hi' }
before { special } So you can always just call the bareword name at the start of a spec. |
Should let always be just one line? For example if I had let(:book) { Book.new }
before { book.read } that just looks off to me. Should |
What are you testing afterwards because if you are testing the read functions result you can: subject { book.read }
it { should be_true } if you are testing the book object after you called read on it, you are better off returning subject { book.read }
its(:read?) { should be_true } |
As a user of If we're concerned about expressing intent, how is the word Lazy evaluation? Maybe in production. Not sure how this is useful when testing. Are @myronmarston's stackoverflow answers really that compelling? 1) okay, can't argue with personal preference, but otherwise seems very minor 2) seems to be the most "legit" answer, but doesn't this mean that 3) somewhat the same as 1, but does this really make a difference at all? 4) Easier to read, but not necessarily easier to comprehend. Lazy evaluation masks intent, failures can lead one to waste time tracing the execution order of the lazy evaluation. How would one rewrite this failing test using describe "#search" do
let(:macro_hood) { Fabricate(:macro_neighborhood) }
let(:hood) { Fabricate(:neighborhood, :macro_neighborhood => macro_hood) }
let(:restaurant) { Fabricate(:restaurant, :neighborhood => hood) }
context "given the name of a neighborhood" do
it "returns restaurants in that neighborhood" do
result = RestaurantSearch.search(hood.name)
expect(result.size).to eq 1
expect(result[0].id).to eq restaurant.id
end
end
context "given the name of a restaurant" do
# ...
end
end And if so it that really clearer/cleaner than: describe "#search" do
before do
@macro_hood = Fabricate(:macro_neighborhood)
@hood = Fabricate(:neighborhood, :macro_neighborhood => @macro_hood)
@restaurant = Fabricate(:restaurant, :neighborhood => @hood)
end
context "given the name of a neighborhood" do
it "returns restaurants in that neighborhood" do
result = RestaurantSearch.search(@hood.name)
expect(result.size).to eq 1
expect(result[0].id).to eq @restaurant.id
end
end
end |
@sshaw I don't think I'd use a
Maybe I'm not the target audience for this spec suggestion, though; I tend to avoid |
@sshaw so just my additional 2¢ on the benefits of
My personal feelings about
As I said, I feel too many people abuse For me, my general guideline is:
If you change a This concept comes from DRY. However, not in the way most people think. I'm not referring to pure code duplication here. Often variables in specs have the same name but this is simply coincidental duplication and not actual. What I mean by that is, while the code looks the same, the variables aren't meant to be the same, I could change the names and the duplication would disappear while the specs remain the same. When I refer to DRY here I mean: A single authoritative source for the definition of a domain object / concept. If I change the meaning of that domain object, then it should propagate to all places that use it. |
I am convinced by this argument concerning the dynamic scoping that comes with using let. If you want to add the complexity of dynamic scoping to your tests, go ahead, but in my opinion it is a KIS violation to use it unless you actually need it. Like recursion, some developers are going to love using dynamic scoping because it is intellectually challenging and makes them feel smart, but avoiding this kind of I have done everything under the sun with instance variables, which are Ruby convention and totally predictable, and not once have I found myself saying, "this test would work better/be easier to write/be more legible" if only I had the dynamic scoping that The only down side to instance variables is that it is hard to learn all the cool stuff you can do with them because all the tutorials and Stack Overflow examples are using Everyday Testing in RSpec was a refreshing breath of 'let' free air... |
The examples use tabular code alignment (inserting horizontal whitespace to make things line up):
It's a bit of an anti-pattern and perhaps shouldn't be shown to programmers who might imitate it without understand its costs. That said, it is a nit and not a big deal here. |
@wconrad I really like your blog post. I think, however, for tests that tabular alignment is okay. Especially since RSpec tests can be pretty long. In my experience, the individual example does not change, but rather more examples are added. |
@onebree It seems to me that the longer the block of tabularly aligned code, the more the cost when the alignment changes. That said, you're right that it's less costly if the block seldom changes than if it changes often. |
Wondering what's the big reason to use Like in this example:
vs
UPDATE: in my scenario, seems like you can't use let's to build dynamic specs (it). producing:
|
A constant is neither better nor worse than A constant is accessible to both the class, at load time, and to the instance, at run time.
A loop that defines multiple test cases using A
is like this:
Let is necessary for definitions that cannot happen at load time. For example, in a test using ActiveRecord models, you may need for an instance of an ActiveRecord model to be created:
You could not define this as a constant:
Because the constant is defined at load time, none of the database setup has been done yet. There probably isn't even a database connection. |
I was linked http://www.betterspecs.org/ by collegue to rewrite my spec but I struggle with Assuming I currently have a code like this
how can I rewrite this using
but that means repeating the |
@graywolf-at-work -- I don't worry too much about DRY in my tests. I find clarity and simplicity to be of greater value in a test than DRY. I can't think of any way to have just a single User.new that isn't more trouble than it's worth. For example, you could do this:
But that's really no better. Instead of having to repeat User.new, you have to repeat user_attrs. I have done something like this, that's slightly better, on occasion:
But I don't see anything wrong with repeating User.new. Clarity and simplicity trump DRY in tests. |
There is--it's describe Foo do
let(:user) { User.new }
context 'something' do
...
end
context 'cancelled' do
let(:user) { super().tap { |u| u.cancelled = true } }
...
end
end That said, I wouldn't recommend that here. I think using a describe Foo do
let(:user) { User.new }
context 'something' do
...
end
context 'cancelled' do
before(:each) { user.cancelled = true }
...
end
end |
Write your thoughts about the "use let and let!" best practice.
The text was updated successfully, but these errors were encountered: