Testing

Manual testing vs. automated testing

The easiest way to start testing your GitHub application is probably to create a playground project on GitHub and to run the application locally through Smee.io, as explained in Register a GitHub App.

This will let you play around on your playground repository, manually triggering events (by opening issues, pull requests, etc.), then check in the application logs that these events were consumed by the application.

That’s enough to get started, but once you start using the application on real repositories, you will want to make sure that changes to your code won’t break existing features. Doing that manually on every change will get exhausting very quickly.

To automate tests, you will of course rely on JUnit as with any Quarkus application, but there is an additional challenge: the application needs a GitHub repository to work on, and needs to be fed GitHub events to react to.

Fortunately, the GitHub App extension provides tools to help you simulate events and mock a GitHub repository.

Add the testing dependency

Add the following dependency to your deployment module:

<dependency>
    <groupId>io.quarkiverse.githubapp</groupId>
    <artifactId>quarkus-github-app-testing</artifactId>
    <version>2.4.2</version>
    <scope>test</scope>
</dependency>

Add event payloads to the test resources

When you tested your application manually, GitHub sent events (HTTP requests) to your application. Those events can be inspected using the replay UI at http://localhost:8080/replay/.

In order to simulate events in automated tests, you need to copy the payload of those events to resource files.

Let’s continue with the example from Create a GitHub App: open an issue in your playground repository, go to the replay UI to copy the event payload, then put that payload in a file in your project: src/test/resources/issue-opened.json.

Create a test class

Add the following class to src/test/java, in a package of your choosing:

@QuarkusTest
@GitHubAppTest
class CreateCommentTest {
    @Test
    void testIssueOpened() throws IOException {
        GitHubAppTesting.when() (1)
                .payloadFromClasspath("/issue-opened.json") (2)
                .event(GHEvent.ISSUES) (3)
                .then().github(mocks -> { (4)
                    Mockito.verify(mocks.issue(750705278)) (5)
                            .comment("Hello from my GitHub App"); (6)
                });
    }
}
1 Use GitHubAppTesting.when() to start simulating an event.
2 Define the payload of the simulated event by pointing to a file in the classpath.
3 Define the type of the simulated event.
4 Use .then().github(mocks → …​) to perform assertions on GitHub objects involved in the event handling.
5 The given mocks object gives access to mocks of GitHub objects, indexed by their identifier. See the payload of your event for the identifiers of relevant objects (issue, repository, …​). You can use .issue(long), .pullRequest(long), or even .ghObject(Class<? extends GHObject>, long). See the GitHubMockContext interface for a detailed list of methods.
6 Assertions are performed as usual with Mockito.

Run your tests

Just run ./mvnw test from the commandline.

More advanced Mockito features

You can use most Mockito features on the GitHub object mocks; that includes defining their behavior before the event is simulated:

@QuarkusTest
@GitHubAppTest
class CreateCommentTest {
    @Test
    void testIssueOpened() throws IOException {
        GitHubAppTesting.given() (1)
                .github(mocks -> { (2)
                    Mockito.doThrow(new RuntimeException("Simulated exception")) (3)
                            .when(mocks.issue(750705278))
                            .comment(Mockito.any());
                })
                .when().payloadFromClasspath("/issue-opened.json")
                .event(GHEvent.ISSUES)
                .then().github(mocks -> { (4)
                    Mockito.verify(mocks.issue(750705278)) (5)
                            .createReaction(ReactionContent.CONFUSED);
                });
    }
}
1 Use given().github(…​) to configure mocks.
2 The given mocks object gives access to mocks of GitHub objects, indexed by their identifier, just like in .then().github(…​). This can be used to configure the behavior of objects referenced in the event payload, such as (here) the GHIssue.
3 Here we’re configuring the mock to throw an exception when the application tries to comment on the issue.
4 We can still use .then().github(mocks → …​) to perform assertions on GitHub objects involved in the event handling.
5 Here we’re verifying that the application caught the runtime exception and added a confused reaction to the GitHub issue.

