ScriptRunner Connect External Coding Series #2: Testing Without Tears

Skip to page content

Mocking APIs Like a Pro

Remember when testing meant deploying to production and hoping for the best? Yeah, we've all been there. Today we're going to talk about something revolutionary: testing ScriptRunner Connect scripts locally without losing your sanity (or your weekend).


If you've been following along with ScriptRunner Connect External Coding Series #1: Your IDE, Your Rules - Setting Up Accessible Development, you've already set up your accessible development environment with ScriptRunner Connect's External Coding. Now it's time for the really fun part – creating tests that actually help you instead of making you question your life choices.

Why Local Testing is a Accessibility Superpower

Before we dive into the technical bits, let's talk about why local testing is especially powerful for developers who rely on accessibility tools:

Screen Reader Friendly Testing

  • Your screen reader can properly announce test results in your familiar IDE
  • No struggling with web-based console outputs
  • Jest's output formats work beautifully with screen readers
  • You can navigate test files using your IDE's accessibility features

Consistent Workflow

  • Test output appears in the same terminal you use for everything else
  • Your keyboard shortcuts work exactly as expected
  • Error messages are properly structured for accessibility tools
  • Code navigation between tests and implementation is seamless

Debugging Without Drama

  • Set breakpoints using your IDE's accessible debugging interface
  • Step through code using familiar keyboard commands
  • Inspect variables without relying on visual-only debugging tools

The Testing Architecture That Actually Makes Sense

ScriptRunner Connect's External Coding provides a testing setup that's both powerful and accessible. Here's what makes it special:

Runtime Mocks: Your Testing Best Friend

The node/runtimeMocks.ts file is like having a clone of ScriptRunner Connect's runtime environment living on your machine. It provides local implementations of all the cloud-specific functionality:

 

// Mocks for @sr-connect/convert package
global._convert = {
    base64ToBuffer: (base64) => Buffer.from(base64, 'base64'),
    base64ToText: (base64, encoding) => Buffer.from(base64, 'base64').toString(encoding ?? 'utf8'),
    // ... more conversion utilities
};

Think of it as a stand-in actor who knows all the lines perfectly – your tests run locally but behave exactly like they would in the cloud.

Pre-configured Jest: Testing Made Simple

No more wrestling with Jest configurations or TypeScript setup. Everything just works:

  • TypeScript compilation handled automatically
  • ES modules properly configured
  • Screen reader friendly output formatting
  • Parallel test execution (because waiting is boring)

Setting Up Your Test Files: Where Everything Goes

Before we start writing tests, let's understand exactly where to create our test files and what the project structure should look like. This is crucial for screen reader users who need to navigate the file structure efficiently.

Project Structure for Testing

Here's how your project should be organized after following Series #1: Your IDE, Your Rules - Setting Up Accessible Development and adding the test files we'll create in this post:

your-scriptrunner-connect-project/
├── node/                                         # Local development environment
│   ├── runtimeMocks.ts                           # Runtime environment mocks
│   ├── client/jira/cloud/                        # Mock API clients for testing    (create this directory)
│   │   └── JiraCloudClientMock.ts                # Mock Jira client                (we'll create this)
│   ├── tests/                                    # All test files go here
│   │   ├── cloud/atlassian/jira/                 # Mirror your scripts structure   (create this directory)
│   │   │   └── Projects.test.ts                  # Test for Projects.ts            (we'll create this)
│   │   └── OnJiraCloudProjectCreated.test.ts     # Test for main event handler     (we'll create this)
│   ├── apiRegistry.ts                            # API client registry             (we'll update this)
│   ├── jest.config.ts                            # Jest configuration
│   └── tsconfig.json                             # Node-specific TypeScript config
├── scripts/                                      # Your ScriptRunner scripts
│   ├── OnJiraCloudProjectCreated.ts              # Main event handler
│   ├── cloud/atlassian/jira/                     # API integration helpers
│   │   └── Projects.ts                           # Projects API integration
│   └── api/                                      # API connections
├── package.json                                  # Dependencies
└── tsconfig.json                                 # Main TypeScript configuration

Files We'll Create in This Post

We'll be creating these files to demonstrate the testing patterns:

  1. node/client/jira/cloud/JiraCloudClientMock.ts - Mock Jira client for testing without real API calls
  2. node/tests/cloud/atlassian/jira/Projects.test.ts - Tests for the Projects API integration
  3. node/tests/OnJiraCloudProjectCreated.test.ts - Tests for the main event handler

We'll also update: 4. node/apiRegistry.ts - Update to use the mock client for testing

Accessibility Note: This mirrored structure makes it easy for screen readers to navigate between implementation and test files. If you're testing scripts/cloud/atlassian/jira/Projects.ts, its test lives at node/tests/cloud/atlassian/jira/Projects.test.ts – logical and predictable!

