Skip to content

Experimental feature for unboxing. #122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions docs/JunitRules.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,6 @@ The rules are applied and the test object created just in time for each atomic t
[SpringSpecJUnitStyle](../src/test/java/specs/SpringSpecJUnitStyle.java) and
[SpringSpecWithRuleClasses](../src/test/java/specs/SpringSpecWithRuleClasses.java)





### What is Supported

* `@ClassRule` is applied
Expand All @@ -93,6 +89,7 @@ The rules are applied and the test object created just in time for each atomic t
* `TestRule`s are applied at the level of each atomic test
* `MethodRule`s are applied at the level of each atomic test
* `junitMixin` is implemented to be thread-safe
* `junitMixin` provides an overload for unboxing the supplier - `junitMixin(SomeClass.class, SomeInterface.class)` - if your mixin has an interface to it. See also [`let` - when get is getting you down.](VariablesAndValues.md)

### What is not supported

Expand Down
40 changes: 40 additions & 0 deletions docs/VariablesAndValues.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,43 @@ describe("The Variable convenience wrapper", () -> {

});
```

### When `get` is getting you down

`Variable` objects and the `Supplier` returned by `let` require use of the `get` function to read their contents. This requires you to write `get()` throughout
your code, which can lead to a little bloat.

If the object in question can be used via an interface, then you can use the `unbox` function inside `Unboxer` to wrap the supplier with a proxy. This will reduce your boilerplate code.

E.g.

```java
Supplier<List<String>> list = let(ArrayList::new);

it("can use the object", () -> {
list.get().add("Hello");
assertThat(list.get().get(0), is("Hello"));
});
```

can be replaced by

```java
List<String> list = unbox(let(ArrayList::new), List.class);

it("can use the object as though it was not in a supplier", () -> {
list.add("Hello");
assertThat(list.get(0), is("Hello"));
});
```

The `unbox` method can be used with any `Supplier<>`. There is also an overload of `let` as a short form for this:

```java
List<String> list = let(ArrayList::new, List.class);

