Testing Atlassian Rest Plugins

With the latest versions of the Atlassian products, you can now add your own rest component plugins.

This is really cool, however, while testing rest plugins is actually pretty easy, figuring out exactly how to do it can drive a person crazy. And so I thought I’d write this post in hopes of saving others hours of research time.

The Project

For this example we’re just going to create a really simple rest resource that returns a list of the projects in JIRA.
I’m not going to walk through plugin development step-by-step but I’ll get us started…

Create a project

Let’s create our project using the Atlassian Plugin SDK.
Just open a terminal and navigate to your base workspace folder. Then run:

atlas-create-jira-plugin

When asked for the plugin name, call it jira-projects-rest

Add some Maven Dependencies

We’ll need a few libraries to help us test, just add the following to the pom dependency list

        <dependency>
            <groupId>javax.ws.rs</groupId>
            <artifactId>jsr311-api</artifactId>
            <version>1.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.atlassian.plugins.rest</groupId>
            <artifactId>atlassian-rest-common</artifactId>
            <version>1.0.2</version>
            <scope>provided</scope>
        </dependency>
 
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.3</version>
            <scope>provided</scope>
        </dependency>
 
        <dependency>
	    <groupId>com.sun.jersey</groupId>
	    <artifactId>jersey-test-framework</artifactId>
	    <version>1.1.2-ea</version>
	    <scope>test</scope>
	</dependency>

Add the rest descriptor

Once the project is created, edit the atlassian-plugin.xml and add our rest component:

<atlassian-plugin key="${project.groupId}.${project.artifactId}" name="${project.artifactId}" plugins-version="2">
    <plugin-info>
        <description>${project.description}</description>
        <version>${project.version}</version>
        <vendor name="${project.organization.name}" url="${project.organization.url}" />
        <application-version min="4.0"/>
        <bundle-instructions>
            <Import-Package>org.apache.commons.collections,*;resolution:=optional</Import-Package>
        </bundle-instructions>
    </plugin-info>
 
    <rest key="projectsService" path="/projectfinder" version="1.0">
        <description>Provides a service that lists JIRA projects</description>
    </rest>
 
    <component key="projectFinder"
        name="Project Finder"
        class="com.sysbliss.jira.plugins.util.ProjectFinder"/>
 
</atlassian-plugin>

The guts

So in the plugin descriptor you’ll notice we have a component named “projectFinder”.
This is a simple helper class that’s not really required, but it helps to decouple our service wrapper and would make sense in a more complex plugin.

The ProjectFinder is responsible for looking up and returning our project list:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.sysbliss.jira.plugins.util;
 
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
 
import com.atlassian.jira.project.Project;
import com.atlassian.jira.project.ProjectManager;
 
public class ProjectFinder {
 
    private final ProjectManager projectManager;
 
    public ProjectFinder(final ProjectManager projectManager) {
	this.projectManager = projectManager;
    }
 
    public List<Project> getProjects() {
	List<Project> projects = projectManager.getProjectObjects();
	if (projects == null) {
	    projects = Collections.<Project> emptyList();
	}
 
	return projects;
 
    }
}

The service wrapper

So now we need a service wrapper. This basically contains 2 classes:

  • ProjectsResource – The class that will be our jersey Resource
  • RestProject – a JAXB class that represents a single project

ProjectsResource.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 
package com.sysbliss.jira.plugins.rest;
 
import java.util.ArrayList;
import java.util.List;
 
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
 
import com.atlassian.jira.project.Project;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.sysbliss.jira.plugins.util.ProjectFinder;
 
@Path("")
@Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public class ProjectsResource {
 
    private final ProjectFinder projectFinder;
 
    public ProjectsResource(final ProjectFinder projectFinder) {
	this.projectFinder = projectFinder;
    }
 
    @GET
    @Path("projects")
    @AnonymousAllowed
    @Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
    public Response getProjects() {
 
	final List<Project> projectList = projectFinder.getProjects();
	final List<RestProject> restProjectList = new ArrayList<RestProject>();
	for (final Project project : projectList) {
	    restProjectList.add(new RestProject(project.getName()));
	}
 
	final GenericEntity<List<RestProject>> entities = new GenericEntity<List<RestProject>>(restProjectList) {};
 
	return Response.ok(entities).build();
    }
}

