Search

Sunday 10 February 2013

Java: JBehave integration with JIRA

During BDD engines comparison I noticed that JBehave supports various data sources for the stories but not just text files. This is quite valuable feature as we can share the stories accross the teams. But what if we share that using existing infrastructure? I mean all the tests are typically stored under some tracking system where they can be easily queried from. Such tracking systems should be the place where the tests are written to and updated at. So, why shouldn't we integrate that with the engine so that tests are designed, stored and modified in the tracking system and the engine like JBehave picks them up from there? That would be the tight integration between tests and their automated implementation. In this post I'll show how to make such integration between JIRA and JBehave.

General steps

Generally I should do the following steps:

  1. Create Jira ticket with the test content
  2. Generate Jira client API
  3. Configure JBehave to read data from the Jira
  4. Create steps implementation for JBehave
So, let's do the above points step by step.

Create Jira ticket with the test content

For this purpose we'll open some existing Jira project and create some ticket which should be treated as test. For this example we're mostly interested in the Description field. For our example the ticket description will contain the following text:

Given I'm logged into the system 
And I'm on the home page 
When I click on the "About" link 
Then I should see the "About" screen is open
That would be our test scenario stored in Jira.

Generate Jira client API

The entire process of Jira SOAP client generation can be found at the Creating a JIRA SOAP Client article on the Atlassian developers site. All the settings needed for that are described there. Breafly speaking, we should enable RPC plugin for Jira and generate Java SOAP client for the following URL: <hostname>/rpc/soap/jirasoapservice-v2?wsdl. After that we'll have a set of classes interacting with Jira which will be used in our example.

Configure JBehave to read data from the Jira

Now it's time to customize JBehave tests configuration. Firstly, we should customize stories load operation. For this purpose we can use org.jbehave.core.io.StoryLoader interface. For our custom needs we need to create new class which implements this interface. Initially this class looks like:

package org.jira.test;

import org.jbehave.core.io.StoryLoader;

public class JiraStoryLoader implements StoryLoader {

 protected String jiraId = "";

 public JiraStoryLoader(String id) {
  jiraId = id;
 }

 public String loadStoryAsText(String issueId) {
  // TODO Add implementation
 }
}
So, all we have to pass to the loader is the Jira ticket number. We have to update this class with the code retrieving the description from the Jira. So, updates are:
package org.jira.test;

import localhost.rpc.soap.jirasoapservice_v2.JiraSoapServiceProxy;
import org.jbehave.core.io.StoryLoader;
import com.atlassian.jira.rpc.soap.beans.RemoteIssue;

public class JiraStoryLoader implements StoryLoader {

 protected String jiraId = "";

 public JiraStoryLoader(String id) {
  jiraId = id;
 }

 public String loadStoryAsText(String issueId) {
  try {
   JiraSoapServiceProxy jiraProxy = new JiraSoapServiceProxy();
   String loginToken = jiraProxy.login("<your user name>", "<your password>");
   RemoteIssue issue = jiraProxy.getIssue(loginToken, jiraId);
   return issue.getDescription();
  } catch (Throwable e) {
   ;
  }
  return null;
 }
}
The core code is highlighted with red. Here we login to the Jira and retrieve security token. Further this token is passed to any Jira API method as the first parameter. So, next step is to get the issue information by specified issue number. And finally we return the Description value which contains test scenario in our example.

Once we're done with loader we should include it into the test class configuration. Generally our test class looks like:

package org.jira.test;

import java.util.LinkedList;
import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.MostUsefulConfiguration;
import org.jbehave.core.junit.JUnitStory;
import org.jbehave.core.reporters.Format;
import org.jbehave.core.reporters.StoryReporterBuilder;
import org.jbehave.core.steps.InjectableStepsFactory;
import org.jbehave.core.steps.InstanceStepsFactory;

public class JiraTestOperationsTest extends JUnitStory {

 public LinkedList<Object> stepDefinitions = new LinkedList<Object>();

 @Override
 public Configuration configuration() {
  return new MostUsefulConfiguration()
    .useStoryLoader(new JiraStoryLoader("SC-10"))
    .useStoryReporterBuilder(
      new StoryReporterBuilder()
        .withRelativeDirectory("")
        .withDefaultFormats()
        .withFormats(Format.CONSOLE, Format.TXT,
          Format.XML, Format.HTML));
 }

 @Override
 public InjectableStepsFactory stepsFactory() {
  return new InstanceStepsFactory(configuration(), this.stepDefinitions);
 }

 public JiraTestOperationsTest() {
  this.stepDefinitions.add(new JiraTestOperationsSteps());
 }
}

First highlighted part shows the placeholder where we put the reference to our loader with the parameter corresponding to the issue number we want to get test information from. Second highlighted part shows the place where we include reference to the step bindings. That would be the last class we should implement.