it("can use the object as though it was not in a supplier", () -> {
list.add("Hello");
assertThat(list.get(0), is("Hello"));
});
```
21 changes: 20 additions & 1 deletion src/main/java/com/greghaskins/spectrum/Configure.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.greghaskins.spectrum;

import static com.greghaskins.spectrum.Unboxer.unbox;

import com.greghaskins.spectrum.internal.DeclarationState;
import com.greghaskins.spectrum.internal.configuration.BlockFocused;
import com.greghaskins.spectrum.internal.configuration.BlockIgnore;
Expand All @@ -11,7 +13,6 @@
import com.greghaskins.spectrum.internal.junit.Rules;

import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

public interface Configure {
Expand Down Expand Up @@ -154,4 +155,22 @@ static FilterConfigurationChain excludeTags(String... tagsToExclude) {
static <T> Supplier<T> junitMixin(final Class<T> classWithRules) {
return Rules.applyRules(classWithRules, DeclarationState.instance()::addHook);
}

/**
* Uses the given class as a mix-in for JUnit rules to be applied. These rules will cascade down
* and be applied at the level of specs or atomic specs. Provide a proxy to the eventual mix-in
* via the interface given.
*
* @param classWithRules Class to create and apply rules to for each spec.
* @param interfaceToReturn the type of interface the caller requires the proxy object to be
* @param <T> type of the mixin object
* @param <R> the required type at the consumer - allowing for generic interfaces
* (e.g. <code>List&lt;String&gt;</code>)
* @param <S> type of the interface common to the rules object and the proxy
* @return a proxy to the rules object
*/
static <T extends S, R extends S, S> R junitMixin(final Class<T> classWithRules,
final Class<S> interfaceToReturn) {
return unbox(junitMixin(classWithRules), interfaceToReturn);
}
}
29 changes: 29 additions & 0 deletions src/main/java/com/greghaskins/spectrum/Spectrum.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.greghaskins.spectrum;

import static com.greghaskins.spectrum.Unboxer.unbox;

import com.greghaskins.spectrum.dsl.specification.Specification;
import com.greghaskins.spectrum.internal.DeclarationState;
import com.greghaskins.spectrum.internal.Suite;
Expand Down Expand Up @@ -234,6 +236,33 @@ public static <T> Supplier<T> let(final com.greghaskins.spectrum.ThrowingSupplie
return Specification.let(supplier);
}

/**
* A value that will be fresh within each spec and cannot bleed across specs. This is provided
* as a proxy to a lazy loading object, with the interface given.
*
* <p>
* Note that {@code let} is lazy-evaluated: the internal {@code supplier} is not called until the first
* time it is used.
* </p>
*
* @param <T> The type of value
* @param <R> the required type at the consumer - allowing for generic interfaces
* (e.g. <code>List&lt;String&gt;</code>)
* @param <S> the type of the value's interface.
* @param supplier {@link com.greghaskins.spectrum.ThrowingSupplier} function that either
* generates the value, or throws a {@link Throwable}
* @param interfaceToUse an interface type, that is supported by the supplied object and can be used
* to access it through the supplier without having to call {@link Supplier#get()}
* @return proxy to the object supplied, using the interface given
* @see Specification#let
* @see Unboxer#unbox(Supplier, Class)
*/
public static <T extends S, R extends S, S> R let(
final com.greghaskins.spectrum.ThrowingSupplier<T> supplier,
Class<S> interfaceToUse) {
return unbox(let(supplier), interfaceToUse);
}

private final Suite rootSuite;

/**
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/com/greghaskins/spectrum/Unboxer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.greghaskins.spectrum;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.function.Supplier;

/**
* If the number of <code>get</code> calls when using a {@link java.util.function.Supplier}
* makes the test code harder to read, you can use {@link Unboxer#unbox} to create
* a proxy object that masquerades as an interface to the item in the Supplier. This
* can be used with <code>let</code> and {@link Variable}
*/
public interface Unboxer {
/**
* Provide a proxy object to the contents of a supplier to reduce the number of
* {@link Supplier#get} calls in your code. Note, if using {@link Variable} then you
* will want to keep a reference to the original object so you can use {@link Variable#set}.
* @param supplier supplier of object
* @param asClass target type of the unboxer - must be interface
* @param <T> type within the supplier
* @param <R> (inferred) type of the unboxer object (allows generic types to be preserved)
* @param <S> class of the interface to return
* @return a proxy to the contents of the supplier
*/
@SuppressWarnings("unchecked")
static <T extends S, R extends S, S> R unbox(Supplier<T> supplier, Class<S> asClass) {
return (R) Proxy.newProxyInstance(asClass.getClassLoader(), new Class<?>[] {asClass},
(Object proxy, Method method, Object[] args) -> method.invoke(supplier.get(), args));
}
}
31 changes: 28 additions & 3 deletions src/test/java/specs/JUnitRuleExample.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,19 @@

@RunWith(Spectrum.class)
public class JUnitRuleExample {
public interface JUnitTempFolderInterface {
TemporaryFolder getTempFolder();
}

// mixins for the Spectrum native style of mixin
public static class TempFolderRuleMixin {
public static class TempFolderRuleMixin implements JUnitTempFolderInterface {
@Rule
public TemporaryFolder tempFolderRule = new TemporaryFolder();

@Override
public TemporaryFolder getTempFolder() {
return tempFolderRule;
}
}

// alternative morphology of providing a rule - see http://junit.org/junit4/javadoc/4.12/org/junit/Rule.html
Expand Down Expand Up @@ -95,12 +104,28 @@ public static void beforeClass() {
it("has access to an initialised object", () -> {
assertNotNull(tempFolderRuleMixin.get().getFolder().getRoot());
});

describe("A spec with a rule mix-in via interface", () -> {
JUnitTempFolderInterface tempFolderGetter =
junitMixin(TempFolderRuleMixin.class, JUnitTempFolderInterface.class);

it("has access to the rule-provided object at the top level", () -> {
checkCanUseTempFolderAndRecordWhatItWas(ruleProvidedFoldersSeen,
tempFolderGetter.getTempFolder());
});

});
});
}

private void checkCanUseTempFolderAndRecordWhatItWas(Set<File> filesSeen,
Supplier<TempFolderRuleMixin> tempFolderRuleMixin) {
assertNotNull(tempFolderRuleMixin.get().tempFolderRule.getRoot());
filesSeen.add(tempFolderRuleMixin.get().tempFolderRule.getRoot());
checkCanUseTempFolderAndRecordWhatItWas(filesSeen, tempFolderRuleMixin.get().tempFolderRule);
}

private void checkCanUseTempFolderAndRecordWhatItWas(Set<File> filesSeen,
TemporaryFolder folder) {
assertNotNull(folder.getRoot());
filesSeen.add(folder.getRoot());
}
}
68 changes: 68 additions & 0 deletions src/test/java/specs/UnboxerSpecs.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package specs;

import static com.greghaskins.spectrum.Spectrum.describe;
import static com.greghaskins.spectrum.Spectrum.it;
import static com.greghaskins.spectrum.Spectrum.let;
import static com.greghaskins.spectrum.Unboxer.unbox;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;

import com.greghaskins.spectrum.Spectrum;
import com.greghaskins.spectrum.Variable;

import org.junit.runner.RunWith;

import java.util.ArrayList;
import java.util.List;

@RunWith(Spectrum.class)
public class UnboxerSpecs {
{
describe("Using unboxer on let", () -> {
List<String> list = unbox(let(ArrayList::new), List.class);

it("can use the object as though it was not in a supplier", () -> {
list.add("Hello");
assertThat(list.get(0), is("Hello"));
});

it("can use multi-parameter methods correctly", () -> {
list.add("a");
list.add("b");
list.add(0, "_");

assertThat(list.size(), is(3));
assertThat(list.get(0), is("_"));
});
});

describe("Using let overload", () -> {
List<String> list = let(ArrayList::new, List.class);

it("can use the object as though it was not in a supplier", () -> {
list.add("Hello");
assertThat(list.get(0), is("Hello"));
});
});

describe("Using unboxer with variable", () -> {
Variable<ArrayList<String>> listVariable = new Variable<>(new ArrayList<>());
List<String> list = unbox(listVariable, List.class);

it("can read the variable contents", () -> {
list.add("World");
assertThat(list.size(), is(1));
assertThat(list.get(0), is("World"));
});

it("can still read the same list in the next spec", () -> {
assertThat(list.size(), is(1));
});

it("can reset the content via the original variable object", () -> {
listVariable.set(new ArrayList<>());
assertThat(list.size(), is(0));
});
});
}
}