Let’s move on to the RestProject that represents a JIRA project.
For this example, I’m just going to include the name field, but you could add all the other stuff if needed.

RestProject.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 
package com.sysbliss.jira.plugins.rest;
 
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
 
import net.jcip.annotations.Immutable;
 
@Immutable
@XmlRootElement
public class RestProject {
 
    @XmlElement
    public final String name;
 
    public RestProject() {
	this.name = "";
    }
 
    public RestProject(final String name) {
	this.name = name;
    }
}

Unit Testing

Although I gave you the implementation code first, this was actually written test-first and I highly advise all others to do the same

At this point if all is well, we have a simple rest service in JIRA that returns a list containing RestProject objects.
The first thing we want to test is our ProjectFinder class. These will be unit tests.

Since we will need to mock out JIRA Project objects, it’s handy to write a little test utility for that.
Note: All the test classes use the Mockito framework for making mocks.

ProjectMockUtils.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 
package com.sysbliss.jira.plugins.test.util;
 
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
 
import java.util.Collection;
 
import com.atlassian.jira.project.Project;
 
 
public class ProjectMockUtils {
 
    public static Project createMockProject(final Long id, final String name, final String key) {
	final Project project = mock(Project.class);
 
	when(project.getId()).thenReturn(id);
	when(project.getName()).thenReturn(name);
	when(project.getKey()).thenReturn(key);
 
	return project;
    }
}

And now on to the unit tests. These are pretty simple and shouldn’t require too much explanation…
Note: The unit tests are using the TestNG framework.

ProjectFinderTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 
package com.sysbliss.jira.plugins;
 
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
 
import java.util.ArrayList;
import java.util.List;
 
import org.testng.annotations.Test;
 
import com.atlassian.jira.project.Project;
import com.atlassian.jira.project.ProjectManager;
import com.sysbliss.jira.plugins.test.util.ProjectMockUtils;
import com.sysbliss.jira.plugins.util.ProjectFinder;
 
public class ProjectFinderTest {
 
    @Test
    public void projectsAreFound() {
	final List<Project> projects = new ArrayList<Project>();
	projects.add(ProjectMockUtils.createMockProject(1L, "Project 1", "PRJ1"));
	projects.add(ProjectMockUtils.createMockProject(2L, "Project 2", "PRJ2"));
 
	final ProjectManager projectManager = mock(ProjectManager.class);
	when(projectManager.getProjectObjects()).thenReturn(projects);
 
	final ProjectFinder finder = new ProjectFinder(projectManager);
 
	final List<Project> found = finder.getProjects();
 
	assert found.size() == 2 : "Expected 2 projects but got " + found.size();
    }
 
}

So that’s that. I’m keeping it to that one method for this example. In real life, you’d have a bunch of boundry tests, etc, etc.

Next, and equally as exciting, almost the same exact test for the resource wrapper.

ProjectsResourceTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 
package com.sysbliss.jira.plugins;
 
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
 
import java.util.ArrayList;
import java.util.List;
 
import javax.ws.rs.core.Response;
 
import org.testng.annotations.Test;
 
import com.atlassian.jira.project.Project;
import com.atlassian.jira.project.ProjectManager;
import com.sysbliss.jira.plugins.rest.RestProject;
import com.sysbliss.jira.plugins.rest.ProjectsResource;
import com.sysbliss.jira.plugins.test.util.ProjectMockUtils;
import com.sysbliss.jira.plugins.util.ProjectFinder;
 
public class ProjectsResourceTest {
 