Create steps implementation for JBehave

And eventually we should add the class containing steps implementation. For testing purposes we'll create just fake class with the code like:

package org.jira.test;

import org.jbehave.core.annotations.Given;
import org.jbehave.core.annotations.Then;
import org.jbehave.core.annotations.When;

public class JiraTestOperationsSteps {

 public JiraTestOperationsSteps() {
  super();
 }

 @Given("I'm logged into the system")
 public void givenImLoggedIntoTheSystem() {
  System.out.println("Output: we're logged into the system");
 }

 @Given("I'm on the home page")
 public void givenImOnTheHomePage() {
  System.out.println("Output: the home page is open");
 }

 @When("I click on the \"About\" link")
 public void whenIClickOnTheAboutLink() {
  System.out.println("Output: the click was done");
 }

 @Then("I should see the \"About\" screen is open")
 public void thenIShouldSeeTheAboutScreenIsOpen() {
  System.out.println("Output: the About screen is open");
 }
}
Here we're making just some sample output in order to spot that each specific step was affected.

Run test

Now we can run this test. It should produce the output like:

Running story org/jira/test/jira_test_operations_test.story

(org/jira/test/jira_test_operations_test.story)
Scenario: 
Output: we're logged into the system
Given I'm logged into the system
Output: the home page is open
And I'm on the home page
Output: the click was done
When I click on the "About" link
Output: the About screen is open
Then I should see the "About" screen is open



(AfterStories)

Generating reports view to 'D:\Work\SiriusDev\JIRA_TEST\target' using formats '[stats, console, txt, xml, html]' and view properties '{defaultFormats=stats, decorateNonHtml=true, viewDirectory=view, decorated=ftl/jbehave-report-decorated.ftl, reports=ftl/jbehave-reports-with-totals.ftl, maps=ftl/jbehave-maps.ftl, navigator=ftl/jbehave-navigator.ftl, views=ftl/jbehave-views.ftl, nonDecorated=ftl/jbehave-report-non-decorated.ftl}'
Reports view generated with 1 stories (of which 0 pending) containing 1 scenarios (of which 0 pending)
As it's seen on the console output all out output to the console was produced which means that our test was executed.

Conclusion

That was just one example of integrating JIRA and JBehave. This example can be extended so that we just have to define the issue number related to exactly current test and track our stories in JIRA. Thus we've unbound our tests from the local file storage and shared them accross the teams using tracking systems.

This example can be extended to different tracking systems as most of them provide some external interface to retrieve the issue data. We can involve more fields to produce scenarios with specific tags and meta information retrieved from JIRA. We can do a lot of things but that's all is a matter of dedicated posts.

7 comments:

  1. Hi Nickolay, very nice article! It's exactly what we are currently trying to implement in our automation team.
    Everything looks straightforward to me until these lines of code:

    public DirectoryOperationsTest() {
    this.stepDefinitions.add(new JiraTestOperationsSteps());
    }

    This is actually where I stuck with the implementation.
    I assume that this is a method (return type is missing - void?). The question is who invokes this it?

    ReplyDelete
    Replies
    1. Hi Oleg,

      That should be the constructor, so it's name should be the same as the test class (for this article I simply copy-pasted my existing example and forgot to changes the name). I've just made the corrections to the post. That should be something like:

      public JiraTestOperationsTest() {
      this.stepDefinitions.add(new JiraTestOperationsSteps());
      }

      Try this one

      Delete
    2. Yes, it did help.
      Thanks a lot!

      Delete
  2. Could you show how this could be extended to run all stories for a given JIRA project (and preferably where the labels match a list of include/excludes)

    ReplyDelete
    Replies
    1. By design we set a correspondence between JUnit test and Jira ticket (see the new JiraStoryLoader("SC-10") ) statement in the code samples. So, the thing you're asking about is fully related to the running all available JUnit tests (so you need to look at JUnit categories functionality for this purpose). In this example Jira ticket is just used as the text information storage. Nothing more.

      Delete
    2. I managed to process all Stories for a project by overriding storyPaths():

      RemoteIssue[] issues = jira.getIssuesFromJqlSearch(loginToken, "project = " + projectId + " AND issuetype = Story", MAX_RESULTS);
      for( RemoteIssue issue : issues ) {
      storyPaths.add( issue.getKey() ); //Id() );
      }

      Delete
    3. That can be a good option. Thus, the entire solution may become more compact and actually based on single class. The query filtering Jira tickets may be a part of configuration or external properties, so we're flexible for making either batch run or individual tests (just by specifying query returning only one ticket). Yeah, that's definitely good idea. Thank you for sharing that.

      Delete