Skip to content

Commit

Permalink
Issue greghaskins#132 -- Move Old Let Over to Being eagerLet
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Guy Paddock committed Mar 8, 2018
1 parent 088f773 commit 24276b1
Show file tree
Hide file tree
Showing 4 changed files with 526 additions and 4 deletions.
69 changes: 65 additions & 4 deletions docs/VariablesAndValues.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ when the test is broken into separate steps.
The `let` function is used to initialise a fresh, isolated, object for each spec.

### Common Variable Initialization
#### Let
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.

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.

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).
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).

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

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
#### Eager Let
If you need to ensure that a value is initialized at the start of a test, you can use the `eagerLet`
helper function, which has the same semantics as `let` but is evaluated prior to `beforeEach`. This
is often useful when you need to initialize values you can use in your `beforeEach` block. The value
is still initialized after any `beforeAll` blocks.

This is similar to
[RSpec #let!](http://rspec.info/documentation/3.5/rspec-core/RSpec/Core/MemoizedHelpers/ClassMethods.html#let!-instance_method).

> from [EagerLetSpecs.java](../src/test/java/specs/EagerLetSpecs.java)
```java
describe("The `eagerLet` helper function", () -> {
final Supplier<List<String>> items = eagerLet(() -> new ArrayList<>(asList("foo", "bar")));

final Supplier<List<String>> eagerItemsCopy = eagerLet(() -> new ArrayList<>(items.get()));

context("when `beforeEach`, `let`, and `eagerLet` are used", () -> {
final Supplier<List<String>> lazyItemsCopy =
let(() -> new ArrayList<>(items.get()));

beforeEach(() -> {
// This would throw a NullPointerException if it ran before eagerItems
items.get().add("baz");
});

it("evaluates all `eagerLet` blocks at once", () -> {
assertThat(eagerItemsCopy.get(), contains("foo", "bar"));
});

it("evaluates `beforeEach` after `eagerLet`", () -> {
assertThat(items.get(), contains("foo", "bar", "baz"));
});

it("evaluates `let` upon first use", () -> {
assertThat(lazyItemsCopy.get(), contains("foo", "bar", "baz"));
});
});

context("when `beforeAll` and `eagerLet` are used", () -> {
beforeAll(() -> {
assertThat(items.get(), is(nullValue()));
assertThat(eagerItemsCopy.get(), is(nullValue()));
});

it("evaluates `beforeAll` prior to `eagerLet`", () -> {
assertThat(items.get(), is(not(nullValue())));
assertThat(eagerItemsCopy.get(), is(not(nullValue())));
});
});
});
```

#### Variable
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

> from [VariableSpecs.java](../src/test/java/specs/VariableSpecs.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.greghaskins.spectrum.internal.DeclarationState;
import com.greghaskins.spectrum.internal.Suite;
import com.greghaskins.spectrum.internal.blocks.IdempotentBlock;
import com.greghaskins.spectrum.internal.hooks.EagerLetHook;
import com.greghaskins.spectrum.internal.hooks.Hook;
import com.greghaskins.spectrum.internal.hooks.HookContext.AppliesTo;
import com.greghaskins.spectrum.internal.hooks.HookContext.Precedence;
Expand Down Expand Up @@ -185,6 +186,26 @@ static <T> Supplier<T> let(final ThrowingSupplier<T> supplier) {
return letHook;
}

/**
* A value that will be calculated fresh at the start of each spec and cannot bleed across specs.
*
* <p>
* Note that {@code eagerLet} is eagerly evaluated: the {@code supplier} is called at the start
* of the spec, before {@code beforeEach} blocks.
* </p>
*
* @param <T> The type of value
* @param supplier {@link ThrowingSupplier} function that either generates the value, or throws a
* {@link Throwable}
* @return supplier which is refreshed for each spec's context
*/
static <T> Supplier<T> eagerLet(final ThrowingSupplier<T> supplier) {
EagerLetHook<T> eagerLetHook = new EagerLetHook<>(supplier);
DeclarationState.instance().addHook(eagerLetHook, AppliesTo.ATOMIC_ONLY, Precedence.LOCAL);

return eagerLetHook;
}

/**
* Define a test context. Alias for {@link #describe}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.greghaskins.spectrum.internal.hooks;

import com.greghaskins.spectrum.ThrowingSupplier;

/**
* Implementation of an eager version of {@code let}.
*
* <p>Sematics are the same as with {@link LetHook}, except that all values are calculated at the
* start of the test, rather than on an as-needed basis.
*/
public class EagerLetHook<T> extends AbstractSupplyingHook<T> {
private final ThrowingSupplier<T> supplier;

public EagerLetHook(final ThrowingSupplier<T> supplier) {
this.supplier = supplier;
}

protected T before() {
return supplier.get();
}

protected String getExceptionMessageIfUsedAtDeclarationTime() {
return "Cannot use the value from eagerLet() in a suite declaration. "
+ "It may only be used in the context of a running spec.";
}
}
Loading

0 comments on commit 24276b1

Please sign in to comment.