Skip to content

Commit cef91b4

Browse files
yrodieregsmet
authored andcommitted
Add more documentation for the testing framework
(cherry picked from commit ef147fe)
1 parent f59ab3b commit cef91b4

File tree

2 files changed

+145
-24
lines changed

2 files changed

+145
-24
lines changed

docs/modules/ROOT/pages/testing.adoc

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ See the `GitHubMockContext` interface for a detailed list of methods.
8686

8787
Just run `./mvnw test` from the commandline.
8888

89-
== Mockito features
89+
== More advanced Mockito features
9090

91-
You can use most Mockito features on the GitHub object mocks;
91+
You can use most https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html[Mockito features] on the GitHub object mocks;
9292
that includes defining their behavior before the event is simulated:
9393

9494
[source, java]
@@ -100,23 +100,70 @@ class CreateCommentTest {
100100
void testIssueOpened() throws IOException {
101101
GitHubAppTesting.given() // <1>
102102
.github(mocks -> { // <2>
103-
Mockito.doThrow(new RuntimeException()) // <3>
103+
Mockito.doThrow(new RuntimeException("Simulated exception")) // <3>
104104
.when(mocks.issue(750705278))
105105
.comment(Mockito.any());
106106
})
107107
.when().payloadFromClasspath("/issue-opened.json")
108108
.event(GHEvent.ISSUES)
109-
.then().github(mocks -> {
110-
Mockito.verify(mocks.issue(750705278))
111-
.comment("Hello from my GitHub App");
109+
.then().github(mocks -> { // <4>
110+
Mockito.verify(mocks.issue(750705278)) // <5>
111+
.createReaction(ReactionContent.CONFUSED);
112112
});
113113
}
114114
}
115115
----
116116
<1> Use given().github(...) to configure mocks.
117117
<2> The given `mocks` object gives access to mocks of GitHub objects, indexed by their identifier,
118118
just like in `.then().github(...)`.
119+
This can be used to configure the behavior of objects referenced in the event payload,
120+
such as (here) the `GHIssue`.
119121
<3> Here we're configuring the mock to throw an exception when the application tries to comment on the issue.
122+
<4> We can still use `.then().github(mocks -> ...)` to perform assertions on GitHub objects involved in the event handling.
123+
<5> Here we're verifying that the application caught the runtime exception and added a `confused` reaction to the GitHub issue.
124+
125+
You can also use the class `GitHubAppMockito` to simplify mocking for some common scenarios:
126+
127+
[source, java]
128+
----
129+
@QuarkusTest
130+
@GitHubAppTest
131+
class CreateCommentTest {
132+
@Test
133+
void testIssueEdited() throws IOException {
134+
var queryCommentsBuilder = GitHubAppMockito.mockBuilder(GHIssueCommentQueryBuilder.class); // <1>
135+
GitHubAppTesting.given()
136+
.github(mocks -> {
137+
Mockito.when(mocks.issue(750705278).queryComments())
138+
.thenReturn(queryCommentsBuilder);
139+
var previousCommentFromBotMock = mocks.ghObject(GHIssueComment.class, 2);
140+
var commentsMock = GitHubAppMockito.mockPagedIterable(previousCommentFromBotMock); // <2>
141+
Mockito.when(queryCommentsBuilder.list()) // <3>
142+
.thenReturn(commentsMock);
143+
})
144+
.when().payloadFromClasspath("/issue-edited.json")
145+
.event(GHEvent.ISSUES)
146+
.then().github(mocks -> {
147+
Mockito.verify(mocks.issue(750705278)).queryComments();
148+
// The bot already commented , it should not comment again.
149+
Mockito.verifyNoMoreInteractions(mocks.issue(750705278));
150+
});
151+
}
152+
}
153+
----
154+
<1> Use `GitHubAppMockito.mockBuilder` to easily mock builders from the GitHub API.
155+
It will mock the builder by setting its default answer to just `return this;`,
156+
which is convenient since most methods in builders do that.
157+
+
158+
Here we're mocking the builder returned by `GHIssue#queryComments()`.
159+
<2> Use `GitHubAppMockito.mockPagedIterable` to easily mock `PagedIterable` from the GitHub API,
160+
which is the return type from many query or listing methods.
161+
+
162+
Here we're mocking the list of comments returned when querying issue comments,
163+
so that it includes exactly one (mocked) comment.
164+
<3> When mocking builders, don't forget to define the behavior of the "build method" (`list()`/`create()`/...),
165+
because the default answer set by `GitHubAppMockito.mockBuilder` (returning `this`)
166+
will not work for that method.
120167

121168
== Mocking the configuration file
122169

@@ -132,10 +179,8 @@ class CreateCommentTest {
132179
void testIssueOpened() throws IOException {
133180
GitHubAppTesting.given() // <1>
134181
.github(mocks -> { // <2>
135-
mocks.configFileFromString( // <3>
136-
"my-bot.yml",
137-
"greeting.message: \"some custom message\""
138-
);
182+
mocks.configFile("my-bot.yml") // <3>
183+
.fromString("greeting.message: \"some custom message\"");
139184
})
140185
.when()
141186
.payloadFromClasspath("/issue-opened.json")
@@ -164,10 +209,8 @@ class CreateCommentTest {
164209
void testIssueOpened() throws IOException {
165210
GitHubAppTesting.given() // <1>
166211
.github(mocks -> { // <2>
167-
mocks.configFileFromClasspath( // <3>
168-
"my-bot.yml",
169-
"/my-bot-some-custom-message.yml"
170-
);
212+
mocks.configFile("my-bot.yml") // <3>
213+
.fromClasspath("/my-bot-some-custom-message.yml");
171214
})
172215
.when()
173216
.payloadFromClasspath("/issue-opened.json")

integration-tests/testing-framework/src/test/java/io/quarkiverse/githubapp/it/testingframework/TestingFrameworkTest.java

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.quarkiverse.githubapp.it.testingframework;
22

3+
import static io.quarkiverse.githubapp.testing.GitHubAppMockito.mockBuilder;
34
import static io.quarkiverse.githubapp.testing.GitHubAppMockito.mockPagedIterable;
45
import static io.quarkiverse.githubapp.testing.GitHubAppTesting.given;
56
import static io.quarkiverse.githubapp.testing.GitHubAppTesting.when;
@@ -21,6 +22,8 @@
2122
import org.kohsuke.github.GHApp;
2223
import org.kohsuke.github.GHAppInstallation;
2324
import org.kohsuke.github.GHEvent;
25+
import org.kohsuke.github.GHIssueComment;
26+
import org.kohsuke.github.GHIssueCommentQueryBuilder;
2427
import org.kohsuke.github.GHPullRequest;
2528
import org.kohsuke.github.GHRepository;
2629
import org.kohsuke.github.GitHub;
@@ -56,36 +59,111 @@ void ghObjectMocking() {
5659
Mockito.when(mocks.issue(750705278).getBody()).thenReturn("someValue");
5760
})
5861
.when().payloadFromClasspath("/issue-opened.json")
59-
.event(GHEvent.ISSUES)
60-
.then().github(mocks -> {
61-
}))
62+
.event(GHEvent.ISSUES))
6263
.doesNotThrowAnyException();
6364
assertThat(capture[0]).isEqualTo("someValue");
6465
}
6566

6667
@Test
6768
void ghObjectVerify() {
68-
ThrowingCallable assertion = () -> when().payloadFromClasspath("/issue-opened.json")
69+
// Do not change this, the documentation includes the exact same code
70+
ThrowingCallable assertion = () -> when()
71+
.payloadFromClasspath("/issue-opened.json")
6972
.event(GHEvent.ISSUES)
70-
.then().github(mocks -> {
71-
verify(mocks.issue(750705278))
72-
.addLabels("someValue");
73+
.then().github(mocks -> { // <4>
74+
Mockito.verify(mocks.issue(750705278))
75+
.comment("Hello from my GitHub App");
7376
});
7477

7578
// Success
7679
IssueEventListener.behavior = (payload, configFile) -> {
77-
payload.getIssue().addLabels("someValue");
80+
payload.getIssue().comment("Hello from my GitHub App");
7881
};
7982
assertThatCode(assertion).doesNotThrowAnyException();
8083

8184
// Failure
8285
IssueEventListener.behavior = (payload, configFile) -> {
83-
payload.getIssue().addLabels("otherValue");
86+
payload.getIssue().comment("otherValue");
8487
};
8588
assertThatThrownBy(assertion)
8689
.isInstanceOf(AssertionError.class)
8790
.hasMessageContaining("Actual invocations have different arguments:\n" +
88-
"GHIssue#750705278.addLabels(\"otherValue\");");
91+
"GHIssue#750705278.comment(\n \"otherValue\"\n);");
92+
}
93+
94+
@Test
95+
void ghObjectMockAndVerify() {
96+
// Do not change this, the documentation includes the exact same code
97+
ThrowingCallable assertion = () -> given()
98+
.github(mocks -> {
99+
Mockito.doThrow(new RuntimeException("Simulated exception"))
100+
.when(mocks.issue(750705278))
101+
.comment(Mockito.any());
102+
})
103+
.when().payloadFromClasspath("/issue-opened.json")
104+
.event(GHEvent.ISSUES)
105+
.then().github(mocks -> {
106+
Mockito.verify(mocks.issue(750705278))
107+
.createReaction(ReactionContent.CONFUSED);
108+
});
109+
110+
// Success
111+
IssueEventListener.behavior = (payload, configFile) -> {
112+
try {
113+
payload.getIssue().comment("Hello from my GitHub App");
114+
} catch (RuntimeException e) {
115+
payload.getIssue().createReaction(ReactionContent.CONFUSED);
116+
}
117+
};
118+
assertThatCode(assertion).doesNotThrowAnyException();
119+
120+
// Failure
121+
IssueEventListener.behavior = (payload, configFile) -> {
122+
payload.getIssue().comment("Hello from my GitHub App");
123+
};
124+
assertThatThrownBy(assertion)
125+
.isInstanceOf(AssertionError.class)
126+
.hasMessageContaining("The event handler threw an exception: Simulated exception");
127+
}
128+
129+
@Test
130+
void ghAppMockUtils() {
131+
// Do not change this, the documentation includes the exact same code
132+
var queryCommentsBuilder = mockBuilder(GHIssueCommentQueryBuilder.class);
133+
ThrowingCallable assertion = () -> given()
134+
.github(mocks -> {
135+
Mockito.when(mocks.issue(750705278).queryComments())
136+
.thenReturn(queryCommentsBuilder);
137+
var previousCommentFromBotMock = mocks.ghObject(GHIssueComment.class, 2);
138+
var commentsMock = mockPagedIterable(previousCommentFromBotMock);
139+
Mockito.when(queryCommentsBuilder.list())
140+
.thenReturn(commentsMock);
141+
})
142+
.when().payloadFromClasspath("/issue-opened.json")
143+
.event(GHEvent.ISSUES)
144+
.then().github(mocks -> {
145+
Mockito.verify(mocks.issue(750705278)).queryComments();
146+
// The bot already commented, it should not comment again.
147+
Mockito.verifyNoMoreInteractions(mocks.issue(750705278));
148+
});
149+
150+
// Success
151+
IssueEventListener.behavior = (payload, configFile) -> {
152+
if (!payload.getIssue().queryComments().list().iterator().hasNext()) {
153+
payload.getIssue().comment("Hello from my GitHub App");
154+
}
155+
};
156+
assertThatCode(assertion).doesNotThrowAnyException();
157+
158+
// Failure
159+
IssueEventListener.behavior = (payload, configFile) -> {
160+
boolean ignored = !payload.getIssue().queryComments().list().iterator().hasNext();
161+
// Comment regardless
162+
payload.getIssue().comment("Hello from my GitHub App");
163+
};
164+
assertThatThrownBy(assertion)
165+
.isInstanceOf(AssertionError.class)
166+
.hasMessageContaining("No interactions wanted here");
89167
}
90168

91169
@Test

0 commit comments

Comments
 (0)