diff --git a/pom.xml b/pom.xml
index 9987f9a3..499fe747 100644
--- a/pom.xml
+++ b/pom.xml
@@ -245,6 +245,7 @@
spring-shell-samples
+ spring-shell-test-samples
diff --git a/spring-shell-test-samples/.gitignore b/spring-shell-test-samples/.gitignore
new file mode 100644
index 00000000..7672a71c
--- /dev/null
+++ b/spring-shell-test-samples/.gitignore
@@ -0,0 +1,140 @@
+# Created by .ignore support plugin (hsz.mobi)
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+*.iml
+
+## Directory-based project format:
+.idea
+.idea/*.xml
+
+# if you remove the above rule, at least ignore the following:
+
+# User-specific stuff:
+.idea/workspace.xml
+.idea/tasks.xml
+.idea/dictionaries
+.idea/vcs.xml
+.idea/jsLibraryMappings.xml
+
+# Sensitive or high-churn files:
+.idea/dataSources.ids
+.idea/dataSources.xml
+.idea/dataSources.local.xml
+.idea/sqlDataSources.xml
+.idea/dynamic.xml
+.idea/uiDesigner.xml
+
+# Gradle:
+.idea/gradle.xml
+.idea/libraries
+
+# Mongo Explorer plugin:
+.idea/mongoSettings.xml
+
+## File-based project format:
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+### OSX template
+*.DS_Store
+.Appleint
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+### EiffelStudio template
+# The compilation directory
+EIFGENs
+### Xcode template
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## Build generated
+build/
+DerivedData/
+
+## Various settings
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata/
+
+## Other
+*.moved-aside
+*.xccheckout
+*.xcscmblueprint
+### Java template
+*.class
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.war
+*.ear
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+## Eclipse
+.project
+.classpath
+*.launch
+
+## Directory-based project format:
+.settings/
+# if you remove the above rule, at least ignore the following:
+
+### Maven template
+target
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+
+
+spring-shell.log
\ No newline at end of file
diff --git a/spring-shell-test-samples/README.md b/spring-shell-test-samples/README.md
new file mode 100644
index 00000000..23a79e4a
--- /dev/null
+++ b/spring-shell-test-samples/README.md
@@ -0,0 +1,10 @@
+# Spring Shell 2 Tests
+
+Examples of [Spring Shell 2](https://docs.spring.io/spring-shell/docs/current/reference/htmlsingle/) unit, functional and integration tests.
+
+## Run
+
+Execute the following command from the parent directory to run the tests:
+```
+mvn clean test
+```
diff --git a/spring-shell-test-samples/pom.xml b/spring-shell-test-samples/pom.xml
new file mode 100644
index 00000000..126c47a4
--- /dev/null
+++ b/spring-shell-test-samples/pom.xml
@@ -0,0 +1,42 @@
+
+ 4.0.0
+
+ spring-shell-test-samples
+ Spring Shell Test Samples
+ jar
+
+
+ org.springframework.shell
+ spring-shell-parent
+ 3.0.0.BUILD-SNAPSHOT
+
+
+ Examples of unit, functional and integration tests using Spring Shell 2
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
+
+
+ org.springframework.shell
+ spring-shell-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
diff --git a/spring-shell-test-samples/src/main/java/com/example/CalculatorApplication.java b/spring-shell-test-samples/src/main/java/com/example/CalculatorApplication.java
new file mode 100644
index 00000000..bfdd984d
--- /dev/null
+++ b/spring-shell-test-samples/src/main/java/com/example/CalculatorApplication.java
@@ -0,0 +1,19 @@
+package com.example;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * Main entry point for the illustrative Spring Shell Calculator application.
+ *
+ * @author Sualeh Fatehi
+ */
+@SpringBootApplication
+public class CalculatorApplication {
+
+ public static void main(final String[] args) {
+ SpringApplication.run(CalculatorApplication.class, args);
+ }
+
+}
diff --git a/spring-shell-test-samples/src/main/java/com/example/CalculatorCommands.java b/spring-shell-test-samples/src/main/java/com/example/CalculatorCommands.java
new file mode 100644
index 00000000..4a655ed3
--- /dev/null
+++ b/spring-shell-test-samples/src/main/java/com/example/CalculatorCommands.java
@@ -0,0 +1,33 @@
+package com.example;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.shell.standard.ShellCommandGroup;
+import org.springframework.shell.standard.ShellComponent;
+import org.springframework.shell.standard.ShellMethod;
+
+/**
+ *
+ * Basic commands for the illustrative Spring Shell Calculator application.
+ *
+ * @author Sualeh Fatehi
+ */
+@ShellComponent
+@ShellCommandGroup("Calculator Commands")
+public class CalculatorCommands {
+
+ @Autowired
+ private CalculatorState state;
+
+ @ShellMethod(value = "Add two integers")
+ public int add(final int a, final int b) {
+ return a + b;
+ }
+
+ @ShellMethod(value = "Add an integer to the value in memory")
+ public int addToMemory(final int b) {
+ final int sum = state.getMemory() + b;
+ state.setMemory(sum);
+ return sum;
+ }
+
+}
diff --git a/spring-shell-test-samples/src/main/java/com/example/CalculatorState.java b/spring-shell-test-samples/src/main/java/com/example/CalculatorState.java
new file mode 100644
index 00000000..fe1a49a1
--- /dev/null
+++ b/spring-shell-test-samples/src/main/java/com/example/CalculatorState.java
@@ -0,0 +1,25 @@
+package com.example;
+
+/**
+ *
+ * Program state for the illustrative Spring Shell Calculator application.
+ *
+ * @author Sualeh Fatehi
+ */
+public class CalculatorState {
+
+ private int memory;
+
+ public void clear() {
+ memory = 0;
+ }
+
+ public int getMemory() {
+ return memory;
+ }
+
+ public void setMemory(final int memory) {
+ this.memory = memory;
+ }
+
+}
diff --git a/spring-shell-test-samples/src/test/java/com/example/test/BaseCalculatorTest.java b/spring-shell-test-samples/src/test/java/com/example/test/BaseCalculatorTest.java
new file mode 100644
index 00000000..ace8c039
--- /dev/null
+++ b/spring-shell-test-samples/src/test/java/com/example/test/BaseCalculatorTest.java
@@ -0,0 +1,25 @@
+package com.example.test;
+
+import static org.springframework.util.ReflectionUtils.invokeMethod;
+import javax.validation.constraints.NotNull;
+import org.springframework.shell.CommandRegistry;
+import org.springframework.shell.MethodTarget;
+
+/**
+ *
+ * Utility methods for tests.
+ *
+ * @author Sualeh Fatehi
+ */
+public class BaseCalculatorTest {
+
+ protected T invoke(final MethodTarget methodTarget, final Object... args) {
+ return (T) invokeMethod(methodTarget.getMethod(), methodTarget.getBean(), args);
+ }
+
+ protected MethodTarget lookupCommand(@NotNull final CommandRegistry registry,
+ @NotNull final String command) {
+ return registry.listCommands().get(command);
+ }
+
+}
diff --git a/spring-shell-test-samples/src/test/java/com/example/test/TestCalculatorStateConfig.java b/spring-shell-test-samples/src/test/java/com/example/test/TestCalculatorStateConfig.java
new file mode 100644
index 00000000..3ffc12e3
--- /dev/null
+++ b/spring-shell-test-samples/src/test/java/com/example/test/TestCalculatorStateConfig.java
@@ -0,0 +1,21 @@
+package com.example.test;
+
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
+import com.example.CalculatorState;
+
+/**
+ *
+ * Spring configuration with beans for setting up tests.
+ *
+ * @author Sualeh Fatehi
+ */
+@TestConfiguration
+public class TestCalculatorStateConfig {
+
+ @Bean("state")
+ public CalculatorState calculatorState() {
+ return new CalculatorState();
+ }
+
+}
diff --git a/spring-shell-test-samples/src/test/java/com/example/test/functional/CalculatorCommandsTest.java b/spring-shell-test-samples/src/test/java/com/example/test/functional/CalculatorCommandsTest.java
new file mode 100644
index 00000000..9ce94123
--- /dev/null
+++ b/spring-shell-test-samples/src/test/java/com/example/test/functional/CalculatorCommandsTest.java
@@ -0,0 +1,86 @@
+package com.example.test.functional;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.junit.Assert.assertThat;
+import static org.springframework.util.ReflectionUtils.findMethod;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.shell.ConfigurableCommandRegistry;
+import org.springframework.shell.MethodTarget;
+import org.springframework.shell.standard.StandardMethodTargetRegistrar;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import com.example.CalculatorCommands;
+import com.example.CalculatorState;
+import com.example.test.BaseCalculatorTest;
+import com.example.test.TestCalculatorStateConfig;
+
+/**
+ * Illustrative functional tests for the Spring Shell Calculator application. These
+ * functional tests use Spring Shell commands auto-wired by the Spring Test Runner outside
+ * of the shell, to test functionality of the commands.
+ *
+ * @author Sualeh Fatehi
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(classes = { TestCalculatorStateConfig.class, CalculatorCommands.class })
+public class CalculatorCommandsTest extends BaseCalculatorTest {
+
+ private static final Class COMMAND_CLASS_UNDER_TEST = CalculatorCommands.class;
+
+ private final ConfigurableCommandRegistry registry = new ConfigurableCommandRegistry();
+
+ @Autowired
+ private CalculatorState state;
+
+ @Autowired
+ private ApplicationContext context;
+
+ @Before
+ public void setup() {
+ final StandardMethodTargetRegistrar registrar = new StandardMethodTargetRegistrar();
+ registrar.setApplicationContext(context);
+ registrar.register(registry);
+
+ state.clear();
+ }
+
+ @Test
+ public void testAdd() {
+ final String command = "add";
+ final String commandMethod = "add";
+
+ final MethodTarget commandTarget = lookupCommand(registry, command);
+ assertThat(commandTarget, notNullValue());
+ assertThat(commandTarget.getGroup(), is("Calculator Commands"));
+ assertThat(commandTarget.getHelp(), is("Add two integers"));
+ assertThat(commandTarget.getMethod(),
+ is(findMethod(COMMAND_CLASS_UNDER_TEST, commandMethod, int.class, int.class)));
+ assertThat(commandTarget.getAvailability().isAvailable(), is(true));
+ assertThat(invoke(commandTarget, 1, 2), is(3));
+ assertThat(state.getMemory(), is(0));
+ }
+
+ @Test
+ public void testaddToMemory() {
+ final String command = "add-to-memory";
+ final String commandMethod = "addToMemory";
+
+ final MethodTarget commandTarget = lookupCommand(registry, command);
+ assertThat(commandTarget, notNullValue());
+ assertThat(commandTarget.getGroup(), is("Calculator Commands"));
+ assertThat(commandTarget.getHelp(), is("Add an integer to the value in memory"));
+ assertThat(commandTarget.getMethod(),
+ is(findMethod(COMMAND_CLASS_UNDER_TEST, commandMethod, int.class)));
+ assertThat(commandTarget.getAvailability().isAvailable(), is(true));
+
+ state.setMemory(1);
+ assertThat(invoke(commandTarget, 2), is(3));
+ assertThat(state.getMemory(), is(3));
+ }
+
+}
diff --git a/spring-shell-test-samples/src/test/java/com/example/test/integration/CalculatorCommandsIntegrationTest.java b/spring-shell-test-samples/src/test/java/com/example/test/integration/CalculatorCommandsIntegrationTest.java
new file mode 100644
index 00000000..6b08c9d8
--- /dev/null
+++ b/spring-shell-test-samples/src/test/java/com/example/test/integration/CalculatorCommandsIntegrationTest.java
@@ -0,0 +1,84 @@
+package com.example.test.integration;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.junit.Assert.assertThat;
+import static org.springframework.util.ReflectionUtils.findMethod;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.shell.MethodTarget;
+import org.springframework.shell.Shell;
+import org.springframework.shell.jline.InteractiveShellApplicationRunner;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import com.example.CalculatorCommands;
+import com.example.CalculatorState;
+import com.example.test.BaseCalculatorTest;
+import com.example.test.TestCalculatorStateConfig;
+
+/**
+ *
+ * Illustrative integration tests for the Spring Shell Calculator application. These
+ * integration tests use Spring Shell auto-wired by the Spring Test Runner.
+ *
+ * @author Sualeh Fatehi
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringBootTest(properties = { InteractiveShellApplicationRunner.SPRING_SHELL_INTERACTIVE_ENABLED + "=" + false })
+@ContextConfiguration(classes = TestCalculatorStateConfig.class)
+public class CalculatorCommandsIntegrationTest extends BaseCalculatorTest {
+
+ private static final Class COMMAND_CLASS_UNDER_TEST = CalculatorCommands.class;
+
+ @Autowired
+ private Shell shell;
+
+ @Autowired
+ private CalculatorState state;
+
+ /**
+ * Test "happy path" or basic addition with positive numbers and zeroes. Use Spring Shell
+ * auto-wired by the Spring Test Runner.
+ */
+ @Test
+ public void testAdd() {
+ final String command = "add";
+ final String commandMethod = "add";
+
+ final MethodTarget commandTarget = lookupCommand(shell, command);
+ assertThat(commandTarget, notNullValue());
+ assertThat(commandTarget.getGroup(), is("Calculator Commands"));
+ assertThat(commandTarget.getHelp(), is("Add two integers"));
+ assertThat(commandTarget.getMethod(),
+ is(findMethod(COMMAND_CLASS_UNDER_TEST, commandMethod, int.class, int.class)));
+ assertThat(commandTarget.getAvailability().isAvailable(), is(true));
+ assertThat(shell.evaluate(() -> command + " 1 2"), is(3));
+ assertThat(state.getMemory(), is(0));
+ }
+
+ /**
+ * Test "happy path" or basic addition with positive numbers and zeroes, using calculator
+ * memory (program state). Use Spring Shell and calculator memory auto-wired by the Spring
+ * Test Runner.
+ */
+ @Test
+ public void testAddToMemory() {
+ final String command = "add-to-memory";
+ final String commandMethod = "addToMemory";
+
+ final MethodTarget commandTarget = lookupCommand(shell, command);
+ assertThat(commandTarget, notNullValue());
+ assertThat(commandTarget.getGroup(), is("Calculator Commands"));
+ assertThat(commandTarget.getHelp(), is("Add an integer to the value in memory"));
+ assertThat(commandTarget.getMethod(),
+ is(findMethod(COMMAND_CLASS_UNDER_TEST, commandMethod, int.class)));
+ assertThat(commandTarget.getAvailability().isAvailable(), is(true));
+
+ state.setMemory(1);
+ assertThat(shell.evaluate(() -> command + " 2"), is(3));
+ assertThat(state.getMemory(), is(3));
+ }
+
+}
diff --git a/spring-shell-test-samples/src/test/java/com/example/test/unit/AddTest.java b/spring-shell-test-samples/src/test/java/com/example/test/unit/AddTest.java
new file mode 100644
index 00000000..f22e1a4d
--- /dev/null
+++ b/spring-shell-test-samples/src/test/java/com/example/test/unit/AddTest.java
@@ -0,0 +1,50 @@
+package com.example.test.unit;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import org.junit.Before;
+import org.junit.Test;
+import com.example.CalculatorCommands;
+
+/**
+ *
+ * Illustrative unit tests for the Spring Shell Calculator application. The unit tests
+ * test calculator commands as "plain-old Java objects" or POJOs, without relying on the
+ * Spring Framework. Boundary conditions of individual methods are tested.
+ *
+ * @author Sualeh Fatehi
+ */
+public class AddTest {
+
+ private CalculatorCommands commands;
+
+ /**
+ * Setup test calculator commands as "plain-old Java objects" or POJOs, initialized for
+ * each test.
+ */
+ @Before
+ public void setup() {
+ commands = new CalculatorCommands();
+ }
+
+ /**
+ * Test "happy path" or basic addition with positive numbers and zeroes.
+ */
+ @Test
+ public void addHappyPath() {
+ assertThat(commands.add(0, 1), is(1));
+ assertThat(commands.add(1, 2), is(3));
+ assertThat(commands.add(1, 0), is(1));
+ }
+
+ /**
+ * Test addition with negative numbers and zeroes.
+ */
+ @Test
+ public void addNegatives() {
+ assertThat(commands.add(0, -1), is(-1));
+ assertThat(commands.add(1, -2), is(-1));
+ assertThat(commands.add(-1, 0), is(-1));
+ }
+
+}
diff --git a/spring-shell-test-samples/src/test/java/com/example/test/unit/AddToMemoryTest.java b/spring-shell-test-samples/src/test/java/com/example/test/unit/AddToMemoryTest.java
new file mode 100644
index 00000000..0eb9a503
--- /dev/null
+++ b/spring-shell-test-samples/src/test/java/com/example/test/unit/AddToMemoryTest.java
@@ -0,0 +1,73 @@
+package com.example.test.unit;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.springframework.util.ReflectionUtils.findField;
+import static org.springframework.util.ReflectionUtils.makeAccessible;
+import static org.springframework.util.ReflectionUtils.setField;
+import java.lang.reflect.Field;
+import org.junit.Before;
+import org.junit.Test;
+import com.example.CalculatorCommands;
+import com.example.CalculatorState;
+
+/**
+ *
+ * Illustrative unit tests for the Spring Shell Calculator application. The unit tests
+ * test calculator commands as "plain-old Java objects" or POJOs, without relying on the
+ * Spring Framework. Boundary conditions of individual methods are tested.
+ *
+ * @author Sualeh Fatehi
+ */
+public class AddToMemoryTest {
+
+ private CalculatorCommands commands;
+
+ private CalculatorState state;
+
+ /**
+ * Setup test calculator commands as "plain-old Java objects" or POJOs, initialized for
+ * each test.
+ */
+ @Before
+ public void setup() {
+ state = new CalculatorState();
+ commands = new CalculatorCommands();
+ final Field stateField = findField(CalculatorCommands.class, "state");
+ makeAccessible(stateField);
+ setField(stateField, commands, state);
+ }
+
+ /**
+ * Test "happy path" or basic addition with positive numbers and zeroes, using calculator
+ * memory (program state).
+ */
+ @Test
+ public void addToMemoryHappyPath() {
+ assertThat(commands.addToMemory(1), is(1));
+ assertThat(state.getMemory(), is(1));
+
+ assertThat(commands.addToMemory(2), is(3));
+ assertThat(state.getMemory(), is(3));
+
+ assertThat(commands.addToMemory(0), is(3));
+ assertThat(state.getMemory(), is(3));
+ }
+
+ /**
+ * Test addition with negative numbers and zeroes, using calculator memory (program
+ * state).
+ */
+ @Test
+ public void addToMemoryNegatives() {
+ assertThat(commands.addToMemory(-1), is(-1));
+ assertThat(state.getMemory(), is(-1));
+
+ assertThat(commands.addToMemory(-2), is(-3));
+ assertThat(state.getMemory(), is(-3));
+
+ assertThat(commands.addToMemory(0), is(-3));
+ assertThat(state.getMemory(), is(-3));
+ }
+
+}