diff --git a/README.md b/README.md index 7f0eff3..7720602 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,15 @@ ## Overview System Stubs is used to test code which depends on methods in `java.lang.System`. -It is published under the [MIT license](http://opensource.org/licenses/MIT) and requires at least Java 8. There is a [walkthrough of its main features](https://www.baeldung.com/java-system-stubs) over on [Baeldung.com](https://www.baeldung.com). +The core is test framework agnostic, but there's explicit support for JUnit 4, JUnit 5 and TestNG in +specialist sub-modules. -System Stubs [originated](History.md) as a fork of System Lambda, -originally by Stefan Birkner, and is a partial rewrite and refactor of it. It has diverged in implementation -from the original, but largely [retains compatibility](History.md#execute-around). +It is published under the [MIT license](http://opensource.org/licenses/MIT) and requires at least Java 11. +There is a [walkthrough of its main features](https://www.baeldung.com/java-system-stubs) over on +[Baeldung.com](https://www.baeldung.com). + +System Stubs [originated](History.md) as a fork of System Lambda, and is a partial rewrite and refactor of it. +It has diverged in implementation from the original, but largely [retains compatibility](History.md#execute-around). It is divided into: @@ -38,6 +42,35 @@ It is divided into: - [`system-stubs-junit4`](system-stubs-junit4/README.md) - a set of JUnit4 rules that activate the stubs around test code - [`system-stubs-jupiter`](system-stubs-jupiter/README.md) - a JUnit 5 extension that automatically injects System Stubs into JUnit 5 tests. +- [`system-stubs-testng`](system-stubs-testng/README.md) - a plugin/listener for the TestNG framework, which automatically +injects System Stubs into TestNG tests. + + +## QuickStart (JUnit 5) + +```java +@ExtendWith(SystemStubsExtension.class) +class WithEnvironmentVariables { + + @SystemStub + private EnvironmentVariables variables = + new EnvironmentVariables("input", "foo"); + + @Test + void hasAccessToEnvironmentVariables() { + assertThat(System.getenv("input")) + .isEqualTo("foo"); + } + + @Test + void changeEnvironmentVariablesDuringTest() { + variables.set("input", "bar"); + + assertThat(System.getenv("input")) + .isEqualTo("bar"); + } +} +``` ## Installation @@ -71,34 +104,28 @@ System Stubs into JUnit 5 tests. ``` -## QuickStart (JUnit 5) - -```java -@ExtendWith(SystemStubsExtension.class) -class WithEnvironmentVariables { - - @SystemStub - private EnvironmentVariables variables = - new EnvironmentVariables("input", "foo"); +### TestNG Plugin - @Test - void hasAccessToEnvironmentVariables() { - assertThat(System.getenv("input")) - .isEqualTo("foo"); - } - - @Test - void changeEnvironmentVariablesDuringTest() { - variables.set("input", "bar"); - - assertThat(System.getenv("input")) - .isEqualTo("bar"); - } -} +```xml + + uk.org.webcompere + system-stubs-testng + 2.1.2 + ``` See the full guide to [JUnit 5](system-stubs-jupiter/README.md), or use it with [JUnit 4](system-stubs-junit4/README.md). +## Catalogue of SystemStubs Objects + +- `EnvironmentVariables` - for overriding the environment variables +- `SystemProperties` - for temporarily overwriting system properties and then restoring them afterwards +- `SystemOut` - for tapping the output to `System.out` +- `SystemErr` - for tapping the output to `System.err` +- `SystemErrAndOut` - for tapping the output to both `System.err` and `System.out` +- `SystemIn` - for providing input to `System.in` +- `SystemExit` - prevents system exit from occurring, recording the exit code + ## Using System Stubs Individually You can declare a system stub object: @@ -121,6 +148,7 @@ a test, for example: ```java EnvironmentVariables env = new EnvironmentVariables("HOST", "localhost"); + // start controlling the environment env.setup(); @@ -187,7 +215,7 @@ the code under the test doesn't use checked exceptions. This is a good argument for using the JUnit4 or JUnit5 plugins, where you do not need to specifically turn the stubbing on via the `execute` method. -## Available Stubs +## How to Use Each of the Stubs ### System.exit diff --git a/pom.xml b/pom.xml index 8786cad..7c05077 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,7 @@ system-stubs-core system-stubs-junit4 system-stubs-jupiter + system-stubs-testng @@ -119,6 +120,13 @@ provided + + + org.testng + testng + 7.7.0 + provided + diff --git a/system-stubs-testng/README.md b/system-stubs-testng/README.md new file mode 100644 index 0000000..c17fd0e --- /dev/null +++ b/system-stubs-testng/README.md @@ -0,0 +1,101 @@ +# System Stubs TestNG + +Provides some automatic instantiation of System Stubs objects during the test lifecycle. + +```xml + + uk.org.webcompere + system-stubs-testng + 2.1.1 + +``` + +## Options + +### Stub Without the Plugin +System Stubs Core can be used with TestNG as it is framework agnostic. + +We can call the `setup` method on any of the stubs in a before method, and `teardown` in the after method. + +```java +private EnvironmentVariables environmentVariables = new EnvironmentVariables(); + +@BeforeTest +public void beforeTest() throws Exception { + environmentVariables.set("setinbefore", "yes"); + + environmentVariables.setup(); +} + +@AfterTest +public void afterTest() throws Exception { + environmentVariables.teardown(); +} +``` + +With this code, we'd expect tests to be able to modify the runtime environment by manipulating the +`environmentVariables` object, and we'd expect the tests to have an environment variable `setinbefore` set +to `yes`. + +Similarly, we can use `setup` and `teardown` inside a test case, or use the `SystemStubs` methods such as +`withEnvironmentVariables`. See the [main documentation](../README.md) for more on the execute around pattern. + +### Using of the Plugin + +The plugin: + +- Automatically instantiates system stubs objects before they're first used by a TestNG annotated method +- Activates the objects during tests +- Turns the objects off after tests + +Usage: + +```java +@Listeners(SystemStubsListener.class) +public class CaptureSystemOutTest { + + @SystemStub + private SystemOut out; + + @BeforeTest + public void beforeTest() { + out.clear(); + } + + @Test + public void canReadThingsSentToSystemOut() { + // simulate the system under test writing to std out + System.out.println("Can I assert this?"); + + assertThat(out.getText()).isEqualTo("Can I assert this?\n"); + } +} +``` + +> Note: in this instance we've used the `SystemOut` stub. We've had to remember to call its `clear` method as it +> will be shared between tests. + +We can use each of the stubs such as: + +- `EnvironmentVariables` - for overriding the environment variables +- `SystemProperties` - for temporarily overwriting system properties and then restoring them afterwards +- `SystemOut` - for tapping the `System.out` +- ... and the others + +All we need to do is: + +- Add the `@Listeners(SystemStubsListener.class)` annotation to our TestNG test class (using an array with {} if we have other listeners) +- Add a field for each System Stub we want to use +- Annotate that field with the `@SystemStubs` annotation + +### Benefits of the Plugin + +With the plugin, there's less boilerplate to write. Any exception handling is also covered by the plugin - or at +least, we don't have to explicitly add `throws` to any of our methods that set up or teardown a stub. + +However, the plugin is simple and opinionated. For fine-grained control of the stubs, the direct method +may sometimes be preferable. + +## Feedback + +This TestNG module is incubating. Please raise issues with examples if it proves to have issues in practice. diff --git a/system-stubs-testng/pom.xml b/system-stubs-testng/pom.xml new file mode 100644 index 0000000..2773153 --- /dev/null +++ b/system-stubs-testng/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + + uk.org.webcompere + system-stubs-parent + 2.1.2-SNAPSHOT + ../pom.xml + + + + system-stubs-testng + 2.1.2-SNAPSHOT + jar + + + 11 + 11 + + + + + uk.org.webcompere + system-stubs-core + + + + com.github.spotbugs + spotbugs-annotations + + + + org.testng + testng + provided + + + + org.assertj + assertj-core + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.jacoco + jacoco-maven-plugin + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + com.github.spotbugs + spotbugs-maven-plugin + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + + diff --git a/system-stubs-testng/src/main/java/uk/org/webcompere/systemstubs/testng/SystemStub.java b/system-stubs-testng/src/main/java/uk/org/webcompere/systemstubs/testng/SystemStub.java new file mode 100644 index 0000000..633abb0 --- /dev/null +++ b/system-stubs-testng/src/main/java/uk/org/webcompere/systemstubs/testng/SystemStub.java @@ -0,0 +1,16 @@ +package uk.org.webcompere.systemstubs.testng; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a field in a test class as a system stub - this causes the {@link SystemStubsListener} to activate + * it during tests. It also causes the field to become instantiated if left uninitialized + * @since 1.0.0 + */ +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface SystemStub { +} diff --git a/system-stubs-testng/src/main/java/uk/org/webcompere/systemstubs/testng/SystemStubsListener.java b/system-stubs-testng/src/main/java/uk/org/webcompere/systemstubs/testng/SystemStubsListener.java new file mode 100644 index 0000000..0352ef8 --- /dev/null +++ b/system-stubs-testng/src/main/java/uk/org/webcompere/systemstubs/testng/SystemStubsListener.java @@ -0,0 +1,119 @@ +package uk.org.webcompere.systemstubs.testng; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.testng.IInvokedMethod; +import org.testng.IInvokedMethodListener; +import org.testng.ITestResult; +import uk.org.webcompere.systemstubs.resource.TestResource; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import static java.util.stream.Collectors.toList; +import static uk.org.webcompere.systemstubs.resource.Resources.executeCleanup; + +/** + * Add this to a test class with: + * + *
+ * @@Listeners(SystemStubsListener.class)
+ * public class MyTestClass {
+ *
+ * }
+ * 
+ * This causes any of the system stubs objects, that inherit {@link TestResource} to + * become active during tests. It will also instantiate any objects not initialized in the + * initializer list. + */ +public class SystemStubsListener implements IInvokedMethodListener { + @Override + public void beforeInvocation(IInvokedMethod method, ITestResult testResult) { + List stubs = ensureAllStubsAreInstantiated(method); + if (method.isTestMethod()) { + try { + for (TestResource stub: stubs) { + stub.setup(); + } + } catch (Exception e) { + throw new AssertionError("Could not set up stubs", e); + } + } + } + + @Override + public void afterInvocation(IInvokedMethod method, ITestResult testResult) { + if (method.isTestMethod()) { + try { + executeCleanup(getAllStubs(method)); + } catch (Exception e) { + throw new AssertionError("Could not tidy up stubs", e); + } + } + } + + private static TestResource readSystemStubResource(Field field, Object testObject) { + if (!TestResource.class.isAssignableFrom(field.getType())) { + throw new IllegalArgumentException("Cannot use @SystemStub with non TestResource object in field " + + field.getName() + " this one's a " + + field.getType().getCanonicalName()); + } + try { + makeAccessible(field); + return (TestResource)field.get(testObject); + } catch (Exception e) { + throw new AssertionError("Cannot read field " + field.getName(), e); + } + } + + private static List getAllStubs(IInvokedMethod method) { + var testObject = method.getTestMethod().getInstance(); + var fields = testObject.getClass().getDeclaredFields(); + return Arrays.stream(fields) + .filter(field -> field.isAnnotationPresent(SystemStub.class)) + .map(field -> readSystemStubResource(field, testObject)) + .filter(Objects::nonNull) + .collect(toList()); + } + + private static T makeAccessible(T object) { + if (!object.isAccessible()) { + object.setAccessible(true); + } + return object; + } + + private static List ensureAllStubsAreInstantiated(IInvokedMethod method) { + var testObject = method.getTestMethod().getInstance(); + var fields = testObject.getClass().getDeclaredFields(); + return Arrays.stream(fields) + .filter(field -> field.isAnnotationPresent(SystemStub.class)) + .map(field -> instantiateIfNecessary(field, testObject)) + .collect(toList()); + } + + @SuppressFBWarnings(value = "REC_CATCH_EXCEPTION", + justification = "Generic catch block provided as lots can go wrong when using reflection") + private static TestResource instantiateIfNecessary(Field field, Object testObject) { + if (!TestResource.class.isAssignableFrom(field.getType())) { + throw new IllegalArgumentException("Cannot use @SystemStub with non TestResource object in field " + + field.getName() + " this one's a " + + field.getType().getCanonicalName()); + } + + try { + makeAccessible(field); + var currentObject = field.get(testObject); + if (currentObject == null) { + var newInstance = field.getType().getDeclaredConstructor().newInstance(); + field.set(testObject, newInstance); + return (TestResource) newInstance; + } + return (TestResource) currentObject; + } catch (Exception e) { + throw new AssertionError("Cannot access field " + field.getName(), e); + } + } +} diff --git a/system-stubs-testng/src/test/java/uk/org/webcompere/systemstubs/testng/SystemStubsPluginWhenStubIsBlankTest.java b/system-stubs-testng/src/test/java/uk/org/webcompere/systemstubs/testng/SystemStubsPluginWhenStubIsBlankTest.java new file mode 100644 index 0000000..6c9c34f --- /dev/null +++ b/system-stubs-testng/src/test/java/uk/org/webcompere/systemstubs/testng/SystemStubsPluginWhenStubIsBlankTest.java @@ -0,0 +1,33 @@ +package uk.org.webcompere.systemstubs.testng; + +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; + +import static org.assertj.core.api.Assertions.assertThat; + +@Listeners(SystemStubsListener.class) +public class SystemStubsPluginWhenStubIsBlankTest { + + @SystemStub + private EnvironmentVariables environmentVariables; + + @BeforeTest + public void beforeTest() { + // even though the stub looks to be null, it's instantiated by here + environmentVariables.set("setinbefore", "yes"); + } + + @Test + public void noEnvironmentVariable() { + assertThat(System.getenv("scooby")).isBlank(); + } + + @Test + public void hasEnvironmentVariable() { + environmentVariables.set("foo", "bar"); + + assertThat(System.getenv("foo")).isEqualTo("bar"); + } +} diff --git a/system-stubs-testng/src/test/java/uk/org/webcompere/systemstubs/testng/SystemStubsPluginWhenStubIsDefinedTest.java b/system-stubs-testng/src/test/java/uk/org/webcompere/systemstubs/testng/SystemStubsPluginWhenStubIsDefinedTest.java new file mode 100644 index 0000000..9ee5714 --- /dev/null +++ b/system-stubs-testng/src/test/java/uk/org/webcompere/systemstubs/testng/SystemStubsPluginWhenStubIsDefinedTest.java @@ -0,0 +1,39 @@ +package uk.org.webcompere.systemstubs.testng; + +import org.testng.IInvokedMethod; +import org.testng.annotations.*; +import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; + +import static org.assertj.core.api.Assertions.assertThat; + +@Listeners(SystemStubsListener.class) +public class SystemStubsPluginWhenStubIsDefinedTest { + + @SystemStub + private EnvironmentVariables environmentVariables = new EnvironmentVariables(); + + @BeforeTest + public void beforeTest() { + environmentVariables.set("setinbefore", "yes"); + + // shouldn't apply yet, as we're not inside a test + assertThat(System.getenv("setinbefore")).isNotEqualTo("yes"); + } + + @Test + public void noEnvironmentVariable() { + assertThat(System.getenv("scooby")).isBlank(); + } + + @Test + public void hasEnvironmentVariable() { + environmentVariables.set("foo", "bar"); + + assertThat(System.getenv("foo")).isEqualTo("bar"); + } + + @Test + public void environmentSetInBeforeWillApplyInTest() { + assertThat(System.getenv("setinbefore")).isEqualTo("yes"); + } +} diff --git a/system-stubs-testng/src/test/java/uk/org/webcompere/systemstubs/testng/examples/CaptureSystemOutTest.java b/system-stubs-testng/src/test/java/uk/org/webcompere/systemstubs/testng/examples/CaptureSystemOutTest.java new file mode 100644 index 0000000..474e341 --- /dev/null +++ b/system-stubs-testng/src/test/java/uk/org/webcompere/systemstubs/testng/examples/CaptureSystemOutTest.java @@ -0,0 +1,30 @@ +package uk.org.webcompere.systemstubs.testng.examples; + +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import uk.org.webcompere.systemstubs.stream.SystemOut; +import uk.org.webcompere.systemstubs.testng.SystemStub; +import uk.org.webcompere.systemstubs.testng.SystemStubsListener; + +import static org.assertj.core.api.Assertions.assertThat; + +@Listeners(SystemStubsListener.class) +public class CaptureSystemOutTest { + + @SystemStub + private SystemOut out; + + @BeforeTest + public void beforeTest() { + out.clear(); + } + + @Test + public void canReadThingsSentToSystemOut() { + // simulate the system under test writing to std out + System.out.println("Can I assert this?"); + + assertThat(out.getText()).startsWith("Can I assert this?"); + } +} diff --git a/system-stubs-testng/src/test/java/uk/org/webcompere/systemstubs/testng/examples/SystemStubsWithoutPluginTest.java b/system-stubs-testng/src/test/java/uk/org/webcompere/systemstubs/testng/examples/SystemStubsWithoutPluginTest.java new file mode 100644 index 0000000..6be3e36 --- /dev/null +++ b/system-stubs-testng/src/test/java/uk/org/webcompere/systemstubs/testng/examples/SystemStubsWithoutPluginTest.java @@ -0,0 +1,45 @@ +package uk.org.webcompere.systemstubs.testng.examples; + +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; +import uk.org.webcompere.systemstubs.testng.SystemStub; +import uk.org.webcompere.systemstubs.testng.SystemStubsListener; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SystemStubsWithoutPluginTest { + + private EnvironmentVariables environmentVariables = new EnvironmentVariables(); + + @BeforeTest + public void beforeTest() throws Exception { + environmentVariables.set("setinbefore", "yes"); + + environmentVariables.setup(); + } + + @AfterTest + public void afterTest() throws Exception { + environmentVariables.teardown(); + } + + @Test + public void noEnvironmentVariable() { + assertThat(System.getenv("scooby")).isBlank(); + } + + @Test + public void hasEnvironmentVariable() { + environmentVariables.set("foo", "bar"); + + assertThat(System.getenv("foo")).isEqualTo("bar"); + } + + @Test + public void environmentSetInBeforeWillApplyInTest() { + assertThat(System.getenv("setinbefore")).isEqualTo("yes"); + } +}