Skip to content

Commit d3e897f

Browse files
committed
Add base shell test system
- NOTE: very much wip and unstable - This commit is a first step to provide boot style @ShellTest annotation - New modules spring-shell-test and spring-shell-test-autoconfigure - Focus is to autoconfigure context without shell runners so that we can create "sessions" and hook to configures jline terminal with custom in/out streams. - Skeleton fork from jediterm to provide basic terminal emulation to part of a control amd escape characters working. - ShellTestClient is a concept user can use to interact with a shell in a same way user would use a "real" shell. - Fixes #489
1 parent 2bfcf99 commit d3e897f

File tree

82 files changed

+9694
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+9694
-0
lines changed

settings.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ include 'spring-shell-samples'
4949
include 'spring-shell-standard'
5050
include 'spring-shell-standard-commands'
5151
include 'spring-shell-table'
52+
include 'spring-shell-test'
53+
include 'spring-shell-test-autoconfigure'
5254

5355
file("${rootDir}/spring-shell-starters").eachDirMatch(~/spring-shell-starter.*/) {
5456
include "spring-shell-starters:${it.name}"

spring-shell-docs/spring-shell-docs.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ description = 'Spring Shell Documentation'
77
dependencies {
88
management platform(project(":spring-shell-management"))
99
implementation project(':spring-shell-starters:spring-shell-starter')
10+
implementation project(':spring-shell-starters:spring-shell-starter-test')
1011
testImplementation 'org.springframework.boot:spring-boot-starter-test'
12+
testImplementation 'org.awaitility:awaitility'
1113
}
1214

1315
asciidoctorj {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[[using-shell-testing-basics]]
2+
==== Basics
3+
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
4+
5+
Spring Shell provides a number of utilities and annotations to help when testing your application.
6+
Test support is provided by two modules: `spring-shell-test` contains core items, and
7+
`spring-shell-test-autoconfigure` supports auto-configuration for tests.
8+
9+
To test _interactive_ commands.
10+
11+
====
12+
[source, java, indent=0]
13+
----
14+
include::{snippets}/TestingSnippets.java[tag=testing-shelltest-interactive]
15+
----
16+
====
17+
18+
To test _non-interactive_ commands.
19+
20+
====
21+
[source, java, indent=0]
22+
----
23+
include::{snippets}/TestingSnippets.java[tag=testing-shelltest-noninteractive]
24+
----
25+
====
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[[using-shell-testing]]
2+
== Testing
3+
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
4+
5+
Testing cli application is difficult due to various reasons:
6+
7+
- There are differences between OS's.
8+
- Within OS there may be different shell implementations in use.
9+
- What goes into a shell and comes out from a shell my be totally
10+
different what you see in shell itself due to control characters.
11+
- Shell may feel syncronous but most likely it is not meaning when
12+
someting is written into it, you can't assume next update in
13+
in it is not final.
14+
15+
NOTE: Testing support is currently under development and will be
16+
unstable for various parts.
17+
18+
include::using-shell-testing-basics.adoc[]

spring-shell-docs/src/main/asciidoc/using-shell.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ include::using-shell-components.adoc[]
1313
include::using-shell-customization.adoc[]
1414

1515
include::using-shell-execution.adoc[]
16+
17+
include::using-shell-testing.adoc[]
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.docs;
17+
18+
import java.util.concurrent.TimeUnit;
19+
20+
import org.junit.jupiter.api.Test;
21+
22+
import org.springframework.beans.factory.annotation.Autowired;
23+
import org.springframework.shell.test.ShellAssertions;
24+
import org.springframework.shell.test.ShellTestClient;
25+
import org.springframework.shell.test.ShellTestClient.InteractiveShellSession;
26+
import org.springframework.shell.test.ShellTestClient.NonInteractiveShellSession;
27+
import org.springframework.shell.test.autoconfigure.ShellTest;
28+
import org.springframework.test.annotation.DirtiesContext;
29+
import org.springframework.test.annotation.DirtiesContext.ClassMode;
30+
31+
import static org.awaitility.Awaitility.await;
32+
33+
class TestingSnippets {
34+
35+
// tag::testing-shelltest-interactive[]
36+
@ShellTest
37+
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
38+
class InteractiveTestSample {
39+
40+
@Autowired
41+
ShellTestClient client;
42+
43+
@Test
44+
void test() {
45+
InteractiveShellSession session = client
46+
.interactive()
47+
.run();
48+
49+
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
50+
ShellAssertions.assertThat(session.screen())
51+
.containsText("shell");
52+
});
53+
54+
session.write(session.writeSequence().text("help").carriageReturn().build());
55+
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
56+
ShellAssertions.assertThat(session.screen())
57+
.containsText("AVAILABLE COMMANDS");
58+
});
59+
}
60+
}
61+
// end::testing-shelltest-interactive[]
62+
63+
// tag::testing-shelltest-noninteractive[]
64+
@ShellTest
65+
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
66+
class NonInteractiveTestSample {
67+
68+
@Autowired
69+
ShellTestClient client;
70+
71+
@Test
72+
void test() {
73+
NonInteractiveShellSession session = client
74+
.nonInterative("help", "help")
75+
.run();
76+
77+
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
78+
ShellAssertions.assertThat(session.screen())
79+
.containsText("AVAILABLE COMMANDS");
80+
});
81+
}
82+
}
83+
// end::testing-shelltest-noninteractive[]
84+
}

