Skip to content
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

JUnit5: first, experimental support for dynamic tests #289

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ configure(subprojects.findAll {!it.name.contains("android")}) {
}
testLogging {
showStandardStreams = true
events "failed"
exceptionFormat "full"
}
}

Expand Down
37 changes: 37 additions & 0 deletions docs/junit5.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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<DynamicTest> 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.tngtech.jgiven.impl;

import com.tngtech.jgiven.report.model.ReportModel;

public class ReportModelHolder {
private final ThreadLocal<ReportModel> reportModel = new ThreadLocal<ReportModel>();

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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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> T stage(Class<T> 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
*
* <pre>
* addIntroWord("given");
* return addStage(stageClass);
* </pre>
*
* @see #addIntroWord(String)
* @see #addStage(Class)
*/
public <T> T given(Class<T> stageClass) {
addIntroWord("given");
return addStage(stageClass);
}

/**
* Convenience method for adding the 'when' intro word
* and adding a stage class. Equivalent to
*
* <pre>
* addIntroWord("when");
* return addStage(stageClass);
* </pre>
*
* @see #addIntroWord(String)
* @see #addStage(Class)
*/
public <T> T when(Class<T> stageClass) {
addIntroWord("when");
return addStage(stageClass);
}

/**
* Convenience method for adding the 'then' intro word
* and adding a stage class. Equivalent to
*
* <pre>
* addIntroWord("then");
* return addStage(stageClass);
* </pre>
*
* @see #addIntroWord(String)
* @see #addStage(Class)
*/
public <T> T then(Class<T> stageClass) {
addIntroWord("then");
return addStage(stageClass);
}

}
Original file line number Diff line number Diff line change
@@ -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
*
* <h1>HIGHLY EXPERIMENTAL</h1>
*
* 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;
}
}
};
}

}
Original file line number Diff line number Diff line change
@@ -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.
*
* <h1>HIGHLY EXPERIMENTAL</h1>
*
* 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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -43,6 +44,7 @@
*
* @see ScenarioTest
* @see SimpleScenarioTest
* @since 0.14.0
*/
public class JGivenExtension implements
TestInstancePostProcessor,
Expand All @@ -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<NamedArgument> args = new ArrayList<NamedArgument>();
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
*
* @see JGivenExtension
* @see SimpleScenarioTest
* @since 0.14.0
*
*/
@ExtendWith( JGivenExtension.class )
public class ScenarioTest<GIVEN, WHEN, THEN> extends ScenarioTestBase<GIVEN, WHEN, THEN> {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<STAGE> extends SimpleScenarioTestBase<STAGE> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,8 +24,19 @@ public class DynamicTestTest {
@TestFactory
Collection<org.junit.jupiter.api.DynamicTest> 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();
})
);
}

Expand Down