    @Test
    public void resourceProjectsAreFound() {
 
	final List<Project> projects = new ArrayList<Project>();
	projects.add(ProjectMockUtils.createMockProject(1L, "Project 1", "PRJ1"));
	projects.add(ProjectMockUtils.createMockProject(2L, "Project 2", "PRJ2"));
 
	final ProjectManager projectManager = mock(ProjectManager.class);
	when(projectManager.getProjectObjects()).thenReturn(projects);
 
	final ProjectFinder finder = new ProjectFinder(projectManager);
 
	final ProjectsResource resource = new ProjectsResource(finder);
 
	final Response response = resource.getProjects();
 
	final GenericEntity<List<RestProject>> entities = (GenericEntity<List<RestProject>>) response.getEntity();
 
	final List<RestProject> restProjects = entities.getEntity();
 
	assert (restProjects.size() == 2) : "Expected 2 projects but got " + restProjects.size();
    }
 
}

The above test is a little more interesting in the fact that it’s using our jersey resource to test that our resource classes are working properly and that we are getting the correct model objects from the Response class.

So that’s it for the unit tests. Pretty simple and doesn’t require any external connections to anything.
On to the interesting parts….

Integration Testing

The new Atlassian Plugins SDK provides some really cool stuff to help with integration testing and coupled with a few little “tricks” it’s pretty painless.

The Goal

The goal of our integration test harness is to deploy our plugin into a running JIRA instance, provide a known set of data, and actually make http requests to our new resource to verify that everything is working properly.

Before we can get started writing our test, we need to do a little setup.

Creating a dataset

We’re going to need a known set of data in our JIRA instance; namely, 2 empty projects.
The first step in creating the data is to boot up JIRA. Once again, the plugins SDK makes this easy….

Running JIRA

In a terminal, navigate to the project root and run:

atlas-run

If you’ve never run this previously, get some coffee…. maven’s going to do some web surfing for a while.
Once maven has completed, the SDK will boot up JIRA and deploy our plugin. At this point, we don’t really care about our rest resource…. we’re just going to make use of JIRA.

So now just open a browser and point it at: http://localhost:2990/jira

This will bring up JIRA and put you at the dashboard. Login using admin/admin as user/pass.

Create Projects

Finally, go to the JIRA administration section and create 2 projects. It doesn’t matter what they’re called, and they don’t need to have any issues or anything, just be sure there are 2 and only 2 projects created.

After the projects are created, use the backup to xml tool to export the data to an xml file.
The file should be saved as: [project-root]/src/test/xml/it-data.xml

That’s it. Now just CTRL-C in the terminal to shutdown JIRA.

Test Properties

We’re going to be making use of the JiraWebTest class (more on that in a bit) which requires us to make a properties file for integration testing.

Create a file: [project-root]/src/test/resources/localtest.properties
The contents of the file should be:

jira.protocol = http
jira.host = localhost
jira.port = 2990
jira.context = /jira
jira.edition = all
# Please note jira.xml.data.location needs to be the full path
jira.xml.data.location = src/test/xml/
 
jira.release.info = unknown
#
# If the browser path is set then when a test fails the func test framework will try and start
# the browser with a temporary file of captured web output.  if its not set then no harm
# and System.out will be used to dump the web response.
#
browser.path=firefox

Now our integration harness setup is complete and we can finally move on to the interesting stuff.

The Test Suite

Since we’re going to make use of the JiraWebTest, we are forced to use JUnit for the integration tests.
This requires us to create a JUnit Test Suite which is liste below.

Note: ALL integration tests must be in a test package that starts with it.[rest.of.package]

AllTests.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
package it.com.sysbliss.jira.plugins;
 
import junit.framework.Test;
import junit.framework.TestSuite;
 
public class AllTests {
 
    public static Test suite() {
	final TestSuite suite = new TestSuite("Integration Test for it.com.sysbliss.jira.plugins");
 
	suite.addTestSuite(ProjectsRestTest.class);
 
	return suite;
    }
 
}

What’s this JiraWebTest anyway?

JiraWebTest is a class provided by the jira-func-test package that should have been added to your pom.xml by the plugins SDK when you created the project.

It’s a class that your test classes can extend that provides extra functionality when running integration tests.
That extra functionality comes at a price though, like being forced to use JUnit… But wait, that’s not all! You can read an entire post about the pros and cons of the web test framework.