spring-shell-samples/spring-shell-samples.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ description = 'Spring Shell Samples'
99
dependencies {
1010
management platform(project(":spring-shell-management"))
1111
implementation project(':spring-shell-starters:spring-shell-starter-jna')
12+
testImplementation project(':spring-shell-starters:spring-shell-starter-test')
1213
testImplementation 'org.springframework.boot:spring-boot-starter-test'
14+
testImplementation 'org.awaitility:awaitility'
1315
}
1416

1517
springBoot {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.samples;
17+
18+
import java.util.concurrent.TimeUnit;
19+
20+
import org.springframework.beans.factory.annotation.Autowired;
21+
import org.springframework.context.annotation.Import;
22+
import org.springframework.shell.samples.standard.ResolvedCommands;
23+
import org.springframework.shell.test.ShellAssertions;
24+
import org.springframework.shell.test.ShellTestClient;
25+
import org.springframework.shell.test.ShellTestClient.BaseShellSession;
26+
import org.springframework.shell.test.ShellTestClient.InteractiveShellSession;
27+
import org.springframework.shell.test.ShellTestClient.NonInteractiveShellSession;
28+
import org.springframework.shell.test.autoconfigure.ShellTest;
29+
import org.springframework.test.annotation.DirtiesContext;
30+
import org.springframework.test.annotation.DirtiesContext.ClassMode;
31+
32+
import static org.awaitility.Awaitility.await;
33+
34+
@ShellTest
35+
@Import(ResolvedCommands.ResolvedCommandsConfiguration.class)
36+
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
37+
public class AbstractSampleTests {
38+
39+
@Autowired
40+
protected ShellTestClient client;
41+
42+
protected void assertScreenContainsText(BaseShellSession<?> session, String text) {
43+
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
44+
ShellAssertions.assertThat(session.screen()).containsText(text);
45+
});
46+
}
47+
48+
protected BaseShellSession<?> createSession(String command, boolean interactive) {
49+
if (interactive) {
50+
InteractiveShellSession session = client.interactive().run();
51+
session.write(session.writeSequence().command(command).build());
52+
return session;
53+
}
54+
else {
55+
String[] commands = command.split(" ");
56+
NonInteractiveShellSession session = client.nonInterative(commands).run();
57+
return session;
58+
}
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.samples.e2e;
17+
18+
import org.junit.jupiter.params.ParameterizedTest;
19+
import org.junit.jupiter.params.provider.CsvSource;
20+
21+
import org.springframework.shell.samples.AbstractSampleTests;
22+
import org.springframework.shell.test.ShellTestClient.BaseShellSession;
23+
24+
class RequiredValueCommandsTests extends AbstractSampleTests {
25+
26+
@ParameterizedTest
27+
@CsvSource({
28+
"e2e anno required-value,false",
29+
"e2e reg required-value,false",
30+
"e2e anno required-value,true",
31+
"e2e reg required-value,true"
32+
})
33+
void shouldRequireOption(String command, boolean interactive) {
34+
BaseShellSession<?> session = createSession(command, interactive);
35+
assertScreenContainsText(session, "Missing mandatory option");
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.samples.standard;
17+
18+
import java.util.concurrent.TimeUnit;
19+
20+
import org.junit.jupiter.params.ParameterizedTest;
21+
import org.junit.jupiter.params.provider.CsvSource;
22+
23+
import org.springframework.shell.samples.AbstractSampleTests;
24+
import org.springframework.shell.test.ShellAssertions;
25+
import org.springframework.shell.test.ShellTestClient.BaseShellSession;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.awaitility.Awaitility.await;
29+
30+
public class ComponentCommandsTests extends AbstractSampleTests {
31+
32+
@ParameterizedTest
33+
@CsvSource({
34+
"component single,false",
35+
"component single,true"
36+
})
37+
void componentSingle(String command, boolean interactive) {
38+
BaseShellSession<?> session = createSession(command, interactive);
39+
40+
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
41+
assertThat(session.screen().lines()).anySatisfy(line -> {
42+
assertThat(line).containsPattern("[>❯] key1");
43+
});
44+
});
45+
46+
session.write(session.writeSequence().keyDown().build());
47+
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
48+
assertThat(session.screen().lines()).anySatisfy(line -> {
49+
assertThat(line).containsPattern("[>❯] key2");
50+
});
51+
});
52+
53+
session.write(session.writeSequence().cr().build());
54+
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
55+
ShellAssertions.assertThat(session.screen()).containsText("Got value value2");
56+
});
57+
}
58+
59+
@ParameterizedTest
60+
@CsvSource({
61+
"component multi,false",
62+
"component multi,true"
63+
})
64+
void componentMulti(String command, boolean interactive) {
65+
BaseShellSession<?> session = createSession(command, interactive);
66+
67+
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
68+
assertThat(session.screen().lines()).anySatisfy(line -> {
69+
assertThat(line).containsPattern("[>❯] (☐|\\[ \\]) key1");
70+
});
71+
});
72+
73+
session.write(session.writeSequence().space().build());
74+
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
75+
assertThat(session.screen().lines()).anySatisfy(line -> {
76+
assertThat(line).containsPattern("[>❯] (☒|\\[x\\]) key1");
77+
});
78+
});
79+
80+
session.write(session.writeSequence().cr().build());
81+
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
82+
ShellAssertions.assertThat(session.screen()).containsText("Got value value1,value2");
83+
});
84+
}
85+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
plugins {
2+
id 'org.springframework.shell.starter'
3+
}
4+
5+
description = 'Spring Shell Starter Test'
6+
7+
dependencies {
8+
management platform(project(":spring-shell-management"))
9+
api(project(':spring-shell-starters:spring-shell-starter'))
10+
api(project(":spring-shell-test"))
11+
api(project(":spring-shell-test-autoconfigure"))
12+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
plugins {
2+
id 'org.springframework.shell.module'
3+
}
4+
5+
description = 'Spring Shell Test Autoconfigure'
6+
7+
dependencies {
8+
management platform(project(":spring-shell-management"))
9+
implementation project(':spring-shell-core')
10+
implementation project(':spring-shell-standard')
11+
implementation project(':spring-shell-test')
12+
implementation project(':spring-shell-autoconfigure')
13+
implementation 'org.springframework:spring-test'
14+
implementation 'org.springframework.boot:spring-boot-autoconfigure'
15+
implementation 'org.springframework.boot:spring-boot-test-autoconfigure'
16+
implementation 'org.springframework.boot:spring-boot-starter-test'
17+
optional 'org.jline:jline'
18+
optional 'org.assertj:assertj-core'
19+
optional 'org.junit.jupiter:junit-jupiter-api'
20+
testImplementation 'org.awaitility:awaitility'
21+
}

0 commit comments

Comments
 (0)