Let and before hooks in Ruby - Time & Space Complexity
We want to understand how the time it takes to run code with let and before hooks changes as the input grows.
How does the setup and repeated use of these hooks affect the total work done?
Analyze the time complexity of the following Ruby test setup using let and before hooks.
let(:numbers) { (1..n).to_a }
before do
@sum = 0
numbers.each do |num|
@sum += num
end
end
it 'calculates sum' do
expect(@sum).to eq(n * (n + 1) / 2)
end
This code creates an array of numbers from 1 to n, sums them before each test, and checks the sum.
Look at what repeats when the test runs.
- Primary operation: Looping through the array to sum numbers.
- How many times: Once before each test example runs.
As n grows, the number of numbers to add grows too.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 additions |
| 100 | About 100 additions |
| 1000 | About 1000 additions |
Pattern observation: The work grows directly with n; doubling n doubles the work.
Time Complexity: O(n)
This means the time to run the before hook grows in a straight line as the input size grows.
[X] Wrong: "The let block runs only once, so it doesn't affect time much."
[OK] Correct: The let block runs lazily once per test example (memoized after first use within the example), so the cost repeats for each test.
Understanding how setup code like before hooks scales helps you write tests that run efficiently as your project grows.
"What if we moved the summing code inside the let block instead of the before hook? How would the time complexity change?"
Practice
let in Ruby tests?Solution
Step 1: Understand
letbehaviorletdefines a value that is created only when needed (lazy evaluation) and reused within the same test.Step 2: Differentiate from
beforebeforeruns code before each test but does not create reusable values likelet.Final Answer:
To create reusable values that are lazily evaluated -> Option AQuick Check:
let= reusable lazy value [OK]
- Confusing let with before hook
- Thinking let runs before each test eagerly
- Assuming let defines test cases
let variable named user with value "Alice"?Solution
Step 1: Recall
letsyntaxlettakes a symbol argument and a block returning the value:let(:name) { value }.Step 2: Check each option
let(:user) { "Alice" } matches correct syntax. Options B and D miss parentheses or block syntax. before { user = "Alice" } usesbefore, notlet.Final Answer:
let(:user) { "Alice" } -> Option CQuick Check:
Correctletsyntax uses symbol and block [OK]
- Omitting parentheses around symbol
- Using assignment instead of block
- Confusing let with before syntax
let(:count) { 0 }
before { count += 1 }
it "increments count" do
puts count
endSolution
Step 1: Understand
letandbeforeinteractionlet(:count) { 0 }defines a method returning 0. Butcountis not a variable; it's a method call.Step 2: Analyze
before { count += 1 }count += 1tries to modifycountas a variable, butcountis a method, so Ruby raises an error: undefined method+=for nil or no local variable.Final Answer:
Error: undefined method += for nil -> Option DQuick Check:
Cannot modify let method return with += [OK]
- Assuming let creates a variable to modify
- Expecting count to increment automatically
- Ignoring that let is lazily evaluated method
let(:numbers) { [1, 2, 3] }
before do
numbers << 4
end
it "checks numbers length" do
expect(numbers.length).to eq(4)
endSolution
Step 1: Understand let and before execution order
let(:numbers)memoizes a new array [1,2,3] the first timenumbersis called in each test example.Step 2: Trace the test execution
Inbefore,numbers << 4calls/memoizesnumbersand mutates it to [1,2,3,4]. Init,numbers.length(memoized) is 4, so test passes.Final Answer:
No error; test passes as expected -> Option BQuick Check:
Mutation inbefore(:each)affects per-example memoizedletcorrectly [OK]
- Misunderstanding per-example memoization (no inter-test sharing)
- Thinking mutation always causes shared state between tests
- Confusing
before(:each)withbefore(:all)
let and before hooks?let(:user_name) { "Bob" }
before do
@user = User.new(name: user_name)
@user.activate!
end
it "checks user is active" do
expect(@user.active?).to be true
endWhat is the main advantage of this approach?
Solution
Step 1: Analyze separation of concerns
let(:user_name)defines reusable data lazily, whilebeforeruns setup code creating and activating user.Step 2: Understand benefits
This keeps data definition and setup logic cleanly separated, improving test readability and maintainability.Final Answer:
Separates reusable data (user_name) from setup actions (before) -> Option AQuick Check:
let for data, before for setup [OK]
- Thinking let runs before before hook
- Avoiding instance variables unnecessarily
- Confusing reset behavior of let
Ready to prove it?
Pass the test and this pattern moves unknown