That being said, we are going to make as little use of this class as possible in hope something better comes along, however it does provide something very useful: The ability to load our exported test data.

And so, we’ll put up with it for now.

Finally, the integration test!

So here I’ll just proved the entire class and provide explanations after it.

ProjectsRestTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 
package it.com.sysbliss.jira.plugins;
 
import java.io.ByteArrayInputStream;
import java.io.InputStream;
 
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
 
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.GenericType;
import com.sun.jersey.api.client.WebResource;
 
import com.atlassian.jira.webtests.JIRAWebTest;
import com.sysbliss.jira.plugins.rest.RestProject;
import com.sysbliss.jira.plugins.rest.RestProjects;
 
public class ProjectsRestTest extends JIRAWebTest {
 
    public ProjectsRestTest(final String name) {
	super(name);
    }
 
    @Override
    public void setUp() {
	super.setUp();
	restoreData("it-data.xml");
    }
 
    @Override
    public void tearDown() {
	super.tearDown();
    }
 
    public void testRestProjectsFound() throws Exception {
	final Client client = Client.create();
	final WebResource resource = client.resource("http://localhost:2990/jira/rest/projectversions/1.0/projects.xml");
 
	final List<RestProject> projects = resource.get(new GenericType<List<RestProject>>() {});
	assertEquals(2, projects.size());
    }
}

Extend JiraWebTest

line 18:
we simply extend JiraWebTest to give us our data loading functionality

line 21:
make sure you call super() !

lines 24-28:

  • override the setUp method
  • call super.setUp()
  • call restoreData(“it-data.xml”); – this loads our test data before each test

lines 30-33:
Make sure you override tearDown and call super.tearDown();

lines 36-37:

  • create a jersey Client
  • create a WebResource with our resource url

line 39:
Execute our GET request and cast the response to our model using the GenericType provided by jax-rs

line 40:
We can now use our model objects to assert that our request returned the 2 project objects we have in our test data.

Running the tests

whew, we’re finally ready to actually run the tests… plugins SDK anyone?

Drop out to a terminal in the project root and run:

atlas-integration-test

Sit back and enjoy as you watch the following whiz (well, maybe half-walk/half-jog) by you:

  • Project is compiled and unit tests are run
  • JIRA is started and the plugin is automatically deployed
  • admin logs into JIRA and imports our test data
  • we are welcomed to the Hotel California
  • framework verifys JIRA is ready to run the integration tests
  • The integration tests are run
  • JIRA shuts down

The crowd rejoices

Although this article is a bit verbose, once you set this up, doing it again for other projects is a breeze and the feeling you get seeing the “build successful” at the end never get old.

Hope this was helpful…

Share and Enjoy:
  • Digg
  • Facebook
  • Google Bookmarks
  • DZone
  • LinkedIn
  • Slashdot
  • StumbleUpon

