From c188c4a81064869a5ab1db996f6e6ceebeeca0bc Mon Sep 17 00:00:00 2001 From: Ashley Frieze Date: Mon, 4 Sep 2017 09:41:42 +0100 Subject: [PATCH 1/3] Experimental feature for unboxing. --- docs/VariablesAndValues.md | 29 ++++++++++ .../com/greghaskins/spectrum/Unboxer.java | 29 ++++++++++ src/test/java/specs/UnboxerSpecs.java | 58 +++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 src/main/java/com/greghaskins/spectrum/Unboxer.java create mode 100644 src/test/java/specs/UnboxerSpecs.java diff --git a/docs/VariablesAndValues.md b/docs/VariablesAndValues.md index 6c60c1a..fa1323d 100644 --- a/docs/VariablesAndValues.md +++ b/docs/VariablesAndValues.md @@ -80,3 +80,32 @@ 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 = 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 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")); +}); +``` diff --git a/src/main/java/com/greghaskins/spectrum/Unboxer.java b/src/main/java/com/greghaskins/spectrum/Unboxer.java new file mode 100644 index 0000000..8b5223b --- /dev/null +++ b/src/main/java/com/greghaskins/spectrum/Unboxer.java @@ -0,0 +1,29 @@ +package com.greghaskins.spectrum; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.function.Supplier; + +/** + * If the number of get 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 let 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 type within the supplier + * @param type of the unboxer object + * @return a proxy to the contents of the supplier + */ + @SuppressWarnings("unchecked") + static R unbox(Supplier supplier, Class asClass) { + return (R) Proxy.newProxyInstance(asClass.getClassLoader(), new Class[] {asClass}, + (Object proxy, Method method, Object[] args) -> method.invoke(supplier.get(), args)); + } +} diff --git a/src/test/java/specs/UnboxerSpecs.java b/src/test/java/specs/UnboxerSpecs.java new file mode 100644 index 0000000..0063df9 --- /dev/null +++ b/src/test/java/specs/UnboxerSpecs.java @@ -0,0 +1,58 @@ +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 java.util.ArrayList; +import java.util.List; + +import com.greghaskins.spectrum.Spectrum; +import com.greghaskins.spectrum.Variable; +import org.junit.runner.RunWith; + +@RunWith(Spectrum.class) +public class UnboxerSpecs { + { + describe("Using unboxer on let", () -> { + List 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 unboxer with variable", () -> { + Variable> listVariable = new Variable<>(new ArrayList<>()); + List 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)); + }); + }); + } +} From e522f260f5d68f08f3defcef444b219fec16d72f Mon Sep 17 00:00:00 2001 From: Ashley Frieze Date: Mon, 4 Sep 2017 09:46:28 +0100 Subject: [PATCH 2/3] Line endings and JavaDoc --- docs/VariablesAndValues.md | 8 +- .../com/greghaskins/spectrum/Unboxer.java | 31 +++---- src/test/java/specs/UnboxerSpecs.java | 85 ++++++++++--------- 3 files changed, 63 insertions(+), 61 deletions(-) diff --git a/docs/VariablesAndValues.md b/docs/VariablesAndValues.md index fa1323d..6ccb5e1 100644 --- a/docs/VariablesAndValues.md +++ b/docs/VariablesAndValues.md @@ -94,8 +94,8 @@ E.g. Supplier> list = let(ArrayList::new); it("can use the object", () -> { - list.get().add("Hello"); - assertThat(list.get().get(0), is("Hello")); + list.get().add("Hello"); + assertThat(list.get().get(0), is("Hello")); }); ``` @@ -105,7 +105,7 @@ can be replaced by List 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")); + list.add("Hello"); + assertThat(list.get(0), is("Hello")); }); ``` diff --git a/src/main/java/com/greghaskins/spectrum/Unboxer.java b/src/main/java/com/greghaskins/spectrum/Unboxer.java index 8b5223b..af919a7 100644 --- a/src/main/java/com/greghaskins/spectrum/Unboxer.java +++ b/src/main/java/com/greghaskins/spectrum/Unboxer.java @@ -11,19 +11,20 @@ * can be used with let 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 type within the supplier - * @param type of the unboxer object - * @return a proxy to the contents of the supplier - */ - @SuppressWarnings("unchecked") - static R unbox(Supplier supplier, Class asClass) { - return (R) Proxy.newProxyInstance(asClass.getClassLoader(), new Class[] {asClass}, - (Object proxy, Method method, Object[] args) -> method.invoke(supplier.get(), args)); - } + /** + * 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 type within the supplier + * @param (inferred) type of the unboxer object (allows generic types to be preserved) + * @param class of the interface to return + * @return a proxy to the contents of the supplier + */ + @SuppressWarnings("unchecked") + static R unbox(Supplier supplier, Class asClass) { + return (R) Proxy.newProxyInstance(asClass.getClassLoader(), new Class[] {asClass}, + (Object proxy, Method method, Object[] args) -> method.invoke(supplier.get(), args)); + } } diff --git a/src/test/java/specs/UnboxerSpecs.java b/src/test/java/specs/UnboxerSpecs.java index 0063df9..b59ce41 100644 --- a/src/test/java/specs/UnboxerSpecs.java +++ b/src/test/java/specs/UnboxerSpecs.java @@ -7,52 +7,53 @@ import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; -import java.util.ArrayList; -import java.util.List; - 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 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 unboxer with variable", () -> { - Variable> listVariable = new Variable<>(new ArrayList<>()); - List 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)); - }); - }); - } + { + describe("Using unboxer on let", () -> { + List 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 unboxer with variable", () -> { + Variable> listVariable = new Variable<>(new ArrayList<>()); + List 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)); + }); + }); + } } From 456ffa5f05a0b70e8e814f40a037f848779377b4 Mon Sep 17 00:00:00 2001 From: Ashley Frieze Date: Fri, 8 Sep 2017 09:38:04 +0100 Subject: [PATCH 3/3] Extend with suggestions from Greg --- docs/JunitRules.md | 5 +-- docs/VariablesAndValues.md | 11 +++++++ .../com/greghaskins/spectrum/Configure.java | 21 ++++++++++++- .../com/greghaskins/spectrum/Spectrum.java | 29 +++++++++++++++++ .../com/greghaskins/spectrum/Unboxer.java | 2 +- src/test/java/specs/JUnitRuleExample.java | 31 +++++++++++++++++-- src/test/java/specs/UnboxerSpecs.java | 9 ++++++ 7 files changed, 99 insertions(+), 9 deletions(-) diff --git a/docs/JunitRules.md b/docs/JunitRules.md index 4afa73d..b942aac 100644 --- a/docs/JunitRules.md +++ b/docs/JunitRules.md @@ -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 @@ -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 diff --git a/docs/VariablesAndValues.md b/docs/VariablesAndValues.md index 6ccb5e1..4022f8c 100644 --- a/docs/VariablesAndValues.md +++ b/docs/VariablesAndValues.md @@ -109,3 +109,14 @@ it("can use the object as though it was not in a supplier", () -> { 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 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")); +}); +``` diff --git a/src/main/java/com/greghaskins/spectrum/Configure.java b/src/main/java/com/greghaskins/spectrum/Configure.java index bcbe257..1b5099d 100644 --- a/src/main/java/com/greghaskins/spectrum/Configure.java +++ b/src/main/java/com/greghaskins/spectrum/Configure.java @@ -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; @@ -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 { @@ -154,4 +155,22 @@ static FilterConfigurationChain excludeTags(String... tagsToExclude) { static Supplier junitMixin(final Class 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 type of the mixin object + * @param the required type at the consumer - allowing for generic interfaces + * (e.g. List<String>) + * @param type of the interface common to the rules object and the proxy + * @return a proxy to the rules object + */ + static R junitMixin(final Class classWithRules, + final Class interfaceToReturn) { + return unbox(junitMixin(classWithRules), interfaceToReturn); + } } diff --git a/src/main/java/com/greghaskins/spectrum/Spectrum.java b/src/main/java/com/greghaskins/spectrum/Spectrum.java index 953e413..44f788e 100644 --- a/src/main/java/com/greghaskins/spectrum/Spectrum.java +++ b/src/main/java/com/greghaskins/spectrum/Spectrum.java @@ -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; @@ -234,6 +236,33 @@ public static Supplier 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. + * + *

+ * Note that {@code let} is lazy-evaluated: the internal {@code supplier} is not called until the first + * time it is used. + *

+ * + * @param The type of value + * @param the required type at the consumer - allowing for generic interfaces + * (e.g. List<String>) + * @param 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 R let( + final com.greghaskins.spectrum.ThrowingSupplier supplier, + Class interfaceToUse) { + return unbox(let(supplier), interfaceToUse); + } + private final Suite rootSuite; /** diff --git a/src/main/java/com/greghaskins/spectrum/Unboxer.java b/src/main/java/com/greghaskins/spectrum/Unboxer.java index af919a7..ccd6d5f 100644 --- a/src/main/java/com/greghaskins/spectrum/Unboxer.java +++ b/src/main/java/com/greghaskins/spectrum/Unboxer.java @@ -24,7 +24,7 @@ public interface Unboxer { */ @SuppressWarnings("unchecked") static R unbox(Supplier supplier, Class asClass) { - return (R) Proxy.newProxyInstance(asClass.getClassLoader(), new Class[] {asClass}, + return (R) Proxy.newProxyInstance(asClass.getClassLoader(), new Class[] {asClass}, (Object proxy, Method method, Object[] args) -> method.invoke(supplier.get(), args)); } } diff --git a/src/test/java/specs/JUnitRuleExample.java b/src/test/java/specs/JUnitRuleExample.java index 3271f30..d0e69f9 100644 --- a/src/test/java/specs/JUnitRuleExample.java +++ b/src/test/java/specs/JUnitRuleExample.java @@ -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 @@ -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 filesSeen, Supplier tempFolderRuleMixin) { - assertNotNull(tempFolderRuleMixin.get().tempFolderRule.getRoot()); - filesSeen.add(tempFolderRuleMixin.get().tempFolderRule.getRoot()); + checkCanUseTempFolderAndRecordWhatItWas(filesSeen, tempFolderRuleMixin.get().tempFolderRule); + } + + private void checkCanUseTempFolderAndRecordWhatItWas(Set filesSeen, + TemporaryFolder folder) { + assertNotNull(folder.getRoot()); + filesSeen.add(folder.getRoot()); } } diff --git a/src/test/java/specs/UnboxerSpecs.java b/src/test/java/specs/UnboxerSpecs.java index b59ce41..3cd1f2e 100644 --- a/src/test/java/specs/UnboxerSpecs.java +++ b/src/test/java/specs/UnboxerSpecs.java @@ -36,6 +36,15 @@ public class UnboxerSpecs { }); }); + describe("Using let overload", () -> { + List 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> listVariable = new Variable<>(new ArrayList<>()); List list = unbox(listVariable, List.class);