Setting Up a Local Jira Instance Using Testcontainers in Spring Boot

Truong Bui
9 min readMay 28, 2024

--

Part of the Testcontainers Article Series: If you haven’t read my other articles yet, please refer to the following links:
1. Setting Up a Local MariaDB Using Testcontainers in Spring Boot
2. Setting Up a Local Kafka Using Testcontainers in Spring Boot
3. Setting Up a Local Redis Cluster Using Testcontainers in Spring Boot

My team encountered a significant challenge with integration tests on our QA Jira. I proposed a solution: integrating Testcontainers into our Spring Boot project to run a Jira Software Docker container locally. This would allow us to conduct integration tests on the local Jira container rather than the QA Jira.

However, after some hands-on development, I discovered this approach is impractical for our team. There are still too many unknowns, and extensive configuration is required to make the Jira container replicate the QA Jira instance. 😆

Even though this solution is impractical for my team, I believe it’s still worth sharing, along with some demonstration code. Hopefully, it will inspire ideas for others facing similar challenges.

Now, let’s dive into the demonstration !!! 💪

Prerequisites

  • Java 17
  • Maven Wrapper
  • Spring Boot 3+
  • Swagger (for testing purposes)
  • Docker runtime in advance (Docker Install)

Defining Dependencies

Create the project as a Spring Boot project with the dependencies provided inside the POM file below. I have named it jira-testcontainers.

https://github.com/buingoctruong/springboot-testcontainers-jira/blob/master/pom.xml

Jira Properties

In order to connect to the Jira instance, we need to add corresponding properties to application.yaml file.

# Jira Configuration
jira:
config:
# URL connection to Jira
jira-url: http://localhost:8080
username: admin
password: password

Using the @ConfigurationProperties annotation, we can easily bind properties from the application.yaml file to member variables of a specific class. In this case, all properties in the application.yaml file with the prefix jira.config.jira-url will be automatically bound to the corresponding member variables of the JiraProperties class.

@Data
@Validated
@ConfigurationProperties(prefix = "jira.config")
public class JiraProperties {
private final String jiraUrl;
private final String username;
private final String password;
}

Configuring Jira Client

To interact with Jira’s issue-tracking system via its REST API, we need to create an authenticated client. the following code snippet demonstrates how to set up such a client using the Jira REST client library.

@Bean
public IssueRestClient issueRestClient(JiraProperties jiraProperties) {
JiraRestClientFactory jiraRestClientFactory = new AsynchronousJiraRestClientFactory();
return jiraRestClientFactory
.createWithBasicHttpAuthentication(URI.create(jiraProperties.getJiraUrl()),
jiraProperties.getUsername(), jiraProperties.getPassword())
.getIssueClient();
}

This method encapsulates the configuration and authentication logic, returning an IssueRestClient that can be used to perform various operations on Jira issues.

Controller

Going to this section indicates that we have completed the Jira configurations tasks. Let’s create a simple controller to test Jira on the local machine.

@Slf4j
@RestController
@RequestMapping("/jira")
@RequiredArgsConstructor
public class JiraController {
private final IssueRestClient issueRestClient;
@PostMapping(path = "/issue")
public BasicIssue createJiraIssue(@RequestBody IssueInputDTO issueRequest) {
IssueTypeDTO issueTypeDTO = issueRequest.getIssueType();

IssueType issueType = new IssueType(URI.create(issueTypeDTO.getUri()), issueTypeDTO.getId(),
issueTypeDTO.getName(), issueTypeDTO.isSubTask, issueTypeDTO.getDescription(),
URI.create(issueTypeDTO.getIcon()));

IssueInputBuilder issueInputBuilder = new IssueInputBuilder()
.setProjectKey(issueRequest.getProjectKey()).setSummary(issueRequest.getSummary())
.setIssueType(issueType).setReporterName(issueRequest.getReporterName())
.setDescription(issueRequest.getDescription())
.setFieldValue("labels", issueRequest.getFieldValues().get("labels"));

IssueInput issueInput = issueInputBuilder
.setComponentsNames(issueRequest.getComponentsNames()).build();

BasicIssue issue;
try {
issue = issueRestClient.createIssue(issueInput).claim();
} catch (RuntimeException e) {
log.error("Failed to create issue {}", issueInput, e);
log.info("Start creating issue without components");
issueInput = issueInputBuilder.setComponentsNames(ImmutableList.of()).build();
issue = issueRestClient.createIssue(issueInput).claim();
}
return issue;
}

@Data
public static class IssueInputDTO {
private final String projectKey;
private final String summary;
private final IssueTypeDTO issueType;
private final String reporterName;
private final String description;
private final Map<String, List<String>> fieldValues;
private final List<String> componentsNames;
}

@Data
public static class IssueTypeDTO {
private final String uri;
private final long id;
private final String name;
private final boolean isSubTask;
private final String description;
private final String icon;
}
}

