Skip to main content

Testing Strategies

Overview

Comprehensive testing strategies for Q01 Core API integrations, covering unit tests, integration tests, mocking strategies, test data management, and CI/CD integration.

Testing Levels:

  1. Unit Tests - Individual functions
  2. Integration Tests - API interactions
  3. Contract Tests - API contracts
  4. E2E Tests - Complete workflows
  5. Performance Tests - Load and stress
  6. Security Tests - Vulnerability scanning

Unit Testing

Testing API Client Methods

// api-client.js
class CoreAPIClient {
constructor(baseURL, token) {
this.baseURL = baseURL;
this.token = token;
}

async get(dimension, id) {
const response = await fetch(`${this.baseURL}/api/v4/core/${dimension}/${id}`, {
headers: {'Authorization': `Bearer ${this.token}`}
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

return response.json();
}

async create(dimension, data) {
const response = await fetch(`${this.baseURL}/api/v4/core/${dimension}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

return response.json();
}
}

module.exports = CoreAPIClient;
// api-client.test.js
const CoreAPIClient = require('./api-client');
const fetchMock = require('jest-fetch-mock');

fetchMock.enableMocks();

describe('CoreAPIClient', () => {
let client;

beforeEach(() => {
fetch.resetMocks();
client = new CoreAPIClient('https://api.q01.io', 'test-token');
});

describe('get', () => {
it('should fetch record by ID', async () => {
// Arrange
const mockProduct = {PRD_ID: 123, XPRD01: 'Widget'};
fetch.mockResponseOnce(JSON.stringify(mockProduct));

// Act
const result = await client.get('products', 123);

// Assert
expect(fetch).toHaveBeenCalledWith(
'https://api.q01.io/api/v4/core/products/123',
expect.objectContaining({
headers: {'Authorization': 'Bearer test-token'}
})
);
expect(result).toEqual(mockProduct);
});

it('should throw error on HTTP 404', async () => {
// Arrange
fetch.mockResponseOnce('Not Found', {status: 404});

// Act & Assert
await expect(client.get('products', 999))
.rejects.toThrow('HTTP 404: Not Found');
});
});

describe('create', () => {
it('should create new record', async () => {
// Arrange
const newProduct = {XPRD01: 'Widget', XPRD02: 29.99};
const createdProduct = {PRD_ID: 123, ...newProduct};
fetch.mockResponseOnce(JSON.stringify(createdProduct));

// Act
const result = await client.create('products', newProduct);

// Assert
expect(fetch).toHaveBeenCalledWith(
'https://api.q01.io/api/v4/core/products',
expect.objectContaining({
method: 'POST',
headers: {
'Authorization': 'Bearer test-token',
'Content-Type': 'application/json'
},
body: JSON.stringify(newProduct)
})
);
expect(result).toEqual(createdProduct);
});
});
});

Testing Business Logic

// product-service.js
class ProductService {
constructor(apiClient) {
this.apiClient = apiClient;
}

async getActiveProducts() {
const products = await this.apiClient.list('products', {
filters: 'TREC:eq:N,XPRD06:eq:Y'
});

return products.data;
}

validateProduct(product) {
const errors = [];

if (!product.XPRD01 || product.XPRD01.trim() === '') {
errors.push('Product name is required');
}

if (!product.XPRD02 || product.XPRD02 <= 0) {
errors.push('Price must be greater than 0');
}

return errors;
}
}

module.exports = ProductService;
// product-service.test.js
const ProductService = require('./product-service');

describe('ProductService', () => {
let service;
let mockAPIClient;

beforeEach(() => {
mockAPIClient = {
list: jest.fn()
};
service = new ProductService(mockAPIClient);
});

describe('getActiveProducts', () => {
it('should fetch only active products', async () => {
// Arrange
const mockProducts = {
data: [
{PRD_ID: 1, XPRD01: 'Product 1', XPRD06: 'Y'},
{PRD_ID: 2, XPRD01: 'Product 2', XPRD06: 'Y'}
]
};
mockAPIClient.list.mockResolvedValue(mockProducts);

// Act
const result = await service.getActiveProducts();

// Assert
expect(mockAPIClient.list).toHaveBeenCalledWith('products', {
filters: 'TREC:eq:N,XPRD06:eq:Y'
});
expect(result).toEqual(mockProducts.data);
});
});

describe('validateProduct', () => {
it('should return no errors for valid product', () => {
const validProduct = {
XPRD01: 'Widget',
XPRD02: 29.99
};

const errors = service.validateProduct(validProduct);

expect(errors).toHaveLength(0);
});

it('should return error for missing name', () => {
const invalidProduct = {
XPRD01: '',
XPRD02: 29.99
};

const errors = service.validateProduct(invalidProduct);

expect(errors).toContain('Product name is required');
});

it('should return error for invalid price', () => {
const invalidProduct = {
XPRD01: 'Widget',
XPRD02: -10
};

const errors = service.validateProduct(invalidProduct);

expect(errors).toContain('Price must be greater than 0');
});
});
});

Integration Testing

Testing Against Real API

// integration.test.js
const CoreAPIClient = require('./api-client');

// Use test environment
const API_URL = process.env.TEST_API_URL || 'https://api-test.q01.io';
const API_TOKEN = process.env.TEST_API_TOKEN;

describe('Integration Tests', () => {
let client;

beforeAll(() => {
if (!API_TOKEN) {
throw new Error('TEST_API_TOKEN environment variable required');
}
client = new CoreAPIClient(API_URL, API_TOKEN);
});

describe('Product CRUD', () => {
let createdProductId;

it('should create product', async () => {
const newProduct = {
XPRD01: `Test Product ${Date.now()}`,
XPRD02: 29.99,
XPRD03: `TEST-${Date.now()}`,
XPRD05: 1,
XPRD06: 'Y'
};

const result = await client.create('products', newProduct);

expect(result).toHaveProperty('PRD_ID');
expect(result.XPRD01).toBe(newProduct.XPRD01);
expect(result.XPRD02).toBe(newProduct.XPRD02);

createdProductId = result.PRD_ID;
});

it('should read product', async () => {
const result = await client.get('products', createdProductId);

expect(result.PRD_ID).toBe(createdProductId);
expect(result).toHaveProperty('XPRD01');
expect(result).toHaveProperty('XPRD02');
});

it('should update product', async () => {
const updates = {
XPRD02: 39.99
};

const result = await client.update('products', createdProductId, updates);

expect(result.XPRD02).toBe(39.99);
});

it('should delete product', async () => {
await client.delete('products', createdProductId);

// Verify deleted (should have TREC = 'C')
const result = await client.get('products', createdProductId);
expect(result.TREC).toBe('C');
});
});

describe('Filtering and Pagination', () => {
it('should filter by category', async () => {
const result = await client.list('products', {
filters: 'XPRD05:eq:1,TREC:eq:N',
limit: 10
});

expect(result.data).toBeInstanceOf(Array);
result.data.forEach(product => {
expect(product.XPRD05).toBe(1);
expect(product.TREC).toBe('N');
});
});

it('should paginate results', async () => {
const page1 = await client.list('products', {limit: 10, offset: 0});
const page2 = await client.list('products', {limit: 10, offset: 10});

expect(page1.data).toHaveLength(10);
expect(page2.data).toHaveLength(10);
expect(page1.data[0].PRD_ID).not.toBe(page2.data[0].PRD_ID);
});
});
});

Contract Testing

API Contract Verification

// contract.test.js
const Pact = require('@pact-foundation/pact');
const {like, eachLike} = Pact.Matchers;
const CoreAPIClient = require('./api-client');

describe('Core API Contract', () => {
const provider = new Pact({
consumer: 'MyApp',
provider: 'CoreAPI',
port: 1234,
log: './logs/pact.log',
logLevel: 'warn'
});

beforeAll(() => provider.setup());
afterAll(() => provider.finalize());
afterEach(() => provider.verify());

describe('GET /api/v4/core/products/:id', () => {
it('returns product by ID', async () => {
// Define expected interaction
await provider.addInteraction({
state: 'product 123 exists',
uponReceiving: 'a request for product 123',
withRequest: {
method: 'GET',
path: '/api/v4/core/products/123',
headers: {
'Authorization': like('Bearer token123')
}
},
willRespondWith: {
status: 200,
headers: {'Content-Type': 'application/json'},
body: {
PRD_ID: 123,
XPRD01: like('Product Name'),
XPRD02: like(29.99),
XPRD03: like('PRD-001'),
TREC: 'N'
}
}
});

// Test
const client = new CoreAPIClient('http://localhost:1234', 'token123');
const result = await client.get('products', 123);

expect(result.PRD_ID).toBe(123);
expect(result).toHaveProperty('XPRD01');
});
});

describe('POST /api/v4/core/products', () => {
it('creates new product', async () => {
await provider.addInteraction({
state: 'user has create permission',
uponReceiving: 'a request to create product',
withRequest: {
method: 'POST',
path: '/api/v4/core/products',
headers: {
'Authorization': like('Bearer token123'),
'Content-Type': 'application/json'
},
body: {
XPRD01: 'New Product',
XPRD02: 29.99,
XPRD05: 1
}
},
willRespondWith: {
status: 201,
headers: {'Content-Type': 'application/json'},
body: {
PRD_ID: like(456),
XPRD01: 'New Product',
XPRD02: 29.99,
XPRD05: 1,
TREC: 'N'
}
}
});

const client = new CoreAPIClient('http://localhost:1234', 'token123');
const result = await client.create('products', {
XPRD01: 'New Product',
XPRD02: 29.99,
XPRD05: 1
});

expect(result).toHaveProperty('PRD_ID');
expect(result.XPRD01).toBe('New Product');
});
});
});

Test Data Management

Test Data Setup

// test-data.js
class TestDataManager {
constructor(apiClient) {
this.apiClient = apiClient;
this.createdRecords = [];
}

async createTestProduct(overrides = {}) {
const product = {
XPRD01: `Test Product ${Date.now()}`,
XPRD02: 29.99,
XPRD03: `TEST-${Date.now()}`,
XPRD05: 1,
XPRD06: 'Y',
...overrides
};

const result = await this.apiClient.create('products', product);
this.createdRecords.push({dimension: 'products', id: result.PRD_ID});

return result;
}

async createTestCategory(overrides = {}) {
const category = {
XCAT01: `Test Category ${Date.now()}`,
XCAT02: 'Test category description',
...overrides
};

const result = await this.apiClient.create('categories', category);
this.createdRecords.push({dimension: 'categories', id: result.CAT_ID});

return result;
}

async cleanup() {
// Delete all created records in reverse order
for (const record of this.createdRecords.reverse()) {
try {
await this.apiClient.delete(record.dimension, record.id);
} catch (error) {
console.error(`Failed to cleanup ${record.dimension}/${record.id}:`, error);
}
}

this.createdRecords = [];
}
}

module.exports = TestDataManager;
// Usage in tests
const TestDataManager = require('./test-data');

describe('Product Tests with Test Data', () => {
let testData;

beforeEach(() => {
testData = new TestDataManager(apiClient);
});

afterEach(async () => {
await testData.cleanup();
});

it('should create product with category', async () => {
// Setup test data
const category = await testData.createTestCategory();
const product = await testData.createTestProduct({
XPRD05: category.CAT_ID
});

// Test
expect(product.XPRD05).toBe(category.CAT_ID);

// Cleanup handled automatically
});
});

E2E Testing

Playwright E2E Tests

// e2e/product-management.spec.js
const {test, expect} = require('@playwright/test');

test.describe('Product Management', () => {
test.beforeEach(async ({page}) => {
// Login
await page.goto('https://app.q01.io/login');
await page.fill('[name="username"]', 'test@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
});

test('should create new product', async ({page}) => {
// Navigate to products
await page.goto('https://app.q01.io/products');

// Click create button
await page.click('button:has-text("New Product")');

// Fill form
await page.fill('[name="XPRD01"]', 'E2E Test Product');
await page.fill('[name="XPRD02"]', '49.99');
await page.selectOption('[name="XPRD05"]', '1');

// Submit
await page.click('button:has-text("Save")');

// Verify success
await expect(page.locator('.success-message')).toContainText('Product created');

// Verify in list
await page.goto('https://app.q01.io/products');
await expect(page.locator('table')).toContainText('E2E Test Product');
});

test('should filter products by category', async ({page}) => {
await page.goto('https://app.q01.io/products');

// Select category filter
await page.selectOption('[name="category"]', '1');
await page.click('button:has-text("Filter")');

// Verify filtered results
const rows = await page.locator('table tbody tr').all();
for (const row of rows) {
const category = await row.locator('td.category').textContent();
expect(category).toBe('Electronics');
}
});
});

Performance Testing

Load Testing with Artillery

# load-test.yml
config:
target: 'https://api.q01.io'
phases:
- duration: 60
arrivalRate: 10
name: "Warm up"
- duration: 120
arrivalRate: 50
name: "Sustained load"
- duration: 60
arrivalRate: 100
name: "Spike"
variables:
token: "{{ $processEnvironment.API_TOKEN }}"

scenarios:
- name: "Product operations"
flow:
- get:
url: "/api/v4/core/products"
headers:
Authorization: "Bearer {{ token }}"
expect:
- statusCode: 200
- contentType: json
- post:
url: "/api/v4/core/products"
headers:
Authorization: "Bearer {{ token }}"
Content-Type: "application/json"
json:
XPRD01: "Load Test Product {{ $randomNumber() }}"
XPRD02: 29.99
XPRD05: 1
expect:
- statusCode: 201
# Run load test
artillery run load-test.yml

# Generate report
artillery run --output report.json load-test.yml
artillery report report.json

CI/CD Integration

GitHub Actions Workflow

# .github/workflows/test.yml
name: Test

on:
push:
branches: [main, develop]
pull_request:
branches: [main]

jobs:
unit-tests:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'

- name: Install dependencies
run: npm ci

- name: Run unit tests
run: npm test

- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info

integration-tests:
runs-on: ubuntu-latest
needs: unit-tests

steps:
- uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'

- name: Install dependencies
run: npm ci

- name: Run integration tests
run: npm run test:integration
env:
TEST_API_URL: ${{ secrets.TEST_API_URL }}
TEST_API_TOKEN: ${{ secrets.TEST_API_TOKEN }}

e2e-tests:
runs-on: ubuntu-latest
needs: integration-tests

steps:
- uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'

- name: Install dependencies
run: npm ci

- name: Install Playwright
run: npx playwright install --with-deps

- name: Run E2E tests
run: npm run test:e2e

- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/

Testing Checklist

Before Deployment

  • All unit tests passing
  • Integration tests passing
  • Contract tests verified
  • E2E tests passing
  • Performance tests meet targets
  • Security tests passed
  • Test coverage > 80%

Test Data

  • Test data isolated
  • Cleanup after tests
  • No hardcoded IDs
  • Realistic test data
  • Test different scenarios

CI/CD

  • Tests run on every commit
  • Tests run on PR
  • Test reports generated
  • Coverage tracked
  • Failed tests block deployment

Summary

Testing Pyramid:

  1. Unit Tests (70%) - Fast, isolated, many
  2. Integration Tests (20%) - API interactions
  3. E2E Tests (10%) - Complete workflows

Key Takeaways:

  • Test at multiple levels
  • Mock external dependencies
  • Manage test data properly
  • Integrate with CI/CD
  • Monitor test coverage
  • Performance test before production