Skip to content
Permalink
Browse files
feat(serenity-bdd): one-way integration with jira and other issue tra…
…ckers

Serenity BDD reports produced by Serenity/JS can now link to tickets in Jira and other issue
trackers

Closes #189
  • Loading branch information
jan-molak committed Mar 14, 2020
1 parent 8858a33 commit 318abbbec5f6a99be3c9b8d3aa960ae05de9f8f4
Showing 10 changed files with 194 additions and 23 deletions.
@@ -201,6 +201,7 @@ article.content {
span {
border-top: 1px solid #666;
padding-top: 0.5em;
color: #666;
}
}
}
@@ -31,6 +31,7 @@ handbook/integration/index.md:
- handbook/integration/serenityjs-and-jasmine.md
- handbook/integration/serenityjs-and-protractor.md
- handbook/integration/reporting.md
- handbook/integration/jira-and-other-issue-trackers.md

community/index.md:
- community/events-and-articles.md
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1,130 @@
---
title: Jira and other issue trackers
layout: handbook.hbs
---
# Jira and other issue trackers

Test reports and living documentation produced by [Serenity BDD](/modules/serenity-bdd) can link to tickets in your issue tracker, such as [Jira](https://www.atlassian.com/software/jira), [Trello](https://trello.com/), etc.

Linking from your Serenity BDD report to a ticket in an issue tracker does not affect the status of the ticket
and is meant to make it easier for you and your team to understand and document the context that led to the feature being implemented.

In this chapter, I'll show you how to create those links automatically in three easy steps.

## Step 1 - Create a Serenity/JS project

First, you'll need a Node.js project with Serenity/JS and Cucumber.

The easiest way to create such project is to use one of the [Serenity/JS template projects](https://github.com/serenity-js/), for example [`serenity-js-cucumber-protractor-template`](https://github.com/serenity-js/serenity-js-cucumber-protractor-template).

## Step 2 - Tag your scenarios and features

The next step is to tag your Cucumber scenarios and features with appropriate ticket IDs.

Let's say you have created a "My Serenity/JS Project" in Jira, Trello, or a similar issue tracker.
If your issue tracker of choice identifies tickets using unique ticket IDs (i.e. `MSP-1`, `MSP-2`, etc.), and allows you to access them at unique URLs (i.e. `https://jira.my-company.com/browse/MSP-1`), you can link to the issue tickets from your Serenity BDD reports.

To do that, tag either `Feature` or `Scenario` nodes of your Cucumber `.feature` files with `@issue:<issueId>` or `@issues:<issueIds>` tags to create the links automatically when the report is generated.

The below `.feature` file demonstrates usage of both `@issue` and `@issues` tags:

```gherkin
@issue:MSP-1
Feature: Basic arithmetic operations
The feature itself, as well as all the scenarios in this file will be linked
to the **MSP-1** ticket in your issue tracker.
Linking to a ticket **does not** affect the status of ticket.
Please note that you can use the feature's description section to capture the acceptance criteria
and use the [Markdown syntax](https://daringfireball.net/projects/markdown/syntax)
to link to any **external documents**, wikis, etc.
Background:
Given Dominique has requested a new calculation
@issue:MSP-2
Scenario: Addition
This scenario will be linked to both the **MSP-1** issue, inherited from the `Feature`,
and the **MSP-2** issue, because of the `@issue` tag used at the `Scenario` level
When Dominique enters 2
And she uses the + operator
And she enters 3
Then she should get a result of 5
@issue:MSP-2
@issue:MSP-3
Scenario: Subtraction
This scenario will be linked to **MSP-1**, as well as **MSP-2** and **MSP-3** issues.
When Dominique enters 4
And she uses the - operator
And she enters 3
Then she should get a result of 1
@issues:MSP-2,MSP-4
Scenario: Multiplication
Instead of using multiple `@issue` tags we use a single `@issues` tag to link the scenario to several tickets.
When Dominique enters 2
And she uses the * operator
And she enters 8
Then she should get a result of 16
```

## Step 3 - Tell Serenity BDD where your issue tracker is

To make Serenity BDD generate links to tickets in your issue tracker, you need to tell it where your issue tracker is.

Serenity BDD works with issue trackers that provide a Web UI and support [deep linking](https://en.wikipedia.org/wiki/Deep_linking).
You can configure the location of your issue tracker using a command line parameter passed to the `serenity-bdd run` command.

Since all the [Serenity/JS template projects](https://github.com/serenity-js) have a `test:report` script invoking `serenity-bdd run` [defined in their `package.json` file](https://github.com/serenity-js/serenity-js-cucumber-protractor-template/blob/518285a5578a9cb3600a44eb3d10f4413bde8428/package.json#L12), this is what you need to configure.

### Jira

If your Jira server can be accessed at `https://jira.my-company.com`, you can configure its location using the `--jiraUrl` parameter:

```json
"test:report": "serenity-bdd run --jiraUrl 'https://jira.my-company.com'",
```

### Other issue trackers

If you're using a different issue tracker, you can configure its location using the `--issueTrackerUrl` parameter, where the `{0}` token
will be replaced with the `issueId`:

```json
"test:report": "serenity-bdd run --issueTrackerUrl 'https://trello.com/c/MyBoardId/{0}'",
```

The above configuration will produce links such as `https://trello.com/c/MyBoardId/MSP-1`

## Summary

Tagging `Feature` and `Scenario` nodes of your Cucumber `.feature` files with `@issues` and `@issue` and providing either a `--jiraUrl` or an `--issueTrackerUrl` when invoking the `serenity-bdd run` command instructs the Serenity BDD reporter to augment the scenario reports with
links to your issue tracker:

<figure>
![Serenity BDD Scenario Report](/handbook/integration/images/jira-and-other-issue-trackers/scenario-report.png)
<figcaption><span>Scenario report</span></figcaption>
</figure>

However, this is just one of the benefits. Another one is that once Serenity understands what scenarios and features concern what tickets,
it can provide you with another way to slice your reports:

<figure>
![Serenity BDD Summary Report](/handbook/integration/images/jira-and-other-issue-trackers/summary-report.png)
<figcaption><span>Summary report with an additional section describing the "Issues"</span></figcaption>
</figure>

This is particularly useful if you need a way to see all the scenarios that cover a given ticket:

<figure>
![Serenity BDD Issue Report](/handbook/integration/images/jira-and-other-issue-trackers/issue-report.png)
<figcaption><span>Report showing test scenarios concerning a given ticket</span></figcaption>
</figure>
@@ -45,7 +45,7 @@ describe('SerenityBDDReporter', () => {
* @test {TestRunFinishes}
* @test {ExecutionSuccessful}
*/
it('is marked map automated (non-manual) by default', () => {
it('is marked as automated (non-manual) by default', () => {
given(reporter).isNotifiedOfFollowingEvents(
new SceneFinished(defaultCardScenario, new ExecutionSuccessful()),
new TestRunFinishes(),
@@ -65,7 +65,7 @@ describe('SerenityBDDReporter', () => {
* @test {ExecutionSuccessful}
* @test {ManualTag}
*/
it('can be optionally tagged map manual', () => {
it('can be optionally tagged as manual', () => {
given(reporter).isNotifiedOfFollowingEvents(
new SceneTagged(defaultCardScenario, new ManualTag()),
new SceneFinished(defaultCardScenario, new ExecutionSuccessful()),
@@ -77,6 +77,7 @@ describe('SerenityBDDReporter', () => {
expect(report.manual).to.equal(true);
expect(report.tags).to.deep.include.members([{
name: 'Manual',
displayName: 'Manual',
type: 'External Tests',
}]);
});
@@ -104,17 +105,24 @@ describe('SerenityBDDReporter', () => {
report = stageManager.notifyOf.firstCall.lastArg.artifact.map(_ => _);

expect(report.tags).to.deep.include.members([{
name: 'ABC-1234',
type: 'issue',
name: 'ABC-1234',
displayName: 'ABC-1234',
type: 'issue',
}, {
name: 'DEF-5678',
type: 'issue',
name: 'DEF-5678',
displayName: 'DEF-5678',
type: 'issue',
}]);

expect(report.issues).to.deep.equal([
'ABC-1234',
'DEF-5678',
]);

expect(report.additionalIssues).to.deep.equal([
'ABC-1234',
'DEF-5678',
]);
});
});

@@ -139,8 +147,9 @@ describe('SerenityBDDReporter', () => {
report = stageManager.notifyOf.firstCall.lastArg.artifact.map(_ => _);

expect(report.tags).to.deep.include.members([{
name: 'regression',
type: 'tag',
name: 'regression',
displayName: 'regression',
type: 'tag',
}]);
});
});
@@ -168,11 +177,13 @@ describe('SerenityBDDReporter', () => {
expect(report.tags).to.deep.include.members([{
name: 'Checkout',
type: 'feature',
displayName: 'Checkout',
}]);

expect(report.featureTag).to.deep.equal({
name: 'Checkout',
type: 'feature',
displayName: 'Checkout',
});
});

@@ -199,14 +210,17 @@ describe('SerenityBDDReporter', () => {
expect(report.tags).to.deep.include.members([{
name: 'E-Commerce',
type: 'capability',
displayName: 'E-Commerce',
}, {
name: 'E-Commerce/Checkout',
type: 'feature',
displayName: 'Checkout',
}]);

expect(report.featureTag).to.deep.equal({
name: 'Checkout',
name: 'E-Commerce/Checkout',
type: 'feature',
displayName: 'Checkout',
});
});

@@ -235,17 +249,21 @@ describe('SerenityBDDReporter', () => {
expect(report.tags).to.deep.include.members([{
name: 'Digital',
type: 'theme',
displayName: 'Digital',
}, {
name: 'Digital/E-Commerce',
type: 'capability',
displayName: 'E-Commerce',
}, {
name: 'Digital/E-Commerce/Checkout',
name: 'E-Commerce/Checkout',
type: 'feature',
displayName: 'Checkout',
}]);

expect(report.featureTag).to.deep.equal({
name: 'Checkout',
name: 'E-Commerce/Checkout',
type: 'feature',
displayName: 'Checkout',
});
});

@@ -275,6 +293,7 @@ describe('SerenityBDDReporter', () => {
browserName: 'chrome',
browserVersion: '80.0.3987.87',
name: 'chrome 80.0.3987.87',
displayName: 'chrome 80.0.3987.87',
type: 'browser',
}]);
});
@@ -301,6 +320,7 @@ describe('SerenityBDDReporter', () => {

expect(report.tags).to.deep.include.members([{
name: 'iphone',
displayName: 'iphone',
type: 'context',
}]);
});
@@ -329,11 +349,13 @@ describe('SerenityBDDReporter', () => {

expect(report.tags).to.deep.include.members([{
name: 'safari 13.0.5',
displayName: 'safari 13.0.5',
type: 'browser',
browserName: 'safari',
browserVersion: '13.0.5',
}, {
name: 'iphone',
displayName: 'iphone',
type: 'context',
}]);
});
@@ -15,7 +15,7 @@
* @public
*/
export const defaults = {
artifact: 'net.serenity-bdd:serenity-cli:jar:all:2.1.9',
artifact: 'net.serenity-bdd:serenity-cli:jar:all:2.1.10',
repository: 'https://jcenter.bintray.com/',
cacheDir: 'node_modules/@serenity-js/cache',
sourceDir: 'target/site/serenity',
@@ -26,6 +26,7 @@ export interface SerenityBDDReport extends JSONObject {
dataTable?: DataTable; // done [x] cucumber <- can I use this with mocha?
manual: boolean; // done [x]
issues?: string[]; // done [x]
additionalIssues?: string[]; // done [x]
testSource: string; // done [x]
result: string; // done [x]
testFailureCause?: ErrorDetails; // done [x]
@@ -74,6 +75,7 @@ export interface UserStory extends JSONObject {

export interface Tag extends JSONObject {
name: string;
displayName: string;
type: string;
}

@@ -87,30 +87,45 @@ export class SceneReport {

taggedWith(tag: Tag) {
return this.withMutated(report => {
function nameOfRecorded(typeOfTag: { Type: string }) {
const serenityBDDCompatible = (text: string) => text[0].toUpperCase() + text.slice(1);

function displayNameOfRecorded(typeOfTag: { Type: string }) {
const found = report.tags.find(t => t.type === typeOfTag.Type);

return found && serenityBDDCompatible(found.name);
return found && found.displayName;
}

function serenityBDDTagFrom(serenityJSTag: Tag): { name: string, displayName: string, type: string } {
return {
...serenityJSTag.toJSON(),
displayName: serenityJSTag.name.replace(/_+/, ' '),
};
}

const concatenated = (...names: string[]): string => names.filter(name => !! name).join('/');

const serialisedTag = tag.toJSON();
const serenityBDDTag = serenityBDDTagFrom(tag);

if (! report.tags) {
report.tags = [];
}

match<Tag, void>(tag)
.when(ManualTag, _ => report.manual = true)
.when(CapabilityTag, _ => serialisedTag.name = concatenated(nameOfRecorded(ThemeTag), tag.name))
.when(ThemeTag, _ => {
serenityBDDTag.displayName = tag.name;
})
.when(CapabilityTag, _ => {
serenityBDDTag.name = concatenated(displayNameOfRecorded(ThemeTag), tag.name);
serenityBDDTag.displayName = tag.name;
})
.when(FeatureTag, _ => {
serialisedTag.name = concatenated(nameOfRecorded(CapabilityTag), tag.name);
report.featureTag = tag.toJSON();
serenityBDDTag.name = concatenated(displayNameOfRecorded(CapabilityTag), tag.name);
serenityBDDTag.displayName = tag.name;
report.featureTag = serenityBDDTag;
})
.when(IssueTag, _ => {
report.issues = (report.issues || []).concat(tag.name);
report.additionalIssues = (report.additionalIssues || []).concat(tag.name);
})
.when(IssueTag, _ => (report.issues = report.issues || []).push(tag.name))
.when(BrowserTag, (browserTag: BrowserTag) => {
report.context = [report.context, browserTag.browserName].filter(part => !! part).join(',');
report.driver = browserTag.browserName;
@@ -124,8 +139,8 @@ export class SceneReport {
.when(ContextTag, _ => (report.context = tag.name))
.else(_ => void 0);

if (! report.tags.find(current => equal(current, serialisedTag))) {
report.tags.push(serialisedTag);
if (! report.tags.find(current => equal(current, serenityBDDTag))) {
report.tags.push(serenityBDDTag);
}
});
}

0 comments on commit 318abbb

Please sign in to comment.