Local Jira Instance Setup

Okay, let’s go with the most interesting section 😆

As you may have noticed, the value of jira.config.jira-url in the application.yaml file is merely a placeholder. There is no actual Jira instance that exists with the host/port pair connection localhost:8080.

During application startup, the following steps need to be taken:

  1. Utilize Testcontainers for constructing a container implementation for Jira. (Make sure to add necessary environment variables to set up username and password for Jira container in advance. This step is specifically for controller testing when creating Jira issues)
  2. Starts the container using docker, pulling an image if necessary.
  3. Retrieve the host/port pair connection of the recently started container.
  4. Update the value of jira.config.jira-url in the application.yaml file.

Now, the question arises: How can we execute code during application startup? The solution is to implement ApplicationContextInitializer interface, which accepts ApplicationContextInitializedEvent. This event is sent when ApplicationContext becomes available, but before any bean definitions are loaded.

@Configuration
public class LocalJiraInitializer
implements
ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(@NonNull ConfigurableApplicationContext context) {
JiraLocalSetup(context);
}

private void JiraLocalSetup(ConfigurableApplicationContext context) {
ConfigurableEnvironment environment = context.getEnvironment();
DockerImageName dockerImageName = DockerImageName.parse("atlassian/jira-software");
GenericContainer<?> jira = new GenericContainer<>(dockerImageName).withExposedPorts(8080)
.withCreateContainerCmdModifier(cmd -> cmd.withName("jira"))
.withCreateContainerCmdModifier(cmd -> cmd.getHostConfig()
.withBinds(new com.github.dockerjava.api.model.Bind("jiraVolume",
new com.github.dockerjava.api.model.Volume(
"/var/atlassian/application-data/jira"))))
.withEnv("JIRA_ADMIN_USERNAME", "admin").withEnv("JIRA_ADMIN_PASSWORD", "password")
.withEnv("HEAP_NEWSIZE", "128M").withEnv("MAX_HEAP_SIZE", "1024M");
jira.start();
Integer mappedPort = jira.getMappedPort(8080);
String address = jira.getHost();
setProperties(environment, "jira.config.jira-url", "http://" + address + ":" + mappedPort);
}

private void setProperties(ConfigurableEnvironment environment, String name, Object value) {
MutablePropertySources sources = environment.getPropertySources();
PropertySource<?> source = sources.get(name);
if (source == null) {
source = new MapPropertySource(name, new HashMap<>());
sources.addFirst(source);
}
((Map<String, Object>) source.getSource()).put(name, value);
}
}

I will assign the exploration of constructing container implementations using Testcontainers and the ApplicationContextInitializer interface as homework for you.

Testcontainers should only be utilized in the local environment. This configuration class is created to facilitate local application execution with a Jira instance. Therefore, it should be moved to the test folder.

Local Application Startup Class

To start up the Spring ApplicationContext, we need a Spring Boot application’s main class that contains a public static void main() method.

Inside the test folder, there exists a class named JiraTestContainersApplicationTests . I have renamed it to JiraAppRunner 😆 and made the following updates.

@SpringBootTest
@EnableConfigurationProperties
@ComponentScan(basePackages = "github.io.truongbn.jiratestcontainers")
@ConfigurationPropertiesScan(basePackages = "github.io.truongbn.jiratestcontainers")
public class JiraAppRunner {
public static void main(String[] args) {
new SpringApplicationBuilder(JiraAppRunner.class).initializers(new LocalJiraInitializer())
.run(args);
}
}