Creating Your First Test: A Step-by-Step Journey

Now let's create the mock client and tests for the Projects API integration we built in Series #1: Your IDE, Your Rules - Setting Up Accessible Development. This example will show you the patterns that work beautifully with accessibility tools.

Step 1: Create the Mock Jira Client

First, we need a mock API client that doesn't make real HTTP requests but returns predictable test data. Create the file node/client/jira/cloud/JiraCloudClientMock.ts:

 
// node/client/jira/cloud/JiraCloudClientMock.ts
import {
    PlatformImplementation,
    Request,
    Response as CommonsResponse,
    Headers as CommonsHeaders,
    BaseApiCore,
} from '@managed-api/commons-core';
import { JiraCloudApiCore } from '@managed-api/jira-cloud-v3-core';
 
/**
 * Mock implementation of Jira Cloud API client for testing.
 *
 * This client returns predictable test data without making real HTTP requests,
 * perfect for testing your ScriptRunner Connect scripts locally.
 */
export class JiraCloudClientMock extends JiraCloudApiCore {
    constructor() {
        super();
        console.log('Mock Jira client initialized for testing');
    }
 
    protected getPlatformImplementation(): PlatformImplementation {
        return {
            buffer: {
                encode: (input) => Buffer.from(input, 'utf-8'),
                decode: (input) => Buffer.from(input).toString('utf-8'),
            },
            performHttpCall: (request) => this.performHttpCall(request),
        };
    }
 
    private async performHttpCall(request: Request): Promise<CommonsResponse> {
        // Log mock API calls for debugging (screen reader friendly)
        console.log(`Mock API Call: ${request.method} ${request.url}`);
 
        switch (request.url) {
            case '/rest/api/3/project/search':
                return super.buildResponse(
                    request.url,
                    200,
                    'OK',
                    new CommonsHeaders(),
                    JSON.stringify({
                        isLast: true,
                        maxResults: 50,
                        startAt: 0,
                        total: 1,
                        values: [
                            {
                                "avatarUrls": {
                                    "16x16": "https://your-domain.atlassian.net/secure/projectavatar?size=xsmall&pid=10000",
                                    "24x24": "https://your-domain.atlassian.net/secure/projectavatar?size=small&pid=10000",
                                    "32x32": "https://your-domain.atlassian.net/secure/projectavatar?size=medium&pid=10000",
                                    "48x48": "https://your-domain.atlassian.net/secure/projectavatar?size=large&pid=10000"
                                },
                                "id": "10000",
                                "insight": {
                                    "lastIssueUpdateTime": "2021-04-22T05:37:05.000+0000",
                                    "totalIssueCount": 100
                                },
                                "key": "TEST",
                                "name": "Test Project",
                                "projectCategory": {
                                    "description": "Test Project Category",
                                    "id": "10000",
                                    "name": "TESTING",
                                    "self": "https://your-domain.atlassian.net/rest/api/3/projectCategory/10000"
                                },
                                "self": "https://your-domain.atlassian.net/rest/api/3/project/TEST",
                                "simplified": false,
                                "style": "classic"
                            }
                        ],
                    }),
                );
            default:
                throw new Error(`No mock implementation found for: ${request.url}`);
        }
    }
}

Step 2: Update the API Registry for Testing

Now we need to update node/apiRegistry.ts to use our mock client when running tests. This allows your tests to use mock data while your real scripts can still use actual API calls:

 
import { config } from 'dotenv';
import { BaseApiCore } from '@managed-api/commons-core';
import { JiraCloudClientMock } from './client/jira/cloud/JiraCloudClientMock';
 
// Load environment variables from .env file
config();
 
export type ApiRegistry = Record<string, BaseApiCore>;
 
export const API_REGISTRY: ApiRegistry = {
    // Declare here locally implemented API connections
 
    // Client mocks HTTP requests
    'jira/cloud': new JiraCloudClientMock()
};
 

Step 3: Create the Projects API Test

Create the file node/tests/cloud/atlassian/jira/Projects.test.ts:

 
import '../../../../runtimeMocks';
import { jest } from '@jest/globals';
import { getProjects } from "../../../../../scripts/cloud/atlassian/jira/Projects";
 
// Array for intercepted console.log messages
let interceptedConsoleLogs: string[] = [];
 
// Function for getting mocked Context object
export function getContext(ev: EV): Context<EV> {
    return {
        availableMemory: 400,
        invocationId: 'INVOCATION_ID',
        triggerType: 'MANUAL',
        rootTriggerType: 'MANUAL',
        startTime: Date.now(),
        timeout: 900000,
        environment: {
            uid: 'ENVIRONMENT_UID',
            name: 'Default',
            vars: ev,
        },
    };
}
 
