Skip to content

Commit 24276b1

Browse files
author
Guy Paddock
committed
Issue greghaskins#132 -- Move Old Let Over to Being eagerLet
The previous implementation is closer to `let!` from RSpec. As such, it's still useful for cases in which a developer needs values to be available in the `beforeEach` block.
1 parent 088f773 commit 24276b1

File tree

4 files changed

+526
-4
lines changed

4 files changed

+526
-4
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+
}

0 commit comments

Comments
 (0)