Launch the Application

To launch the application, run JiraAppRunner.main() method, it should run successfully on port 1010.

The initial run may take some time as Testcontainers needs to set up all the necessary components for the docker instance 😅. However, this setup process only occurs once. The positive side is that from now on, we can launch the application locally at any time without the need for manual configuration of jira-related tasks.

If you encounter this issue during your initial run, and you find yourself in a similar situation as I did, you can refer to this link for more details: https://github.com/testcontainers/testcontainers-java/discussions/6045

Is everything ready now to play with Testcontainers? Not yet. We still need to configure the Jira instance, which includes setting up the Jira license, creating accounts, establishing the project for tickets, and more.

Configuring Jira Instance

The app is up and running, meaning the Jira Software container is successfully launched locally. To find the exposed port, simply check Docker Desktop. For instance, my container is accessible via port 64403.

Access this link: http://localhost:64403. It will redirect us to the page shown below. (If it navigates elsewhere, select the option to set up the Jira instance manually. This process will take a few minutes to install the Jira database and then redirect to the page below).

Confirm the information and click “Next”. We’ll be redirected to the page below where we can set up the Jira license.

Even when running Jira locally, a license is required. Since we don’t have one yet, click the “Generate a Jira trial license” link to be redirected to the license generation page.

(Note: I used my personal email to register an Atlassian account before proceeding.)

Enter and confirm the information, then click “Generate License”. We’ll be redirected to the license creation page. (Be sure to back up the license for future use, just in case)

Then, simply click the “Next” button. It may take a couple of minutes to complete the setup.

After a brief wait, we’ll be directed to the admin account setup page.

Enter the full name and personal email address (the one associated with your Atlassian account). Remember, the username is “admin” and the password is “password” mirroring the values in the application.yaml file. Click “Next” to proceed.

Selecting “Later” at this stage isn’t critical. Just click “Finish” to continue.

Choose your preferred language, then proceed by clicking “Continue”.

Click “Choose an avatar,” select a preferred image, then proceed by clicking “Next”.

Here, we have three choices. I chose “Create a new project” to set up a brand-new Jira project from scratch.

Choose “Kanban Software Development” as my team utilizes Kanban 😆, then proceed by clicking “Next”.

Notice that we’ll have 5 types of issues in our Jira instance. Continue selecting “Select.”

Enter the project name and project key, then click “Submit”.

As you can see, here is the Kanban UI, indicating that the Jira configuration process is complete 🤩.

Good news! From now on, whenever we re-run the application, we’ll just need to update the new port in the URL. Simply log in with the previously set up username and password, and all the configurations will still be preserved 🥳.

Time to play with Testcontainers

Now, everything is ready! 😎

  • Try out POST:/jira/issue”, the new issue should be created and will displayed on the Kanban board

The request object ↓

{
"projectKey": "PANDA",
"summary": "Summary",
"issueType": {
"uri": "EXPLOSION_URL",
"id": 10004,
"name": "Bug",
"description": "explosion description",
"icon": "EXPLOSION_ICON",
"subTask": false
},
"reporterName": "admin",
"description": "Description",
"componentsNames": [
"Component"
],
"fieldValues": {
"lables": [
"label1",
"lable2"
]
},
"componentsNames": [
"string"
]
}

The response object ↓

{
"self": "http://localhost:65411/rest/api/latest/issue/10000",
"key": "PANDA-1",
"id": 10000
}

Reload the Kanban board, we should notice the new issue was created 🎉.

One useful tip when creating request objects relates to the issue type Find Issue Type ID

We have just completed a brief demonstration to observe the setup of a local Jira using Testcontainers. Additionally, Testcontainers can be utilized for writing integration tests. Isn’t amazing? 😃 hope it’s working as expected you guys!

The completed source code can be found in this GitHub repository: https://github.com/buingoctruong/springboot-testcontainers-jira

I would love to hear your thoughts!

Thank you for reading and goodbye!

--

--