Testing Guide
This guide covers testing practices and procedures for the n8n-nodes-substack project.
Testing Philosophy
The project follows a comprehensive testing strategy focused on:
- Unit Testing: Individual node operations and utility functions
- Integration Testing: End-to-end node execution workflows
- Mock-based Testing: Using mocked Substack API responses for reliable testing
- No External Dependencies: Tests run without requiring actual Substack API access
Test Structure
Test Organization
tests/
├── unit/ # Unit tests
│ ├── integration.test.ts # Full node integration tests
│ ├── post-operations.test.ts # Post resource tests
│ ├── note-operations.test.ts # Note resource tests
│ ├── comment-operations.test.ts # Comment resource tests
│ ├── SubstackUtils.test.ts # Utility function tests
│ └── new-api-integration.test.ts # API integration tests
└── mocks/ # Mock data and utilities
├── mockSubstackClient.ts # Mocked Substack API client
├── mockExecuteFunctions.ts # Mocked n8n execution functions
└── mockData.ts # Test data fixtures
Test Categories
1. Unit Tests
Test individual operations in isolation:
// Example: Testing a single post operation
describe('Post Operations', () => {
it('should retrieve posts with getAll operation', async () => {
// Setup mocks
const mockExecuteFunctions = createMockExecuteFunctions({
nodeParameters: {
resource: 'post',
operation: 'getAll',
limit: 10
}
});
// Execute operation
const result = await substackNode.execute.call(mockExecuteFunctions);
// Verify results
expect(result[0]).toHaveLength(2); // Expected number of posts
expect(result[0][0].json).toHaveProperty('id');
});
});
2. Integration Tests
Test complete node execution workflows:
// Example: Testing resource and operation validation
describe('Parameter Validation', () => {
it('should validate all resource types', async () => {
const resources = ['note', 'post', 'comment', 'profile'];
for (const resource of resources) {
const mockExecuteFunctions = createMockExecuteFunctions({
nodeParameters: { resource, operation: 'getAll' }
});
const result = await substackNode.execute.call(mockExecuteFunctions);
expect(result).toBeDefined();
}
});
});
Running Tests
Basic Test Commands
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run specific test file
npm test -- post-operations.test.ts
# Run tests with coverage
npm test -- --coverage
Test Configuration
Tests use Jest with TypeScript support configured in jest.config.js:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/tests'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
coverageDirectory: 'coverage',
collectCoverageFrom: [
'nodes/**/*.ts',
'!nodes/**/*.d.ts',
'!**/node_modules/**'
]
};
Writing Tests
Mock Setup
Creating Mock Execution Functions
import { createMockExecuteFunctions } from '../mocks/mockExecuteFunctions';
const mockExecuteFunctions = createMockExecuteFunctions({
nodeParameters: {
resource: 'post',
operation: 'getAll',
limit: 10
},
credentials: {
publicationAddress: 'test.substack.com',
apiKey: 'test-api-key'
}
});
Using Mock Data
import { mockPostsData } from '../mocks/mockData';
// Mock the Substack client
jest.mock('substack-api', () => ({
SubstackClient: jest.fn().mockImplementation(() => ({
ownProfile: jest.fn().mockReturnValue(mockProfileData),
posts: jest.fn().mockReturnValue({
async *[Symbol.asyncIterator]() {
for (const post of mockPostsData) {
yield post;
}
}
})
}))
}));
Test Patterns
Testing Successful Operations
it('should handle successful post retrieval', async () => {
const mockExecuteFunctions = createMockExecuteFunctions({
nodeParameters: { resource: 'post', operation: 'getAll' }
});
const result = await substackNode.execute.call(mockExecuteFunctions);
expect(result[0]).toHaveLength(expectedPosts.length);
expect(result[0][0].json).toMatchObject({
id: expect.any(Number),
title: expect.any(String),
publishedAt: expect.any(String)
});
});
Testing Error Handling
it('should handle API errors gracefully', async () => {
const mockClient = {
ownProfile: jest.fn().mockRejectedValue(new Error('API Error'))
};
const mockExecuteFunctions = createMockExecuteFunctions({
nodeParameters: { resource: 'profile', operation: 'getOwnProfile' }
});
await expect(
substackNode.execute.call(mockExecuteFunctions)
).rejects.toThrow('API Error');
});
Testing Edge Cases
it('should handle empty responses', async () => {
// Mock empty response
const mockClient = {
posts: jest.fn().mockReturnValue({
async *[Symbol.asyncIterator]() {
// Empty iterator
}
})
};
const result = await substackNode.execute.call(mockExecuteFunctions);
expect(result[0]).toHaveLength(0);
});
Best Practices
1. Comprehensive Coverage
- Test all supported operations for each resource
- Include both success and error scenarios
- Test edge cases (empty responses, malformed data)
2. Isolated Testing
- Use mocks to avoid external dependencies
- Test each operation independently
- Don't rely on specific API data that might change
3. Realistic Mock Data
- Use realistic Substack API response structures
- Include both minimal and complete data examples
- Test with various data types and edge cases
4. Clear Test Names
// Good: Descriptive test names
it('should format post output with default values for missing fields', async () => {
// Bad: Vague test names
it('should work with posts', async () => {
5. Test Organization
- Group related tests in describe blocks
- Use setup/teardown when appropriate
- Keep tests focused and atomic
Debugging Tests
Common Issues
- Mock Setup Problems
- Ensure mocks are configured before importing the tested module
-
Check that mock return values match expected API structure
-
Async Testing Issues
- Use proper async/await syntax
-
Handle promises correctly in test assertions
-
Type Errors
- Ensure mock objects match TypeScript interfaces
- Use proper typing for mock functions
Debug Tools
# Run tests with detailed output
npm test -- --verbose
# Run specific test with debugging
npm test -- --testNamePattern="specific test name"
# Check test coverage
npm test -- --coverage --coverageReporters=text-lcov
Contributing Test Changes
When adding new operations or modifying existing ones:
- Add corresponding tests for new functionality
- Update existing tests if behavior changes
- Ensure all tests pass before submitting changes
- Maintain test coverage above 80%
- Add mock data for new API responses if needed