The Internet is Ringing Off the Wall
It’s good to have people watching over me. Already got some feedback on this morning’s article. Let’s see what we’ve learned.
Stack is flawed
Joe Rainsberger pointed out that it looks like the Stack in my little experiment doesn’t return a Stack from push, it returns an Array, which just happens to support push and pop. This is just one reason why Stack was a silly little project.
There’s this trick when you’re doing TDD and your pair suggests that the program doesn’t work: you write a test to find out. So I wrote this one:
it "returns a stack" do
expect(stack.push(311)).to be_a(Stack)
end
This fails, of course, because Joe is right:
Failures:
1) Stack returns a stack
Failure/Error: expect(stack.push(311)).to be_a(Stack)
expected [3, 311] to be a kind of Stack
# ./spec/stack_spec.rb:21:in `block (2 levels) in <top (required)>'
Finished in 0.00283 seconds (files took 0.06627 seconds to load)
3 examples, 1 failure
Hey, wait a minute! What’s going on here?
(Oddly, my home iMac is also not updating Jekyll on save from Sublime. It was the MacBook I was complaining about this morning. This is starting to seem like enemy action. But I digress.)
Anyway, I improve the implementation of this silly class like this:
def push(value)
@contents.push(value)
self
end
And the test passes. Good eye, Joe! Thanks!
Use let
, not context
Myron Marston, lead maintainer on RSpec, writes:
Anyhow, I saw your blog post from this morning…thanks for posting that! It’s an interesting read. I did have a bit of feedback on your example specs there. Overall, they are well written. My one suggestion is to be careful with creating objects in your
context
block that you use in multipleit
blocks. Doing it directly in yourcontext
block like you’ve done has a few gotchas:
- If the objects are mutable and any of your examples (the
it
blocks) mutate them, you can easily create an accidental ordering dependency without realizing it. (RSpec provides a means to run the specs in random order with a repeatable seed to help combat and surface ordering dependencies).
- You may not be using this feature yet, but it’s common for users to filter the specs and run a single spec and/or a subset of specs in order to focus on one thing. In such situations, creating the objects in your
context
block will incur the cost of creating them even if the specs that use them aren’t run.
- Specs for DB-dependent code will often wrap each example in a DB transaction. If you create objects that insert records in the DB (as is common in a rails context), then creating those objects directly in a
context
block will create them outside the context of a per-example transaction, which could cause problems.
The normal way to define these sorts of objects that are used by multiple examples is to use
let
, for example:
let(:block) { RegardingCategoryBlock.new("xxxtestingxxx", "some category", []) }
Then you can access
block
from any examples in the context. Within a single example, the first call toblock
will evaulate the block and construct the object, and then memoize and return the value so that later calls toblock
return the created object, The memoized value is cleared between examples so that mutations to the object in one example do not affect any other examples.
All that said, I think it’s fine to create objects and assign them to local variables in a
context
block if they are immutable primitives, as they don’t suffer from any of the downsides I listed above. Many RSpec users prefer just to always uselet
for consistency and to not have to worry about these issues.
OK, I think I understand that: context
doesn’t work well with a mutable object. This is, I’m pleased to say, an example of the difficulty I’m having with learnng RSpec. There aren’t many good even moderately sized examples of what to do, especially if you were looking for a few words about why to do them that way. In any case, I need to keep this in mind for when I try again to TDD the regarding
block using RSpec, which I do plan to do.
This leaves me unclear on just what context
is for. Probably I’ll find out soon enough.
For now, I’ll not go back and fix that old one, since it isn’t fit to the purpose of fixing regarding
anyway. We’ll just let it stand as another example of Jeffries writing down what he doesn’t know.
There is a reason why I do that, by the way. First, none of us knows everything, and many of us are reluctant to display our ignorance publicly at work. The result of that is arguments, people hiding their code, and silence where there should have been conversation. I’ve been wrong so often, over more than a half-century, that I don’t mind being wrong one more time. It seems to be the best way to learn something. So I just laugh, learn, and move on.
Second, once someone in the room admits ignorance and survives, it makes it just a bit easier for others to do the same. Soon, it catches on. The more “wrong” shows up in the room, the faster it gets turned into “right” or at least “less wrong”.
Are you wondering why I said “a reason” and then gave two? Well, either it was a two-for-one holiday sale, or I was wrong again.
Thanks for listening, and for your feedback. Keep those cards and letters coming!
P.S. No one has cracked my problem with Jekyll not refreshing yet. It’s not too late to claim your prize!