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:
- Create Jira ticket with the test content
- Generate Jira client API
- Configure JBehave to read data from the Jira
- Create steps implementation for JBehave
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 openThat 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.
Hi Nickolay, very nice article! It's exactly what we are currently trying to implement in our automation team.
ReplyDeleteEverything 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?
Hi Oleg,
DeleteThat 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
Yes, it did help.
DeleteThanks a lot!
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)
ReplyDeleteBy 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.
DeleteI managed to process all Stories for a project by overriding storyPaths():
DeleteRemoteIssue[] issues = jira.getIssuesFromJqlSearch(loginToken, "project = " + projectId + " AND issuetype = Story", MAX_RESULTS);
for( RemoteIssue issue : issues ) {
storyPaths.add( issue.getKey() ); //Id() );
}
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