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