From 3b9ec5f22837401607439e7f9664fd989ff137ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4fer?= Date: Wed, 4 Jan 2017 21:49:13 +0100 Subject: [PATCH 1/3] JUnit5: first, experimental support for dynamic tests --- .../jgiven/impl/ReportModelHolder.java | 26 +++++++ .../com/tngtech/jgiven/impl/Scenario.java | 4 -- .../com/tngtech/jgiven/impl/ScenarioBase.java | 72 +++++++++++++++++++ .../jgiven/junit5/DynamicJGivenTest.java | 61 ++++++++++++++++ .../jgiven/junit5/JGivenExecutable.java | 20 ++++++ .../jgiven/junit5/JGivenExtension.java | 30 ++++++-- .../tngtech/jgiven/junit5/ScenarioTest.java | 2 + .../jgiven/junit5/SimpleScenarioTest.java | 9 ++- .../jgiven/junit5/test/DynamicTestTest.java | 16 ++++- .../jgiven/junit5/test/GivenStage.java | 14 +++- .../tngtech/jgiven/junit5/test/WhenStage.java | 19 +++++ 11 files changed, 256 insertions(+), 17 deletions(-) create mode 100644 jgiven-core/src/main/java/com/tngtech/jgiven/impl/ReportModelHolder.java create mode 100644 jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/DynamicJGivenTest.java create mode 100644 jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/JGivenExecutable.java diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ReportModelHolder.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ReportModelHolder.java new file mode 100644 index 00000000000..5f6b6662fc9 --- /dev/null +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ReportModelHolder.java @@ -0,0 +1,26 @@ +package com.tngtech.jgiven.impl; + +import com.tngtech.jgiven.report.model.ReportModel; + +public class ReportModelHolder { + private final ThreadLocal reportModel = new ThreadLocal(); + + private static final ReportModelHolder INSTANCE = new ReportModelHolder(); + + public static ReportModelHolder get() { + return INSTANCE; + } + + public ReportModel getReportModelOfCurrentThread() { + return reportModel.get(); + } + + public void setReportModelOfCurrentThread(ReportModel reportModel) { + this.reportModel.set(reportModel); + } + + public void removeReportModelOfCurrentThread() { + reportModel.remove(); + } + +} diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/Scenario.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/Scenario.java index 7154c5900a0..3106da2180a 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/Scenario.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/Scenario.java @@ -41,10 +41,6 @@ public THEN getThenStage() { return thenStage; } - public void addIntroWord( String word ) { - executor.addIntroWord( word ); - } - /** * Creates a scenario with 3 different steps classes. * diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioBase.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioBase.java index 646860dc41f..3a318c065f8 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioBase.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioBase.java @@ -117,4 +117,76 @@ public void section( String sectionTitle ) { public void setStageCreator(StageCreator stageCreator) { this.executor.setStageCreator(stageCreator); } + + public void addIntroWord( String word ) { + executor.addIntroWord( word ); + } + + /** + * Alias for {@link #addStage} + */ + public T stage(Class stageClass) { + return addStage(stageClass); + } + + /** + * Alias for {@link #addIntroWord(String)} + * @see #addIntroWord(String) + */ + public ScenarioBase intro(String introWord) { + addIntroWord(introWord); + return this; + } + + /** + * Convenience method for adding the 'given' intro word + * and adding a stage class. Equivalent to + * + *
+     *     addIntroWord("given");
+     *     return addStage(stageClass);
+     * 
+ * + * @see #addIntroWord(String) + * @see #addStage(Class) + */ + public T given(Class stageClass) { + addIntroWord("given"); + return addStage(stageClass); + } + + /** + * Convenience method for adding the 'when' intro word + * and adding a stage class. Equivalent to + * + *
+     *     addIntroWord("when");
+     *     return addStage(stageClass);
+     * 
+ * + * @see #addIntroWord(String) + * @see #addStage(Class) + */ + public T when(Class stageClass) { + addIntroWord("when"); + return addStage(stageClass); + } + + /** + * Convenience method for adding the 'then' intro word + * and adding a stage class. Equivalent to + * + *
+     *     addIntroWord("then");
+     *     return addStage(stageClass);
+     * 
+ * + * @see #addIntroWord(String) + * @see #addStage(Class) + */ + public T then(Class stageClass) { + addIntroWord("then"); + return addStage(stageClass); + } + } diff --git a/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/DynamicJGivenTest.java b/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/DynamicJGivenTest.java new file mode 100644 index 00000000000..6cb284400d1 --- /dev/null +++ b/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/DynamicJGivenTest.java @@ -0,0 +1,61 @@ +package com.tngtech.jgiven.junit5; + +import com.tngtech.jgiven.impl.ReportModelHolder; +import com.tngtech.jgiven.impl.ScenarioBase; +import com.tngtech.jgiven.report.model.ReportModel; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.function.Executable; + +import java.util.EnumSet; + +import static com.tngtech.jgiven.report.model.ExecutionStatus.FAILED; +import static com.tngtech.jgiven.report.model.ExecutionStatus.SUCCESS; + +/** + * @since 0.15.0 + */ +public class DynamicJGivenTest { + + /** + * JGiven-specific factory method for creating dynamic JUnit 5 tests + * + *

HIGHLY EXPERIMENTAL

+ * + * Most likely this method will change in future versions of JGiven, please don't + * use this method for any serious projects, yet. + * + * @see DynamicTest#dynamicTest(String, Executable) + * @param displayName the display name for the dynamic test; never + * {@code null} or blank + * @param executable the executable code block for the dynamic test; + * never {@code null} + */ + public static DynamicTest dynamicJGivenTest(String displayName, JGivenExecutable executable) { + return DynamicTest.dynamicTest(displayName, executableWrapper(displayName, executable)); + } + + private static Executable executableWrapper(final String displayName, final JGivenExecutable executable) { + return new Executable() { + @Override + public void execute() throws Throwable { + ScenarioBase scenario = new ScenarioBase(); + ReportModel reportModel = ReportModelHolder.get().getReportModelOfCurrentThread(); + scenario.setModel(reportModel); + scenario.startScenario(displayName); + try { + executable.execute(scenario); + + scenario.finished(); + // ignore test when scenario is not implemented + Assumptions.assumeTrue( EnumSet.of( SUCCESS, FAILED ).contains( scenario.getScenarioModel().getExecutionStatus() ) ); + } catch( Exception e ) { + scenario.finished(); + scenario.getExecutor().failed( e ); + throw e; + } + } + }; + } + +} diff --git a/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/JGivenExecutable.java b/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/JGivenExecutable.java new file mode 100644 index 00000000000..32f04c95a15 --- /dev/null +++ b/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/JGivenExecutable.java @@ -0,0 +1,20 @@ +package com.tngtech.jgiven.junit5; + +import com.tngtech.jgiven.impl.ScenarioBase; + +/** + * JGiven-specific variant of the {@link org.junit.jupiter.api.function.Executable} interface + * of JUnit 5 for writing dynamic tests. + * + *

HIGHLY EXPERIMENTAL

+ * + * Most likely this interface will change in future versions of JGiven without prior-notice, + * please don't use this for any serious projects, yet. + * + * @see org.junit.jupiter.api.function.Executable + * @since 0.15.0 + */ +@FunctionalInterface +public interface JGivenExecutable { + void execute(ScenarioBase scenario) throws Throwable; +} diff --git a/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/JGivenExtension.java b/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/JGivenExtension.java index a0c0a26ba16..82e1a042403 100644 --- a/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/JGivenExtension.java +++ b/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/JGivenExtension.java @@ -7,10 +7,9 @@ import java.util.EnumSet; import java.util.List; -import com.tngtech.jgiven.config.ConfigurationUtil; -import com.tngtech.jgiven.impl.Config; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; @@ -22,6 +21,8 @@ import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import com.tngtech.jgiven.base.ScenarioTestBase; +import com.tngtech.jgiven.config.ConfigurationUtil; +import com.tngtech.jgiven.impl.ReportModelHolder; import com.tngtech.jgiven.impl.ScenarioBase; import com.tngtech.jgiven.impl.ScenarioHolder; import com.tngtech.jgiven.report.impl.CommonReportHelper; @@ -43,6 +44,7 @@ * * @see ScenarioTest * @see SimpleScenarioTest + * @since 0.14.0 */ public class JGivenExtension implements TestInstancePostProcessor, @@ -64,26 +66,42 @@ public void beforeAll( ContainerExtensionContext context ) throws Exception { } context.getStore( NAMESPACE ).put( REPORT_MODEL, reportModel ); - ConfigurationUtil.getConfiguration(context.getTestClass().get()) - .configureTag(Tag.class) - .description("JUnit 5 Tag") - .color("orange"); + ConfigurationUtil.getConfiguration( context.getTestClass().get() ) + .configureTag( Tag.class ) + .description( "JUnit 5 Tag" ) + .color( "orange" ); } @Override public void afterAll( ContainerExtensionContext context ) throws Exception { + ReportModelHolder.get().removeReportModelOfCurrentThread(); new CommonReportHelper().finishReport( (ReportModel) context.getStore( NAMESPACE ).get( REPORT_MODEL ) ); } @Override public void beforeEach( TestExtensionContext context ) throws Exception { + ReportModel reportModel = (ReportModel) context.getStore( NAMESPACE ).get( REPORT_MODEL ); + ReportModelHolder.get().setReportModelOfCurrentThread( reportModel ); + + if( isTestFactory( context ) ) { + return; + } + List args = new ArrayList(); getScenario().startScenario( context.getTestClass().get(), context.getTestMethod().get(), args ); + } + private boolean isTestFactory( TestExtensionContext context ) { + return context.getTestMethod().get().getAnnotation( TestFactory.class ) != null; } @Override public void afterEach( TestExtensionContext context ) throws Exception { + ReportModelHolder.get().removeReportModelOfCurrentThread(); + + if( isTestFactory( context ) ) { + return; + } ScenarioBase scenario = getScenario(); try { diff --git a/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/ScenarioTest.java b/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/ScenarioTest.java index 10e497ea843..6ec1ca77416 100644 --- a/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/ScenarioTest.java +++ b/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/ScenarioTest.java @@ -17,6 +17,8 @@ * * @see JGivenExtension * @see SimpleScenarioTest + * @since 0.14.0 + * */ @ExtendWith( JGivenExtension.class ) public class ScenarioTest extends ScenarioTestBase { diff --git a/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/SimpleScenarioTest.java b/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/SimpleScenarioTest.java index 9eea7abfd2c..27b0f457744 100644 --- a/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/SimpleScenarioTest.java +++ b/jgiven-junit5/src/main/java/com/tngtech/jgiven/junit5/SimpleScenarioTest.java @@ -1,12 +1,15 @@ package com.tngtech.jgiven.junit5; -import com.tngtech.jgiven.base.SimpleScenarioTestBase; import org.junit.jupiter.api.extension.ExtendWith; -import com.tngtech.jgiven.base.ScenarioTestBase; +import com.tngtech.jgiven.base.SimpleScenarioTestBase; import com.tngtech.jgiven.impl.Scenario; - +/** + * JUnit 5 + * + * @since 0.14.0 +*/ @ExtendWith( JGivenExtension.class ) public class SimpleScenarioTest extends SimpleScenarioTestBase { diff --git a/jgiven-junit5/src/test/java/com/tngtech/jgiven/junit5/test/DynamicTestTest.java b/jgiven-junit5/src/test/java/com/tngtech/jgiven/junit5/test/DynamicTestTest.java index 8c50ef75e40..5c22f96071f 100644 --- a/jgiven-junit5/src/test/java/com/tngtech/jgiven/junit5/test/DynamicTestTest.java +++ b/jgiven-junit5/src/test/java/com/tngtech/jgiven/junit5/test/DynamicTestTest.java @@ -11,6 +11,7 @@ import java.util.Arrays; import java.util.Collection; +import static com.tngtech.jgiven.junit5.DynamicJGivenTest.dynamicJGivenTest; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.DynamicTest.dynamicTest; @@ -23,8 +24,19 @@ public class DynamicTestTest { @TestFactory Collection dynamicTestsFromCollection() { return Arrays.asList( - dynamicTest("1st dynamic test", () -> assertTrue(true)), - dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2)) + dynamicJGivenTest("1st dynamic test", (s) -> { + s.given(GivenStage.class) + .some_state(); + s.when(WhenStage.class) + .some_action(); + + }), + dynamicJGivenTest("2nd dynamic test", (scenario) -> { + scenario.given(GivenStage.class) + .some_state() + .when().some_action() + .then().some_outcome(); + }) ); } diff --git a/jgiven-junit5/src/test/java/com/tngtech/jgiven/junit5/test/GivenStage.java b/jgiven-junit5/src/test/java/com/tngtech/jgiven/junit5/test/GivenStage.java index b49b2e754a3..163b315a319 100644 --- a/jgiven-junit5/src/test/java/com/tngtech/jgiven/junit5/test/GivenStage.java +++ b/jgiven-junit5/src/test/java/com/tngtech/jgiven/junit5/test/GivenStage.java @@ -1,15 +1,25 @@ package com.tngtech.jgiven.junit5.test; +import com.tngtech.jgiven.Stage; import com.tngtech.jgiven.annotation.ExpectedScenarioState; import com.tngtech.jgiven.annotation.ProvidedScenarioState; -public class GivenStage { +public class GivenStage extends Stage { @ProvidedScenarioState String someState; - public void some_state() { + public GivenStage some_state() { someState = "Some State"; + return self(); + } + + public GivenStage some_action() { + return self(); + } + + public GivenStage some_outcome() { + return self(); } } diff --git a/jgiven-junit5/src/test/java/com/tngtech/jgiven/junit5/test/WhenStage.java b/jgiven-junit5/src/test/java/com/tngtech/jgiven/junit5/test/WhenStage.java index 851e3faf2c6..5b899af324d 100644 --- a/jgiven-junit5/src/test/java/com/tngtech/jgiven/junit5/test/WhenStage.java +++ b/jgiven-junit5/src/test/java/com/tngtech/jgiven/junit5/test/WhenStage.java @@ -1,5 +1,8 @@ package com.tngtech.jgiven.junit5.test; +import com.tngtech.jgiven.annotation.AfterScenario; +import com.tngtech.jgiven.annotation.BeforeScenario; +import com.tngtech.jgiven.annotation.BeforeStage; import com.tngtech.jgiven.annotation.ExpectedScenarioState; import com.tngtech.jgiven.annotation.ProvidedScenarioState; import org.junit.jupiter.api.Assertions; @@ -12,6 +15,22 @@ public class WhenStage { @ProvidedScenarioState String someResult; + @BeforeScenario + protected void someBeforeScenario() { + System.out.println("BEFORE SCENARIO"); + } + + @AfterScenario + protected void someAfterScenario() { + System.out.println("AFTER SCENARIO"); + } + + + @BeforeStage + protected void someBeforeStage() { + Assertions.assertNotNull(someState); + } + void some_action() { Assertions.assertNotNull(someState); someResult = "Some Result"; From 9e676bffad4d00e0b1a4ddfef8e4837ee7899587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4fer?= Date: Thu, 5 Jan 2017 09:43:31 +0100 Subject: [PATCH 2/3] JUnit 5: add documentation for dynamic tests --- docs/junit5.adoc | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/junit5.adoc b/docs/junit5.adoc index 020852a45f8..c9d0be2a0f3 100644 --- a/docs/junit5.adoc +++ b/docs/junit5.adoc @@ -81,6 +81,43 @@ public class JGiven5ScenarioTest } ---- +=== Dynamic Tests +JUnit 5 introduces so-called _dynamic tests_. +Dynamic tests are created completely differently than normal JUnit 5 tests, by using _factory methods_. +A factory method is a method annotated with `@TestFactory` returning a sequence of `DynamicTest` instances. +Normally you create instances of `DynamicTest` by using the `DynamicTest.dynamicTest` method. +This method takes two parameters: a display name in form of a String and a lambda expression that contains +the actual test implementation. + +==== Dynamic JGiven Tests +To write dynamic JGiven tests, all you have to do is to use the method `DynamicJGivenTest.dynamicJGivenTest` +to create instances of `DynamicTest`. +The difference to the JUnit 5 method is that the lambda expression of the JGiven method takes a `ScenarioBase` parameter. +You use this parameter to add stages to the scenario. + +[source,java] +---- +@ExtendWith(JGivenExtension.class) +public class DynamicTestTest { + + @TestFactory + Collection dynamicTests() { + return Arrays.asList( + dynamicJGivenTest("1st dynamic test", (scenario) -> { + scenario.given(GivenStage.class) + .some_state(); + scenario.when(WhenStage.class) + .some_action(); + }) + ); + } +} +---- + +CAUTION: Dynamic JGiven Tests is currently a highly experimental feature and will most likely +be changed in backwards-incompatible ways in future versions of JGiven. Use with caution. +Feedback is welcome! + === Example Project You find a complete example project on GitHub: https://github.com/TNG/JGiven/tree/master/example-projects/junit5 From 1adb5764dbb75fed0bbc4b12bb2bcc2dd790862e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4fer?= Date: Mon, 9 Jan 2017 09:19:38 +0100 Subject: [PATCH 3/3] Build: show stacktraces of failing tests --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index f805c96752d..859db3af966 100644 --- a/build.gradle +++ b/build.gradle @@ -125,6 +125,8 @@ configure(subprojects.findAll {!it.name.contains("android")}) { } testLogging { showStandardStreams = true + events "failed" + exceptionFormat "full" } }