Testing APIs
It's important users don't hit errors when running your APIs. By writing tests, you can confirm business logic is working as expected, APIs are playing nicely with integrations, and your users have a bug free experience.
With the Superblocks SDK and your favorite JavaScript or Python test framework, you can:
- Write end-to-end API tests to verify API outputs when your app is in various states
- Validate your API's execution at every step by checking step outputs
- Test compatibility between your API and other systems, like 3rd-party APIs
- Run tests locally as you develop to get immediate feedback on changes
- Run tests in your CI/CD pipeline to make sure changes are ready to deploy
Set up
To create tests, start by writing and running tests locally. First, navigate to your app's git repo used for Source Control or create a new local directory for tests.
mkdir superblocks
cd superblocks
mkdir tests
In these examples, we'll show how to write tests with JEST and PyTest. Use the following to configure your project:
- JavaScript
- Python
Add the following to the package.json
at the root of your project directory:
{
"name": "superblocks",
"scripts": {
"test": "jest --setupFiles=dotenv/config"
},
"dependencies": {
"@superblocksteam/agent-sdk": "*",
"dotenv": "^16.4.5"
},
"devDependencies": {
"jest": "^29.7.0"
}
}
From your terminal, run:
npm install
Add the following to your requirements.txt
file:
pytest==8.2.2
pytest-dotenv==0.5.2
superblocks-agent-sdk==0.1.1
From your terminal, run:
pip3 install -r requirements.txt
Add a PyTest config file, pytest.ini
, and copy/paste the follow configurations:
[pytest]
env_files=.env
To execute your APIs with the SDK, you'll need an execute token. Find your token in your Personal Settings. Copy it and add it to a .env
file at the root of your directory.
SUPERBLOCKS_EXECUTION_TOKEN=<YOUR_EXECUTION_TOKEN>
If you're adding tests to a git repo, make sure to include .env
in your .gitignore
file.
Writing your first test
- JavaScript
- Python
Create a test file
You'll end up with a project that looks like:
.
├── .env
├── .gitignore
├── package.json
└── tests
└── sample.test.js
As your test suite grows you'll likely want to group the tests for an App, App Page, or related Workflows together. Learn more about organizating test files.
Add a test case that calls your API
The basic unit of tests in Superblocks is an API. APIs can be Backend APIs, Workflows, or Scheduled Jobs. While you can create tests for multiple APIs, we'll start by testing the functionality in a single API.
The following test checks that a simple API with no inputs returns data.
const { Api, Client } = require('@superblocksteam/agent-sdk');
client = new Client({
config: {
token: process.env.SUPERBLOCKS_EXECUTION_TOKEN
}
}) ;
describe('My APIs Test Suite', () => {
// Initalize the API client for the API we will test
const api = new Api('<YOUR_API_ID>');
test('Test API returns', async () => {
let req = await api.run({}, client);
expect(req.getResult());
});
});
Your client config will need to be slightly different if your organization uses Superblocks On-Premise of Superblocks EU. See the docs on On-Premise Agent & EU setup
Finding API IDs
Find the ID of Backend APIs by clicking Copy ID in the options menu next to the API's name.
For Workflows and Scheduled Jobs, copy the ID from the URL in your browser.
Run your test
Finally, run npm test
and Jest will print the results of your test.
$ npm test
PASS tests/sample.test.js
My API Test Suite
✓ Test API returns (428 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.813 s
Ran all test suites.
Create a test file
You'll end up with a project that looks like:
.
├── .env
├── .gitignore
├── pytest.ini
├── requirements.txt
└── tests
└── test_sample.py
As your test suite grows you'll likely want to group the tests for an App, App Page, or related Workflows together. Learn more about organizating test files.
Add a test case that calls your API
The basic unit of tests in Superblocks is an API. APIs can be Backend APIs, Workflows, or Scheduled Jobs. While you can create tests for multiple APIs, we'll start by testing the functionality in a single API.
The following test checks that a simple API with no inputs returns data.
import os
from superblocks_agent_sdk.api import Api
from superblocks_agent_sdk.client import Client, Config
class TestAPI:
def setup_method(self):
self.client = Client(config=Config(
token=os.getenv("SUPERBLOCKS_EXECUTION_TOKEN"
)))
# Initalize the API client for the API we will test
self.api = Api("<YOUR_API_ID>")
def test_api_returns(self):
result = self.api.run(client=self.client)
assert(result.get_result())
Finding API IDs
Find the ID of Backend APIs by clicking Copy ID in the options menu next to the API's name.
For Workflows and Scheduled Jobs, copy the ID from the URL in your browser.
Your client config will need to be slightly different if your organization uses Superblocks On-Premise of Superblocks EU. See the docs on On-Premise Agent & EU setup
Run your test
Finally, run pytest
and PyTest will print the results of your test.
$ pytest
============================= test session starts =============================
platform darwin -- Python 3.10.13, pytest-8.2.2, pluggy-1.5.0
rootdir: /home/demo-testing/python
configfile: pytest.ini
plugins: dotenv-0.5.2
collected 1 item
tests/test_sample.py . [100%]
============================== 1 passed in 0.71s ==============================
Mocking inputs
Your API has inputs if it references parts of your app's state. For example, if there are bindings that reference Component Properties, Frontend Variables, the Global object, etc.
You can determine if your API requires input, by looking at the Inputs in your API's configuration.
To mock your API's inputs, follow the format shown in the Inputs section in your API call:
- JavaScript
- Python
# ...
describe('My APIs Test Suite', () => {
# ...
test('Test API returns', async () => {
let req = await api.run({
inputs: {
user_filter: {
selectedOptionValue: "70bb2118070943b9909f2e265813408b"
}
}
}, client);
expect(req.getResult());
});
});
# ...
class TestAPI:
# ...
def test_api_returns(self):
result = self.api.run(
inputs={
"user_filter": {
"selectedOptionValue": "70bb2118070943b9909f2e265813408b"
}
},
client=self.client
)
assert(result.get_result())
Testing a Workflow? Just add the Workflow's params
and body
to the inputs
of the API.
Mocking steps
If you don't want Superblocks to make calls to external services when testing, you can use mock data instead. In this situation, you must use mock data to mock all of the steps that involve interaction with third-party integrations.
For example, lets say we have an API that counts the number of Zendesk tickets in each status. We don't want to make requests to Zendesk since it may lead us to hitting rate limits. Instead, we'll mock our API's get_tickets
step to return example tickets.
- JavaScript
- Python
const { Api, Client, on } = require('@superblocksteam/agent-sdk');
# ...
describe('My APIs Test Suite', () => {
# ...
test('Test API with mock', async () => {
let mock_tickets = on({ stepName: "get_tickets" }).return([{
"results": [
{ "id": 1, "status": "new"},
{ "id": 2, "status": "new" },
{ "id": 3, "status": "solved" }
]
}]);
let req = await api.run({ mocks: [mock_tickets] }, client);
let results = req.getResult();
// We should see 2 new and 1 solved ticket
expect(results.new).toBe(2);
expect(results.solved).toBe(1);
});
});
import os
from superblocks_agent_sdk.api import Api
from superblocks_agent_sdk.client import Client, Config
from superblocks_agent_sdk.testing.step import Params, on
class TestAPI:
# ...
def test_api_with_mock(self):
mock_tickets = on(Params(step_name="get_tickets")).return_([{
"results": [
{ "id": 1, "status": "new"},
{ "id": 2, "status": "new" },
{ "id": 3, "status": "solved" }
]
}])
req = self.api.run(mocks=[mock_tickets], client=self.client)
results = req.get_result()
assert results.new == 2
assert results.solved == 1
Learn more about types of mocks you can create in Advanced testing topics.
Assert on step output
So far, we've written tests that check the output of the entire API. You can also use the SDK to test individual steps of your API.
Say we have an API that assigns Zendesk tickets to agents. Users can choose to "auto" assign the ticket. If this option is selected, the API checks the number of open tickets assigned to each agent and returns the agent with the lowest ticket count in the assign_to
step.
In our test we will:
- Mock the
get_ticket_cnts
step that returns the number of tickets each agent is assigned in Zendesk - Get the output of the
assign_to
step to test that it selected the user with the lowest ticket count
- JavaScript
- Python
const { Api, Client, on } = require('@superblocksteam/agent-sdk');
# ...
describe('Test Zendesk ticket assignment', () => {
# ...
test('should assign to agent with lowest ticket count', async () => {
let input = {
tickets: {
selectedRow: {
id: "ticket_id"
}
},
assignTo: "Auto"
};
let mockCnts = on({ stepName: "get_tickets_cnts" }).return([{
results: [
{ agentId: "1", displayName: "Alex Thompson", cnt: 23 },
{ agentId: "2", displayName: "Jamie Rivera", cnt: 13 }.
{ agentId: "3", displayName: "Morgan Patel", cnt: 19 },
{ agentId: "4", displayName: "Taylor Kim", cnt: 21 }
]
}]);
let req = await api.run({ inputs: inputs, mocks: [mockCnts] }, client);
// Only get the results of the assign_to step
let results = req.getBlockResult("assign_to");
// Should say to assign to Jamie Rivera since they have 13 tickets
expect(results.agentId).toBe("2");
});
});
import os
from superblocks_agent_sdk.api import Api
from superblocks_agent_sdk.client import Client, Config
from superblocks_agent_sdk.testing.step import Params, on
class TestTicketAssignmentAPI:
# ...
def test_assign_to_lowest_cnt_agent(self):
inputs = {
"tickets": {
"selectedRow": {
"id": "ticket_id"
}
},
"assignTo": "Auto"
}
mock_cnts = on(Params(step_name="get_tickets_cnts")).return_([{
"results": [
{ "agentId": "1", "displayName": "Alex Thompson", "cnt": 23 },
{ "agentId": "2", "displayName": "Jamie Rivera", "cnt": 13 }.
{ "agentId": "3", "displayName": "Morgan Patel", "cnt": 19 },
{ "agentId": "4", "displayName": "Taylor Kim", "cnt": 21 }
]
}])
req = self.api.run(inputs=inputs, mocks=[mock_cnts], client=self.client)
# Only get the results of the assign_to step
result = req.get_block_result("assign_to")
# Should say to assign to Jamie Rivera since they have 13 tickets
assert result.agentId == "2"
Advanced testing topics
On-Premise Agent & EU setup
If your organization uses the On-Premise Agent or Superblocks Cloud in the EU, you'll need to update your test environment set up so the SDK sends requests to the right agent to execute your code. To update your set up, add the following:
Update your .env
file by adding the following:
SUPERBLOCKS_EXECUTION_TOKEN=<YOUR_EXECUTION_TOKEN>
SUPERBLOCKS_DATA_DOMAIN=<YOUR_AGENT_HOST_URL | agent.eu.superblocks.com>
Choosing an Agent
Your organization may have agents deployed in multiple environments. If you do, you'll need to choose which agent the SDK sents requests to. To do this, select an agent that's tagged with the Profile you're trying to test against.
For example, if you're testing against the development
profile, choose an agent that has the profile:development
tag.
You can find agents and how they're tagged on the On-Premise Agents page.
- JavaScript
- Python
Update the client
configuration in your test file to include the following:
const { Api, Client } = require('@superblocksteam/agent-sdk');
client = new Client({
config: {
token: process.env.SUPERBLOCKS_EXECUTION_TOKEN,
endpoint: process.env.SUPERBLOCKS_DATA_DOMAIN
}
});
describe('My APIs Test Suite', () => {
# ...
});
Update the client
configuration in your test file to include the following:
import os
from superblocks_agent_sdk.api import Api
from superblocks_agent_sdk.client import Client, Config
class TestAPI:
def setup_method(self):
self.client = Client(
config=ClientConfig(
token=os.getenv("SUPERBLOCKS_EXECUTION_TOKEN"),
endpoint=os.getenv("SUPERBLOCKS_DATA_DOMAIN")
)
)
Testing code on a branch
By default, the SDK runs API code on the main
branch of your App. If you use Source Control, you can test API code that's on branches by updating the SDK client configuration. The following recipes show how to configure this so that the branch is based on the branch you have checked out using git.
- JavaScript
- Python
Update your package.json
as follows:
{
"scripts": {
"test": "BRANCH=$(git rev-parse --abbrev-ref HEAD) jest --setupFiles=dotenv/config"
},
"devDependencies": {
"jest": "^29.7.0"
},
"dependencies": {
"@superblocksteam/agent-sdk": "^0.0.23",
"dotenv": "^16.4.5"
}
}
When initializing your SDK Client, include:
const client = new Client({
config: {
token: process.env.SUPERBLOCKS_EXECUTION_TOKEN
},
defaults: {
branch: process.env.BRANCH || null,
},
});
Update your requirements.txt
as follows:
pygit2==1.15.1
pytest==8.2.2
pytest-dotenv==0.5.2
superblocks-agent-sdk==0.1.1
From your terminal, run:
pip3 install -r requirements.txt
When initializing your SDK Client, include:
import os
from pygit2 import Repository
from superblocks_agent_sdk.api import Api, Config as ApiConfig
from superblocks_agent_sdk.client import Client, Config as ClientConfig
class TestAPI:
def setup_method(self):
self.client = Client(
config=ClientConfig(
token=os.getenv("SUPERBLOCKS_EXECUTION_TOKEN")
),
defaults=ApiConfig(
branch_name=Repository('.').head.shorthand
)
)
Organizing test files
To easily run your tests in a CI/CD pipeline, we recommend keeping all of your app's tests in one tests
directory.
If you're writing tests in conjunction with Source Control, your repo will end up looking as follows:
.
├── .env
├── .gitignore
├── .superblocks
├── package.json
├── apps
│ └── my_app
│ ├── .superblocks
│ │ └── superblocks.json
│ ├── application.yaml
│ └── pages
│ └── home
│ ├── apis
│ │ └── api1.yaml
│ └── page.yaml
└── tests
└── my_app
└── api1.test.py
Advanced mocking
Mocks let you isolate specific parts of your code to test it without accessing external dependencies. This helps improve the performance of your tests, reduces flakiness related to connectivity issues, and helps you avoid unnecessarily polluting services with data each time your test runs.
What types of mocking are available
The SDK lets you mock step data output in various ways. You can mock step data based on the step's name, integration type, and based on step configurations.
Mock output of a step
When you mock based on the step name, Superblocks won't hit external data sources or send HTTP requests when that step of the API runs. Instead, it will return the mock data you've provided as the step's output.
To mock a step based on its name:
- JavaScript
- Python
import { on, Mock, Params } from "@superblocksteam/agent-sdk";
const mock = on({ stepName: 'MyStep' }).return([{ order: 123 }]);
from superblocks_agent_sdk.testing.step import Params, on
mock = on(params=Params(step_name='MyStep')).return_([{ order: 123 }]);
Mock output of an integration
If you'd like to create a mock that can be re-used across steps or even API tests, you can create mocks based on the integration type, for example Postgres.
- JavaScript
- Python
import { on, Mock, Params } from "@superblocksteam/agent-sdk";
const mock = on({ integrationType: 'postgres' }).return([{ order: 123 }]);
from superblocks_agent_sdk.testing.step import Params, on
mock = on(params=Params(integration_type: 'postgres')).return_([{ order: 123 }]);
Mock output based on step config
For the most flexible mocks, you can mock based on a step's configuration. For example, lets say we wanted to return mock data anytime we make requests to Zendesk's tickets API. You can achieve this using the following mock:
- JavaScript
- Python
import { on, Mock, Params } from "@superblocksteam/agent-sdk";
const mock = on({
integrationType: 'zendesk',
configuration: {
httpMethod: 'GET',
urlPath: '/api/v2/tickets'
}
}).return({
"results": [
{ "id": 1, "status": "new"},
{ "id": 2, "status": "new" },
{ "id": 3, "status": "solved" }
]
});
Mocks also support callback functions to dynamically return data based on the step's configuration. For example, the following mock returns on solved tickets when the step includes the query parameter query=status:new
import { on, Mock, Params } from "@superblocksteam/agent-sdk";
const mock = on({
integrationType: "zendesk",
configuration: {
httpMethod: "GET",
urlPath: "/api/v2/tickets"
}
}).return(({ configuration }) => {
let query = configuration.params.filter(item => item.key === "query");
if (query && query.value === "status:new") {
return {
"results": [
{ "id": 1, "status": "new"},
{ "id": 2, "status": "new" },
]
}
} else {
return {
"results": [
{ "id": 1, "status": "new"},
{ "id": 2, "status": "new" },
{ "id": 3, "status": "solved" }
]
}
});
from superblocks_agent_sdk.testing.step import Params, on
mock = on(params=Params(
integration_type='zendesk',
configuration={
httpMethod='GET',
urlPath='/api/v2/tickets'
}
)).return_({
"results": [
{ "id": 1, "status": "new"},
{ "id": 2, "status": "new" },
{ "id": 3, "status": "solved" }
]
});
Mocks also support callback functions to dynamically return data based on the step's configuration. For example, the following mock returns on solved tickets when the step includes the query parameter query=status:new
from superblocks_agent_sdk.testing.step import Params, on
def return_tickets(params):
query = [item for item in params.configuration["params"] if item["key"] == "query"]
if query and query["value"] == "status:new":
return {
"results": [
{ "id": 1, "status": "new"},
{ "id": 2, "status": "new" },
]
}
else:
return {
"results": [
{ "id": 1, "status": "new"},
{ "id": 2, "status": "new" },
{ "id": 3, "status": "solved" }
]
}
mock = on(params=Params(
integration_type='zendesk',
configuration={
httpMethod='GET',
urlPath='/api/v2/tickets'
}
)).return_(return_tickets);
Steps that can't be mocked
Certain block types in Superblocks don't support mocking, including:
- Variables
- Condition
- Loop
- Parallel
- Return
- Stream
- Steps in the stream block's trigger
- Send
- Try and Catch
- Throw
Testing code on a branch
By default, the SDK runs API code on the main
branch of your App. If you use Source Control, you can test API code that's on branches by updating the SDK client configuration. The following recipes show how to configure this so that the branch is based on the branch you have checked out using git.
- JavaScript
- Python
Update your package.json
as follows:
{
"scripts": {
"test": "BRANCH=$(git rev-parse --abbrev-ref HEAD) jest --setupFiles=dotenv/config"
},
"devDependencies": {
"jest": "^29.7.0"
},
"dependencies": {
"@superblocksteam/agent-sdk": "^0.0.23",
"dotenv": "^16.4.5"
}
}
When initializing your SDK Client, include:
const client = new Client({
config: {
token: process.env.SUPERBLOCKS_EXECUTION_TOKEN
},
defaults: {
branch: process.env.BRANCH || null,
},
});
Update your requirements.txt
as follows:
pygit2==1.15.1
pytest==8.2.2
pytest-dotenv==0.5.2
superblocks-agent-sdk==0.1.1
From your terminal, run:
pip3 install -r requirements.txt
When initializing your SDK Client, include:
import os
from pygit2 import Repository
from superblocks_agent_sdk.api import Api, Config as ApiConfig
from superblocks_agent_sdk.client import Client, Config as ClientConfig
class TestAPI:
def setup_method(self):
self.client = Client(
config=ClientConfig(
token=os.getenv("SUPERBLOCKS_EXECUTION_TOKEN")
),
defaults=ApiConfig(
branch_name=Repository('.').head.shorthand
)
)
Run tests in CI/CD pipeline
Automatically executing API tests during development ensures that each code change you make is production-ready. Since your tests are written in code, you can easily integrate your tests into your CI/CD pipeline. This allows your tests to run automatically with every code push to your repository.
To configure your CI/CD to run tests automatically, follow the instructions below based on your git provider.
- GitHub
- GitLab
- Open your GitHub repo. This should be the repo you use for Source Control for your app.
- Navigate to the repository settings
- Go to Security section, click Secrets and variables → Actions
- Add the following as a secret to your repository:
Name | SUPERBLOCKS_EXECUTION_TOKEN |
---|---|
Secret | Execution token for your organization. You can find this token at https://app.superblocks.com/personal-settings#apiKey |
- Save the secret and go back to your repository's main page
- Open the GitHub workflow file for your Source Control CI
- Add the
run-tests
job to the workflow that corresponds with your testing language
name: Sync changes to Superblocks
on: [push]
jobs:
superblocks-sync:
runs-on: ubuntu-latest
name: Sync to Superblocks
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Push
uses: superblocksteam/import-action@v1
id: push
with:
token: ${{ secrets.SUPERBLOCKS_TOKEN }}
domain: ${{ secrets.SUPERBLOCKS_DOMAIN }}
- name: Pull
uses: superblocksteam/export-action@v1
id: pull
with:
token: ${{ secrets.SUPERBLOCKS_TOKEN }}
domain: ${{ secrets.SUPERBLOCKS_DOMAIN }}
run-tests:
runs-on: ubuntu-latest
needs: superblocks-sync
steps:
- name: checkout
uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v4
# Run JavaScript tests
- name: run js tests
run: |
npm install
npm test
env:
SUPERBLOCKS_EXECUTIONS_TOKEN: ${{ secrets.SUPERBLOCSK_EXECUTION_TOKEN }}
SUPERBLOCKS_DOMAIN: ${{ secrets.SUPERBLOCKS_DOMAIN }}
SUPERBLOCKS_VIEW_MODE: preview
BRANCH: ${{ github.ref_name }}
# Run Python tests
- name: run python tests
run: |
pip3 install -r requirements.txt
pytest
env:
SUPERBLOCKS_EXECUTIONS_TOKEN: ${{ secrets.SUPERBLOCSK_EXECUTION_TOKEN }}
SUPERBLOCKS_DOMAIN: ${{ secrets.SUPERBLOCKS_DOMAIN }}
SUPERBLOCKS_VIEW_MODE: preview
BRANCH: ${{ github.ref_name }}
- Open your GitLab project. This should be the repo you use for Source Control for your app.
- Navigate to the repository settings
- Go to Settings → CI/CD and click on Variables
- Add the following variable:
Key | SUPERBLOCKS_EXECUTION_TOKEN |
---|---|
Value | Execution token for your organization. You can find this token at https://app.superblocks.com/personal-settings#apiKey |
Flags | Mask variable |
- Save the variables and go back to your project's main page
- Go to Build → Pipeline editor
- Add the relevant
run-tests
pipeline step based on your chosen test language
stages:
- sync
- test
pull:
stage: sync
image:
name: superblocksteam/export-action:v1
entrypoint: ['']
pull_policy: always
before_script:
# Checkout the branch, because the job runs in detached mode
- git checkout "$CI_COMMIT_REF_NAME"
# Add authentication for the remote, so we can push
- git remote set-url origin https://oauth2:$SUPERBLOCKS_GITLAB_TOKEN@$CI_SERVER_HOST/$CI_PROJECT_PATH.git
script: /entrypoint.sh
push:
stage: sync
image:
name: superblocksteam/import-action:v1
entrypoint: ['']
pull_policy: always
before_script:
# Checkout the branch, because the job runs in detached mode
- git checkout "$CI_COMMIT_REF_NAME"
script: /entrypoint.sh
# Run JavaScript tests
run-js-tests:
stage: test
image: node:latest
needs:
- pull
- push
variables:
SUPERBLOCKS_EXECUTIONS_TOKEN: $SUPERBLOCSK_EXECUTION_TOKEN
SUPERBLOCKS_DOMAIN: $SUPERBLOCKS_DOMAIN
SUPERBLOCKS_VIEW_MODE: preview
BRANCH: $CI_COMMIT_REF_NAME
before_script:
- npm install
script:
- npm run test
# Run Python tests
run-py-tests:
stage: test
image: python:latest
needs:
- push
- pull
variables:
SUPERBLOCKS_EXECUTIONS_TOKEN: $SUPERBLOCSK_EXECUTION_TOKEN
SUPERBLOCKS_DOMAIN: $SUPERBLOCKS_DOMAIN
SUPERBLOCKS_VIEW_MODE: preview
BRANCH: $CI_COMMIT_REF_NAME
script:
- pip3 install -r requirements.txt
- pytest