Skip to content

Commit 2f7fd16

Browse files
authored
Merge pull request #133 from GuyPaddock/feature/issue-132--let-should-be-lazy
Fix Issue #132 -- Make `let` Lazily Evaluated
2 parents 59a159d + 3f73979 commit 2f7fd16

File tree

6 files changed

+619
-12
lines changed

6 files changed

+619
-12
lines changed

docs/VariablesAndValues.md

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ when the test is broken into separate steps.
1212
The `let` function is used to initialise a fresh, isolated, object for each spec.
1313

1414
### Common Variable Initialization
15+
#### Let
16+
The `let` helper function makes it easy to initialize common variables that are used in multiple
17+
specs. In standard JUnit you might expect to use the initializer list of the class or a `@Before`
18+
method to achieve the same. As there is no easy way for `beforeAll` or `beforeEach` to instantiate
19+
a value that will be used in the specs, `let` is the tool of choice.
1520

16-
The `let` helper function makes it easy to initialize common variables that are used in multiple specs. In standard JUnit you might expect to use the initializer list of the class or a `@Before` method to achieve the same. As there is no easy way for `beforeAll` or `beforeEach` to instantiate a value that will be used in the specs, `let` is the tool of choice.
17-
18-
Values are cached within a spec, and lazily re-initialized between specs as in [RSpec #let](http://rspec.info/documentation/3.5/rspec-core/RSpec/Core/MemoizedHelpers/ClassMethods.html#let-instance_method).
21+
Values are cached within a spec, and lazily re-initialized between specs as in
22+
[RSpec #let](http://rspec.info/documentation/3.5/rspec-core/RSpec/Core/MemoizedHelpers/ClassMethods.html#let-instance_method).
1923

2024
> from [LetSpecs.java](../src/test/java/specs/LetSpecs.java)
2125
@@ -42,7 +46,64 @@ describe("The `let` helper function", () -> {
4246
});
4347
```
4448

45-
For cases where you need to access a shared variable across specs or steps, the `Variable` helper class provides a simple `get`/`set` interface. This may be required, for example, to initialize shared state in a `beforeAll` that is used across multiple specs in that suite. Of course, you should exercise caution when sharing state across tests
49+
#### Eager Let
50+
If you need to ensure that a value is initialized at the start of a test, you can use the `eagerLet`
51+
helper function, which has the same semantics as `let` but is evaluated prior to `beforeEach`. This
52+
is often useful when you need to initialize values you can use in your `beforeEach` block. The value
53+
is still initialized after any `beforeAll` blocks.
54+
55+
This is similar to
56+
[RSpec #let!](http://rspec.info/documentation/3.5/rspec-core/RSpec/Core/MemoizedHelpers/ClassMethods.html#let!-instance_method).
57+
58+
> from [EagerLetSpecs.java](../src/test/java/specs/EagerLetSpecs.java)
59+
60+
```java
61+
describe("The `eagerLet` helper function", () -> {
62+
final Supplier<List<String>> items = eagerLet(() -> new ArrayList<>(asList("foo", "bar")));
63+
64+
final Supplier<List<String>> eagerItemsCopy = eagerLet(() -> new ArrayList<>(items.get()));
65+
66+
context("when `beforeEach`, `let`, and `eagerLet` are used", () -> {
67+
final Supplier<List<String>> lazyItemsCopy =
68+
let(() -> new ArrayList<>(items.get()));
69+
70+
beforeEach(() -> {
71+
// This would throw a NullPointerException if it ran before eagerItems
72+
items.get().add("baz");
73+
});
74+
75+
it("evaluates all `eagerLet` blocks at once", () -> {
76+
assertThat(eagerItemsCopy.get(), contains("foo", "bar"));
77+
});
78+
79+
it("evaluates `beforeEach` after `eagerLet`", () -> {
80+
assertThat(items.get(), contains("foo", "bar", "baz"));
81+
});
82+
83+
it("evaluates `let` upon first use", () -> {
84+
assertThat(lazyItemsCopy.get(), contains("foo", "bar", "baz"));
85+
});
86+
});
87+
88+
context("when `beforeAll` and `eagerLet` are used", () -> {
89+
beforeAll(() -> {
90+
assertThat(items.get(), is(nullValue()));
91+
assertThat(eagerItemsCopy.get(), is(nullValue()));
92+
});
93+
94+
it("evaluates `beforeAll` prior to `eagerLet`", () -> {
95+
assertThat(items.get(), is(not(nullValue())));
96+
assertThat(eagerItemsCopy.get(), is(not(nullValue())));
97+
});
98+
});
99+
});
100+
```
101+
102+
#### Variable
103+
For cases where you need to access a shared variable across specs or steps, the `Variable` helper
104+
class provides a simple `get`/`set` interface. This may be required, for example, to initialize
105+
shared state in a `beforeAll` that is used across multiple specs in that suite. Of course, you
106+
should exercise caution when sharing state across tests
46107

47108
> from [VariableSpecs.java](../src/test/java/specs/VariableSpecs.java)
48109

src/main/java/com/greghaskins/spectrum/dsl/specification/Specification.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.greghaskins.spectrum.internal.DeclarationState;
1313
import com.greghaskins.spectrum.internal.Suite;
1414
import com.greghaskins.spectrum.internal.blocks.IdempotentBlock;
15+
import com.greghaskins.spectrum.internal.hooks.EagerLetHook;
1516
import com.greghaskins.spectrum.internal.hooks.Hook;
1617
import com.greghaskins.spectrum.internal.hooks.HookContext.AppliesTo;
1718
import com.greghaskins.spectrum.internal.hooks.HookContext.Precedence;
@@ -185,6 +186,26 @@ static <T> Supplier<T> let(final ThrowingSupplier<T> supplier) {
185186
return letHook;
186187
}
187188

189+
/**
190+
* A value that will be calculated fresh at the start of each spec and cannot bleed across specs.
191+
*
192+
* <p>
193+
* Note that {@code eagerLet} is eagerly evaluated: the {@code supplier} is called at the start
194+
* of the spec, before {@code beforeEach} blocks.
195+
* </p>
196+
*
197+
* @param <T> The type of value
198+
* @param supplier {@link ThrowingSupplier} function that either generates the value, or throws a
199+
* {@link Throwable}
200+
* @return supplier which is refreshed for each spec's context
201+
*/
202+
static <T> Supplier<T> eagerLet(final ThrowingSupplier<T> supplier) {
203+
EagerLetHook<T> eagerLetHook = new EagerLetHook<>(supplier);
204+
DeclarationState.instance().addHook(eagerLetHook, AppliesTo.ATOMIC_ONLY, Precedence.LOCAL);
205+
206+
return eagerLetHook;
207+
}
208+
188209
/**
189210
* Define a test context. Alias for {@link #describe}.
190211
*
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.greghaskins.spectrum.internal.hooks;
2+
3+
import com.greghaskins.spectrum.ThrowingSupplier;
4+
5+
/**
6+
* Implementation of an eager version of {@code let}.
7+
*
8+
* <p>Sematics are the same as with {@link LetHook}, except that all values are calculated at the
9+
* start of the test, rather than on an as-needed basis.
10+
*/
11+
public class EagerLetHook<T> extends AbstractSupplyingHook<T> {
12+
private final ThrowingSupplier<T> supplier;
13+
14+
public EagerLetHook(final ThrowingSupplier<T> supplier) {
15+
this.supplier = supplier;
16+
}
17+
18+
protected T before() {
19+
return supplier.get();
20+
}
21+
22+
protected String getExceptionMessageIfUsedAtDeclarationTime() {
23+
return "Cannot use the value from eagerLet() in a suite declaration. "
24+
+ "It may only be used in the context of a running spec.";
25+
}
26+
}
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
}

0 commit comments

Comments
 (0)