beforeAll(() => {
    // Mock all console.log methods to intercept messages and store them
    jest.spyOn(console, 'log').mockImplementation((...args) => interceptedConsoleLogs.push(args.join(' ')));
});
 
describe('Test projects', () => {
 
    test('Test successful list of projects', async () => {
        const projects = await getProjects();
 
        expect(projects.length).toBeGreaterThan(0)
    });
});
 
afterAll(() => {
    jest.clearAllMocks();
});


Testing Event Handlers: Where the Magic Happens

Now let's create a test for your main event handler. Create the file node/tests/OnJiraCloudProjectCreated.test.ts:

 
import '../runtimeMocks';
import { jest } from '@jest/globals';
import { ProjectCreatedEvent } from '@sr-connect/jira-cloud/events';
import OnJiraCloudProjectCreated from '../../scripts/OnJiraCloudProjectCreated'
import { getContext } from './cloud/atlassian/jira/Projects.test';
 
describe('OnJiraCloudProjectCreated', () => {
    let consoleLogs: string[] = [];
 
    beforeEach(() => {
        consoleLogs = [];
        jest.spyOn(console, 'log').mockImplementation((...args) =>
            consoleLogs.push(args.join(' '))
        );
    });
 
    afterEach(() => {
        jest.clearAllMocks();
    });
 
    test('Should handle project created event', async () => {
        // Mock event data
        const mockEvent: ProjectCreatedEvent = {
            webhookEvent: "project_created",
            project: {
                id: 10000,
                key: 'TEST',
                name: 'Test Project',
                self: ''
            },
            timestamp: Date.now()
        };
 
        // Mock context
        const context = getContext({});
 
        // Execute the event handler
        await OnJiraCloudProjectCreated(mockEvent, context);
 
        // Verify the script executed (console.log outputs the string plus event and context)
        expect(consoleLogs.length).toBeGreaterThan(0);
        expect(consoleLogs[0]).toContain('Project Created event triggered');
    });
});
 


Running Your Tests: The Accessibility-First Workflow

Now that you've created your test files, let's run them with your accessible development workflow:

  1. Run All Tests with Audio Feedback
    npm test
  2. Focus on Specific Tests
    npm test -- Projects.test.ts

    This runs only the tests you're working on, reducing cognitive load and keeping output manageable.

  3. Watch Mode for Continuous Feedback
    npm test -- --watch

    Jest will re-run tests automatically when you save files. Your screen reader will announce results immediately, creating a tight feedback loop.

  4. Detailed Output When Needed
    npm test -- --verbose

    Gets detailed output for debugging. Screen readers handle this verbose output well, unlike many web-based testing interfaces.

What We've Accomplished

Testing victory achieved! You've just transformed from someone who deploys and prays into someone who tests with confidence. Your screen reader can navigate test results like a boss, Jest is your new best friend, your mock clients are serving up predictable test data, and those runtime mocks are working harder than a coffee machine on Monday morning.

Let's be honest: you've basically become the testing wizard your team didn't know they needed. While others are still debugging in production (we don't judge... much), you're catching bugs locally with the elegance of a screen reader announcing "15 tests passed" in your favourite IDE. Plus, your mock clients ensure you never accidentally spam real Jira instances during testing.

What's brewing next:

About the Author and Development Team

IMG_7964 (1)Rafael Pinto Sperafico, Senior Atlassian Consultant and technical architect behind the ScriptRunner Connect External Coding Series, brings years of hands-on experience helping organisations transform their Atlassian automation development workflows. As part of Ambientia's expert Atlassian team, Rafael Pinto Sperafico specialises in accessibility-first development practices, enterprise-grade ScriptRunner Connect implementations, and cutting-edge AI-assisted development methodologies. This series emerged from real-world client work where teams needed professional, accessible development environments that didn't force them to abandon their familiar tools or compromise on functionality.

The comprehensive insights shared throughout this series reflect the collective expertise of Ambientia's Atlassian Consultants Team – experienced professionals passionate about making powerful development tools accessible to all developers. Our team combines technical expertise in ScriptRunner Connect with a deep understanding of accessibility requirements, helping development teams explore professional local development environments that integrate with cloud execution. We specialise in the methodologies covered in this series: WebStorm SFTP configuration, comprehensive testing frameworks, local API development, and AI-powered automation workflows that respect accessibility standards.

Ready to transform your ScriptRunner Connect development experience? Whether you're struggling with browser-based development limitations, need to implement accessibility-compliant automation workflows, or want to leverage AI assistance like Claude Code while maintaining professional development standards, Ambientia's Atlassian experts are here to help. We offer tailored consultancy for External Coding implementations, accessibility audits for existing automations, and comprehensive training on the workflows covered in this series. Contact Ambientia today to discuss how our proven External Coding methodologies can accelerate your team's productivity while ensuring every developer – regardless of their assistive technology needs – can contribute effectively to your Atlassian automation projects.

More related