Skip to content

Commit f02d5d8

Browse files
author
Guy Paddock
committed
Fix Issue greghaskins#132 -- Make let Lazily Evaluated
The previous implementation would cause all `let` blocks to be evaluated at the start of the test rather than on an as-needed basis, which is inconsistent with the behavior developers who are used to RSpec would expect. Now, `let` only evaluates its block when the test first needs a value, allowing the `let` block to depend on state that is being setup by other blocks (`beforeEach`, other `let` blocks, etc).
1 parent 59a159d commit f02d5d8

File tree

2 files changed

+93
-8
lines changed

2 files changed

+93
-8
lines changed
Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,77 @@
11
package com.greghaskins.spectrum.internal.hooks;
22

3+
import com.greghaskins.spectrum.Block;
34
import com.greghaskins.spectrum.ThrowingSupplier;
5+
import com.greghaskins.spectrum.Variable;
6+
import com.greghaskins.spectrum.internal.DeclarationState;
7+
import com.greghaskins.spectrum.internal.RunReporting;
8+
9+
import org.junit.runner.Description;
10+
import org.junit.runner.notification.Failure;
411

512
/**
6-
* Implementation of let as a supplying hook.
13+
* Implementation of {@code let} as a supplying hook.
14+
*
15+
* <p>Using {@code let} allows you to define shared values that can be used by multiple tests,
16+
* without having to worry about cleaning up the values between tests to prevent shared state in
17+
* one test from affecting the results of another.
18+
*
19+
* <p>Values are lazily initialized and then cached, so a value is not calculated until the first
20+
* time it is needed in a given test. Subsequent fetches of the value within the same test will
21+
* return the cached value.
722
*/
8-
public class LetHook<T> extends AbstractSupplyingHook<T> {
9-
23+
public class LetHook<T> implements SupplyingHook<T> {
1024
private final ThrowingSupplier<T> supplier;
25+
private final Variable<T> cachedValue = new Variable<>();
26+
private boolean isCached;
1127

1228
public LetHook(final ThrowingSupplier<T> supplier) {
1329
this.supplier = supplier;
30+
this.isCached = false;
1431
}
1532

16-
protected T before() {
17-
return supplier.get();
33+
@Override
34+
public void accept(final Description description,
35+
final RunReporting<Description, Failure> reporting, final Block block)
36+
throws Throwable {
37+
try {
38+
block.run();
39+
} finally {
40+
clear();
41+
}
42+
}
43+
44+
@Override
45+
public T get() {
46+
assertSpectrumIsRunningTestsNotDeclaringThem();
47+
48+
if (!this.isCached) {
49+
this.cachedValue.set(supplier.get());
50+
51+
this.isCached = true;
52+
}
53+
54+
return this.cachedValue.get();
1855
}
1956

2057
protected String getExceptionMessageIfUsedAtDeclarationTime() {
2158
return "Cannot use the value from let() in a suite declaration. "
2259
+ "It may only be used in the context of a running spec.";
2360
}
61+
62+
private void clear() {
63+
this.isCached = false;
64+
this.cachedValue.set(null);
65+
}
66+
67+
/**
68+
* Will throw an exception if this method happens to be called while Spectrum is still defining
69+
* tests, rather than executing them. Useful to see if a hook is being accidentally used during
70+
* definition.
71+
*/
72+
private void assertSpectrumIsRunningTestsNotDeclaringThem() {
73+
if (DeclarationState.instance().getCurrentSuiteBeingDeclared() != null) {
74+
throw new IllegalStateException(getExceptionMessageIfUsedAtDeclarationTime());
75+
}
76+
}
2477
}

src/test/java/specs/LetSpecs.java

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package specs;
22

33
import static com.greghaskins.spectrum.dsl.specification.Specification.afterEach;
4+
import static com.greghaskins.spectrum.dsl.specification.Specification.beforeEach;
5+
import static com.greghaskins.spectrum.dsl.specification.Specification.context;
46
import static com.greghaskins.spectrum.dsl.specification.Specification.describe;
57
import static com.greghaskins.spectrum.dsl.specification.Specification.it;
68
import static com.greghaskins.spectrum.dsl.specification.Specification.let;
@@ -15,6 +17,7 @@
1517

1618
import com.greghaskins.spectrum.Spectrum;
1719
import com.greghaskins.spectrum.SpectrumHelper;
20+
import com.greghaskins.spectrum.Variable;
1821

1922
import org.junit.runner.Result;
2023
import org.junit.runner.RunWith;
@@ -29,7 +32,6 @@
2932
public class LetSpecs {
3033
{
3134
describe("The `let` helper function", () -> {
32-
3335
final Supplier<List<String>> items = let(() -> new ArrayList<>(asList("foo", "bar")));
3436

3537
it("is a way to supply a value for specs", () -> {
@@ -41,18 +43,36 @@ public class LetSpecs {
4143

4244
items.get().add("baz");
4345
items.get().add("blah");
46+
4447
assertThat(items.get(), contains("foo", "bar", "baz", "blah"));
4548
});
4649

50+
context("when the value returned by the supplier is `null`", () -> {
51+
final AtomicInteger callCounter = new AtomicInteger();
52+
53+
final Supplier<String> stringLet = let(() -> {
54+
if (callCounter.getAndIncrement() == 0) {
55+
return null;
56+
} else {
57+
return "fail";
58+
}
59+
});
60+
61+
it("does not call the supplier multiple times", () -> {
62+
assertThat(stringLet.get(), is(nullValue()));
63+
assertThat(stringLet.get(), is(nullValue()));
64+
});
65+
});
66+
4767
it("creates a fresh value for every spec", () -> {
4868
assertThat(items.get(), contains("foo", "bar"));
4969
});
5070

5171
describe("in complex test hierarchies", () -> {
5272
describe("a new let object is created for each spec", () -> {
5373
AtomicInteger integer = new AtomicInteger();
54-
describe("a thing", () -> {
5574

75+
describe("a thing", () -> {
5676
final Supplier<Integer> intLet = let(integer::getAndIncrement);
5777

5878
it("starts with one value", () -> {
@@ -85,14 +105,26 @@ public class LetSpecs {
85105
});
86106
});
87107
});
108+
});
109+
});
110+
});
111+
112+
describe("lazy initialization", () -> {
113+
context("when setup has to be done before a let is evaluated", () -> {
114+
final Variable<Integer> theVariable = new Variable<>();
115+
final Supplier<Integer> result = let(() -> theVariable.get());
88116

117+
beforeEach(() -> {
118+
theVariable.set(123);
89119
});
90120

121+
it("does not cache a value until the let is referenced", () -> {
122+
assertThat(result.get(), is(123));
123+
});
91124
});
92125
});
93126

94127
describe("when trying to use a value outside a spec", () -> {
95-
96128
final Supplier<Result> result =
97129
let(() -> SpectrumHelper.run(getSuiteThatUsesLetValueOutsideSpec()));
98130

0 commit comments

Comments
 (0)