15 Comments

  1. Matt Doar says:

    This is a great resource, thanks. I’ve added a link to it from Sarah Maddox’s starter tutorial on Rest Plugins
    at http://confluence.atlassian.com/display/DEVNET/Plugin+Tutorial+-+Writing+REST+services

    ~Matt

  2. Sarah Maddox says:

    Great stuff! Thank you Jonathan, and thank you Matt for pointing out this page. I’ve added a link on the documentation page itself now too.
    Cheers
    Sarah

  3. Enzo says:

    Hi, may you help me resolve this problem? I got message after run atlas-integration-test

    [INFO] ———————————————–
    [ERROR] BUILD ERROR
    [INFO] ———————————————–
    [INFO] Unable to execute mojo

  4. Erik Husby says:

    There are some disconnects between the code and import statements.

    In ProjectRestTest, the code references Client, WebResource, and GenericType but there are no import statements for them. The imports list several classes from org.apache.commons.httpclient which are not mentioned in the code. Replacing Client with HttpClient from commons-httpclient 3.1 fails because there are not create or resource methods there.

    In several of the other sources, there was a GenericEnity that had no import statement. Intellij suggested javax.ws.rs.core.GenericEntity which appears to work.

    What is the solution to the import problems?

  5. jdoklovic says:

    Sorry… I started out using HttpClient, but then switched to using the jersey testing utilities.
    I added the dependency list in the beginning of this post.

    The javax.ws.rs.core.GenericEntity is the proper class as well.

  6. Erik Husby says:

    Thanks for the quick response. Problems resolved.

  7. Erik Husby says:

    Two more things.

    1. Had to change the URL used in ProjectRestTest.java to http://localhost:2990/jira/rest/projectfinder/1.0/projects.xml instead of http://localhost:2990/jira/rest/projectversions/1.0/projects.xml

    2. atlas-integration-test is running the test twice! Is that because I am using JUnit instead of TestNG?

  8. sdt says:

    This is a great tutorial, and the first good tutorial I’ve seen related to JIRA 4 plugin development! Kudos to you!

  9. Matt Doar says:

    Using FuncTestCase as the parent class and annotations also worked for me.

    ~Matt

    {code}
    package it.com.consultingtoolsmiths.jira.plugins.example;

    import com.consultingtoolsmiths.jira.plugins.example.BuildsResponse;
    import com.atlassian.jira.functest.framework.FuncTestCase;
    import org.junit.Before;
    import org.junit.Test;
    import com.sun.jersey.api.client.GenericType;
    import com.sun.jersey.api.client.Client;
    import com.sun.jersey.api.client.ClientResponse;
    import com.sun.jersey.api.client.WebResource;

    public class EnhancedLinksResourceTest extends FuncTestCase
    {
    Client client;

    @Before
    public void setUpTest()
    {
    // Restore the test data
    administration.restoreData(“example.xml”);
    client = Client.create();
    }

    @Test
    public void testRestProjectsFound() {
    String dut = “/rest/ct/1.0/builds/affected.json?issue=SW-1″;
    final WebResource resource = client.resource(“http://localhost:2990/jira” + dut);

    final BuildsResponse response = resource.get(new GenericType() {});
    assertEquals(4, response.num_builds());
    }

    }
    {code}

  10. Matt Doar says:

    Ack. The display ate an important bit of into. After “new GenericType” there is a <BuildResponse> missing.

  11. dat94ija says:

    Hello, I tried to follow up the structions to test this plugin but I am
    getting an error when I run atlas-run,

    Downloading:
    https://m2proxy.atlassian.com/repository/public/com/sun/grizzly/gri
    zzly-portunif/1.9.8/grizzly-portunif-1.9.8.jar
    16K downloaded (grizzly-portunif-1.9.8.jar)
    Downloading: https://m2proxy.atlassian.com/repository/public/com/sun/grizzly/gri
    zzly-http-servlet/1.9.8/grizzly-http-servlet-1.9.8.jar
    43K downloaded (grizzly-http-servlet-1.9.8.jar)
    18329K downloaded (glassfish-embedded-all-3.0-Prelude-Embedded-b14.jar)
    [INFO] [dependency:copy-dependencies]
    [INFO] [resources:resources]
    [WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources,
    i.e. build is platform dependent!
    [INFO] Copying 1 resource
    [INFO] [jira:filter-plugin-descriptor]
    [INFO] [resources:copy-resources {execution: virtual-execution}]
    [INFO] Using ‘UTF-8′ encoding to copy filtered resources.
    [INFO] Copying 1 resource
    [INFO] [compiler:compile]
    [INFO] Compiling 4 source files to C:developmentpluginsjira-projects-resttar
    getclasses
    [INFO] ————————————————————————
    [ERROR] BUILD FAILURE
    [INFO] ————————————————————————
    [INFO] Compilation failure

  12. jdoklovic says:

    can you post the rest of the output? What’s it failing to compile?

  13. dat94ija says:

    The compiling error was beacuse of error in my Java classes. I fixed it.

  14. Rambanam says:

    is it work on 4.3 or above?

  15. André Pitombeira says:

    Thanks! Very Good.

Leave a Reply