You can also use the class GitHubAppMockito to simplify mocking for some common scenarios:

@QuarkusTest
@GitHubAppTest
class CreateCommentTest {
    @Test
    void testIssueEdited() throws IOException {
        var queryCommentsBuilder = GitHubAppMockito.mockBuilder(GHIssueCommentQueryBuilder.class); (1)
        GitHubAppTesting.given()
                .github(mocks -> {
                    Mockito.when(mocks.issue(750705278).queryComments())
                            .thenReturn(queryCommentsBuilder);
                    var previousCommentFromBotMock = mocks.ghObject(GHIssueComment.class, 2);
                    var commentsMock = GitHubAppMockito.mockPagedIterable(previousCommentFromBotMock); (2)
                    Mockito.when(queryCommentsBuilder.list()) (3)
                            .thenReturn(commentsMock);
                })
                .when().payloadFromClasspath("/issue-edited.json")
                .event(GHEvent.ISSUES)
                .then().github(mocks -> {
                    Mockito.verify(mocks.issue(750705278)).queryComments();
                    // The bot already commented , it should not comment again.
                    Mockito.verifyNoMoreInteractions(mocks.issue(750705278));
                });
    }
}
1 Use GitHubAppMockito.mockBuilder to easily mock builders from the GitHub API. It will mock the builder by setting its default answer to just return this;, which is convenient since most methods in builders do that.

Here we’re mocking the builder returned by GHIssue#queryComments().

2 Use GitHubAppMockito.mockPagedIterable to easily mock PagedIterable from the GitHub API, which is the return type from many query or listing methods.

Here we’re mocking the list of comments returned when querying issue comments, so that it includes exactly one (mocked) comment.

3 When mocking builders, don’t forget to define the behavior of the "build method" (list()/create()/…​), because the default answer set by GitHubAppMockito.mockBuilder (returning this) will not work for that method.

Mocking the configuration file

If your event handler uses @ConfigFile to extract a configuration file from the GitHub repository, this file can be defined explicitly as a string:

@QuarkusTest
@GitHubAppTest
class CreateCommentTest {
    @Test
    void testIssueOpened() throws IOException {
        GitHubAppTesting.given() (1)
                .github(mocks -> { (2)
                    mocks.configFile("my-bot.yml") (3)
                            .fromString("greeting.message: \"some custom message\"");
                })
                .when()
                .payloadFromClasspath("/issue-opened.json")
                .event(GHEvent.ISSUES)
                .then().github(mocks -> {
                    Mockito.verify(mocks.issue(750705278))
                            .comment("some custom message");
                });
    }
}
1 Use given().github(…​) to configure mocks.
2 The given mocks object gives access to mocks of GitHub objects…​ including the configuration file.
3 Here we’re setting the content of the configuration file to a given string. This string will be parsed and mapped to an object, then passed to the event handler as the @ConfigFile-annotated argument.

Alternatively, the file can be extracted from a resource in the classpath:

@QuarkusTest
@GitHubAppTest
class CreateCommentTest {
    @Test
    void testIssueOpened() throws IOException {
        GitHubAppTesting.given() (1)
                .github(mocks -> { (2)
                    mocks.configFile("my-bot.yml") (3)
                            .fromClasspath("/my-bot-some-custom-message.yml");
                })
                .when()
                .payloadFromClasspath("/issue-opened.json")
                .event(GHEvent.ISSUES)
                .then().github(mocks -> {
                    Mockito.verify(mocks.issue(750705278))
                            .comment("some custom message");
                });
    }
}
1 Use given().github(…​) to configure mocks.
2 The given mocks object gives access to mocks of GitHub objects…​ including the configuration file.
3 Here we’re setting the content of the configuration file to a file extracted from the classpath (test resources). This file will be parsed and mapped to an object, then passed to the event handler as the @ConfigFile-annotated argument.

Limitations

  • The testing tools can be used exclusively with Mockito.

  • The testing tools cannot be used to test an application running in native mode.