This guide covers testing practices and procedures for the n8n-nodes-substack project.
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
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 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');
});
});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();
}
});
});# 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 -- --coverageTests 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/**'
]
};import { createMockExecuteFunctions } from '../mocks/mockExecuteFunctions';
const mockExecuteFunctions = createMockExecuteFunctions({
nodeParameters: {
resource: 'post',
operation: 'getAll',
limit: 10
},
credentials: {
publicationAddress: 'test.substack.com',
apiKey: 'test-api-key'
}
});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;
}
}
})
}))
}));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)
});
});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');
});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);
});- Test all supported operations for each resource
- Include both success and error scenarios
- Test edge cases (empty responses, malformed data)
- Use mocks to avoid external dependencies
- Test each operation independently
- Don't rely on specific API data that might change
- Use realistic Substack API response structures
- Include both minimal and complete data examples
- Test with various data types and edge cases
// 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 () => {- Group related tests in describe blocks
- Use setup/teardown when appropriate
- Keep tests focused and atomic
-
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
# 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-lcovWhen 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