eslint-plugin-getdeps
v1.8.1
Published
ESLint rules for enforcing the getDeps dependency injection pattern
Maintainers
Readme
eslint-plugin-getdeps
Elegant dependency injection for modern JavaScript
Because testing shouldn't be a battle against your own code
ESLint rules for enforcing the getDeps dependency injection pattern. This pattern transforms your code from a testing nightmare into a testing dream, using dependency injection with default parameters to create clean, maintainable, and thoroughly testable code.
Installation
npm install --save-dev eslint-plugin-getdepsUsage
Quick Start (Recommended)
For most projects, use the recommended configuration:
// eslint.config.js
import getdeps from 'eslint-plugin-getdeps';
export default [
getdeps.configs.recommended,
// your other config...
];Advanced Usage
For maximum enforcement, use the strict configuration:
// eslint.config.js
import getdeps from 'eslint-plugin-getdeps';
export default [
getdeps.configs.strict,
// your other config...
];Manual Configuration
For custom setups, configure rules individually:
// eslint.config.js
import getdeps from 'eslint-plugin-getdeps';
export default [
{
plugins: {
getdeps,
},
rules: {
'getdeps/no-unused-getdeps-props': 'error',
'getdeps/no-missing-getdeps-props': 'error',
'getdeps/no-nested-getdeps-calls': 'error',
'getdeps/no-getdeps-as-argument': 'error',
'getdeps/no-export-getdeps': 'error',
'getdeps/no-unnecessary-getdeps': 'error',
'getdeps/imports-in-getdeps': 'error',
'getdeps/getdeps-shadowing': 'error',
'getdeps/no-multiple-getdeps': 'error',
'getdeps/no-globals-in-getdeps': 'error',
},
},
];Configuration Differences
| Configuration | no-unnecessary-getdeps | imports-in-getdeps | no-globals-in-getdeps | Purpose |
|---------------|-------------------------|---------------------|------------------------|---------|
| recommended | warn | off | error | Gentle introduction with warnings |
| strict | error | always | error | Maximum enforcement of all patterns |
| manual | Your choice | Your choice | Your choice | Full customization |
Configuration Options
All rules support configurable function names to match your team's conventions:
// eslint.config.js
import getdeps from 'eslint-plugin-getdeps';
export default [
{
plugins: {
getdeps,
},
rules: {
'getdeps/no-unused-getdeps-props': ['error', {
functionName: 'getDependencies', // Custom function name (default: 'getDeps')
callName: 'deps' // Custom call name (default: '_getDeps')
}],
'getdeps/no-missing-getdeps-props': ['error', {
functionName: 'getDependencies', // Custom function name (default: 'getDeps')
callName: 'deps' // Custom call name (default: '_getDeps')
}],
'getdeps/no-nested-getdeps-calls': ['error', {
functionName: 'getDependencies',
callName: 'deps'
}],
'getdeps/no-getdeps-as-argument': ['error', {
functionName: 'getDependencies',
callName: 'deps',
allowInTests: true, // Allow in test files (default: true)
testFilePatterns: [ // Custom test file patterns (default: see below)
'**/*.test.js',
'**/*.spec.js',
'**/__tests__/**/*.js',
'**/test/**/*.js'
]
}],
'getdeps/no-export-getdeps': ['error', {
functionName: 'getDependencies',
callName: 'deps',
allowInTests: false,
allowInHelpers: false
}],
'getdeps/no-unnecessary-getdeps': ['error', {
functionName: 'getDependencies'
}],
},
},
];Configuration Options:
functionName: The name of your dependency function (default:'getDeps')callName: The name(s) used to call the function in your code (default:['getDeps', '_getDeps', 'getDependencies', '_getDependencies'])allowInTests: Whether to allow passing getDeps as arguments in test files (default:true)testFilePatterns: Array of glob patterns to identify test files (default:['**/*.test.js', '**/*.spec.js', '**/__tests__/**/*.js', '**/test/**/*.js'])
Important Note: By default, the plugin supports multiple call names: getDeps, _getDeps, getDependencies, and _getDependencies. This means you can use any of these names without additional configuration. If you want to use a different name, you can configure the callName option.
Example with custom names:
// Using custom function names
function getDependencies() {
return { logger: console.log };
}
function myFunction(deps = getDependencies) {
const { logger } = deps(); // ✅ Works with custom names
}Example: Using getDeps() directly (fixes the bug report issue):
// If you want to call getDeps() directly instead of _getDeps()
// eslint.config.js
export default [
{
plugins: { getdeps },
rules: {
'getdeps/no-unused-getdeps-props': ['error', {
functionName: 'getDeps',
callName: 'getDeps', // ✅ Configure to use getDeps() directly
}],
'getdeps/no-nested-getdeps-calls': ['error', {
functionName: 'getDeps',
callName: 'getDeps',
}],
'getdeps/no-getdeps-as-argument': ['error', {
functionName: 'getDeps',
callName: 'getDeps',
}],
},
},
];
// Now this works without false positives:
function getDeps() {
return {
logger: console.log,
config: { baseUrl: 'https://api.example.com' }
};
}
function functionA() {
const { logger } = getDeps(); // ✅ No false positive
logger.info('Hello from functionA');
}
function functionB() {
const { config } = getDeps(); // ✅ No false positive
console.log('Base URL:', config.baseUrl);
}Example with test file exceptions:
// eslint.config.js - Allow passing getDeps in test files
export default [
{
rules: {
'getdeps/no-getdeps-as-argument': ['error', {
allowInTests: true, // ✅ Allows passing getDeps in test files
testFilePatterns: [
'**/*.test.js',
'**/*.spec.js',
'**/tests/**/*.js', // Custom test directory
'**/e2e/**/*.js' // End-to-end tests
]
}],
},
},
];
// ✅ This is now allowed in test files
// test/user.test.js
function testUserFunction() {
const mockDeps = { logger: jest.fn() };
const result = userFunction('123', () => mockDeps); // ✅ No error in test files
}Example with globals protection:
// eslint.config.js - Protect against global API usage
export default [
{
rules: {
'getdeps/no-globals-in-getdeps': ['error', {
allowInTests: true, // Allow globals in test files
ignoreGlobals: ['process'], // Ignore specific globals
testFilePatterns: [
'**/*.test.js',
'**/*.spec.js',
'**/__tests__/**/*'
]
}],
},
},
];
// ❌ This will be flagged in source files
// src/api.js
function getDeps() {
return {
window: global.window, // ❌ Error - use Jest mocking instead
fetch: global.fetch // ❌ Error - use Jest mocking instead
};
}
// ✅ This is allowed in test files
// test/api.test.js
function getDeps() {
return {
window: global.window, // ✅ OK in test files when allowInTests: true
fetch: global.fetch // ✅ OK in test files when allowInTests: true
};
}Troubleshooting
Common Issues
False Positives with no-unused-getdeps-props
Problem: The rule reports properties as unused even when they are used through destructuring.
Example:
function getDeps() {
return {
logger: console.log,
config: { baseUrl: 'https://api.example.com' }
};
}
function functionA() {
const { logger } = getDeps(); // ❌ False positive: logger reported as unused
logger.info('Hello');
}Solution: The plugin now supports multiple call names by default, including getDeps(). If you're still experiencing issues, you can configure the callName option explicitly:
// eslint.config.js
export default [
{
plugins: { getdeps },
rules: {
'getdeps/no-unused-getdeps-props': ['error', {
callName: 'getDeps', // ✅ Match your actual function call
}],
},
},
];Why this happens: By default, the plugin supports multiple call names including getDeps(), so this issue should not occur with the current version. If you're using a custom call name that's not in the default list, you need to configure it explicitly.
Rules
no-unused-getdeps-props
Detects unused properties in getDeps function returns.
❌ Bad:
function getDeps() {
return {
logger: console.log,
helper: () => {},
unusedProperty: 'this is never used', // ❌ Error
};
}
function someFunction() {
const { logger } = _getDeps(); // Only logger is used
}✅ Good:
function getDeps() {
return {
logger: console.log,
helper: () => {},
};
}
function someFunction() {
const { logger, helper } = _getDeps(); // All properties are used
}no-missing-getdeps-props
Detects missing properties in getDeps function returns that are requested in destructuring patterns. This provides bidirectional protection from missing dependencies.
❌ Bad:
function getDeps() {
return {
logger: console.log,
// Missing 'api' property that is requested in destructuring
};
}
function someFunction() {
const { logger, api } = _getDeps(); // ❌ Error - 'api' is not returned by getDeps
logger('hello');
api.get('/users');
}✅ Good:
function getDeps() {
return {
logger: console.log,
api: fetch, // ✅ All requested properties are returned
};
}
function someFunction() {
const { logger, api } = _getDeps(); // ✅ All properties exist
logger('hello');
api.get('/users');
}no-nested-getdeps-calls
Ensures _getDeps() calls are at the top level of functions.
❌ Bad:
function badFunction() {
if (condition) {
const { logger } = _getDeps(); // ❌ Error - nested in if
}
return () => {
const { logger } = _getDeps(); // ❌ Error - nested in arrow function
};
}✅ Good:
function goodFunction() {
const { logger } = _getDeps(); // ✅ OK - at top level
if (condition) {
logger('hello');
}
return () => {
logger('world');
};
}no-getdeps-as-argument
Prevents getDeps from being passed as arguments - use default parameters instead. Note: This rule allows passing getDeps in test files by default (configurable via allowInTests and testFilePatterns options).
❌ Bad:
function badFunction() {
const handler = createHandler(getDeps); // ❌ Error - passing as argument
}
function badFunction2(getDeps) { // ❌ Error - required parameter
const { logger } = getDeps();
}
const badFunction3 = (getDeps) => { // ❌ Error - required parameter
const { logger } = getDeps();
};✅ Good:
function goodFunction(_getDeps = getDeps) { // ✅ OK - default parameter
const { logger } = _getDeps();
}
const goodFunction2 = (_getDeps = getDeps) => { // ✅ OK - default parameter
const { logger } = _getDeps();
};✅ Allowed in test files:
// test/user.test.js
function testUserFunction() {
const mockDeps = { logger: jest.fn() };
const result = userFunction('123', () => mockDeps); // ✅ OK - test file exception
}no-unnecessary-getdeps
Detects when getDeps is used for dependencies that could be mocked with traditional methods like Jest.
❌ Bad:
function getDeps() {
return {
// Framework dependencies that Jest handles well
axios: require('axios'),
lodash: require('lodash'),
// Simple utility functions with no external dependencies
add: (a, b) => a + b,
isEven: (num) => num % 2 === 0,
};
}✅ Good:
function getDeps() {
return {
// Complex functions with external dependencies
processUser: (userId) => {
const user = database.getUser(userId);
return api.updateUser(user);
},
// Functions that interact with external services
sendEmail: (to, subject) => {
return emailService.send({ to, subject });
},
};
}
// Use Jest mocks for simple dependencies
jest.mock('axios');
jest.mock('lodash');no-export-getdeps
Prevents configured getDeps function names from being exported from modules. This ensures that getDeps functions remain internal to modules and aren't accidentally exposed as part of the public API.
❌ Bad:
function getDeps() {
return { logger: console.log };
}
export { getDeps }; // ❌ Error - getDeps should not be exported
export default getDeps; // ❌ Error - getDeps should not be exported✅ Good:
function getDeps() {
return { logger: console.log };
}
function processUser(userId, deps = getDeps) {
const { logger } = deps();
logger('Processing user');
}
export { processUser }; // ✅ OK - export other functions, not getDeps
export default processUser; // ✅ OK - export other functions, not getDepsConfiguration options:
'getdeps/no-export-getdeps': ['error', {
functionName: 'getDependencies', // Custom function name (default: 'getDeps')
callName: 'deps', // Custom call name (default: '_getDeps')
allowInTests: false, // Allow exports in test files (default: false)
testFilePatterns: [ // Custom test file patterns
'**/*.test.js',
'**/*.spec.js'
],
allowInHelpers: false, // Allow exports in helper files (default: false)
helperFilePatterns: [ // Custom helper file patterns
'**/helpers/**/*',
'**/utils/**/*'
]
}]✅ Allowed in test files when configured:
// test/user.test.js
function getDeps() {
return { logger: jest.fn() };
}
export { getDeps }; // ✅ OK - when allowInTests: true✅ Allowed in helper files when configured:
// helpers/utils.js
function getDeps() {
return { logger: console.log };
}
export { getDeps }; // ✅ OK - when allowInHelpers: trueimports-in-getdeps
Validates that imported modules are properly included in getDeps declarations. This rule helps ensure consistency between your imports and your dependency injection pattern.
Configuration options:
'getdeps/imports-in-getdeps': ['error', {
functionName: 'getDeps', // Custom function name (default: 'getDeps')
callName: '_getDeps', // Custom call name (default: '_getDeps')
mode: 'always', // 'off' | 'always' | 'never' (default: 'always')
ignorePatterns: ['React', 'react'], // Patterns to ignore (default: [])
allowTypeImports: true // Allow type imports to be excluded (default: true)
}]Mode: 'always' (default) - All imports should be in getDeps:
❌ Bad:
import { logger } from './logger';
import axios from 'axios';
function getDeps() {
return { logger }; // ❌ Error - axios is imported but not in getDeps
}
function someFunction() {
const { logger } = _getDeps();
axios.get('/api/users'); // Using axios but it's not in getDeps
}✅ Good:
import { logger } from './logger';
import axios from 'axios';
function getDeps() {
return { logger, axios }; // ✅ All imports are included
}
function someFunction() {
const { logger, axios } = _getDeps();
axios.get('/api/users');
}Mode: 'never' - No imports should be in getDeps:
❌ Bad:
import { logger } from './logger';
function getDeps() {
return { logger }; // ❌ Error - logger is imported and in getDeps
}
function someFunction() {
const { logger } = _getDeps();
}✅ Good:
import { logger } from './logger';
function getDeps() {
return { database, api }; // ✅ No imported modules in getDeps
}
function someFunction() {
const { database, api } = _getDeps();
logger('Using database and api from getDeps');
}Mode: 'off' - No validation:
✅ Good:
import { logger } from './logger';
import axios from 'axios';
function getDeps() {
return { logger }; // ✅ No validation when mode is 'off'
}
function someFunction() {
const { logger } = _getDeps();
axios.get('/api/users');
}Ignoring patterns:
'getdeps/imports-in-getdeps': ['error', {
mode: 'always',
ignorePatterns: ['React', 'react', 'lodash'] // These imports won't be validated
}]getdeps-shadowing
Enforces consistent naming patterns for destructured properties to avoid shadowing and make dependency injection more explicit.
Configuration options:
'getdeps/getdeps-shadowing': ['error', {
functionName: 'getDeps', // Custom function name (default: 'getDeps')
callName: '_getDeps', // Custom call name (default: '_getDeps')
mode: 'all', // 'all' | 'none' (default: 'all')
prefix: '_', // Prefix to use (default: '_')
ignorePatterns: ['api', 'config'] // Patterns to ignore (default: [])
}]Mode: 'all' (default) - All destructured properties should use prefix aliases:
❌ Bad:
function getDeps() {
return { logger, api };
}
function someFunction() {
const { logger, api } = _getDeps(); // ❌ Error - should use prefix aliases to avoid shadowing
}✅ Good:
function getDeps() {
return { logger, api };
}
function someFunction() {
const { logger: _logger, api: _api } = _getDeps(); // ✅ Uses underscore aliases to avoid shadowing
}Custom prefix example:
// Using 'mock' prefix
'getdeps/getdeps-shadowing': ['error', { mode: 'all', prefix: 'mock' }]
function getDeps() {
return { logger, api };
}
function someFunction() {
const { logger: mockLogger, api: mockApi } = _getDeps(); // ✅ Uses 'mock' prefix aliases
}Mode: 'none' - No destructured properties should use prefix aliases:
❌ Bad:
function getDeps() {
return { logger, api };
}
function someFunction() {
const { logger: _logger, api: _api } = _getDeps(); // ❌ Error - should not use prefix aliases
}✅ Good:
function getDeps() {
return { logger, api };
}
function someFunction() {
const { logger, api } = _getDeps(); // ✅ No prefix aliases
}Ignoring patterns:
'getdeps/getdeps-shadowing': ['error', {
mode: 'all',
prefix: '_',
ignorePatterns: ['api'] // Properties containing 'api' won't be validated
}]Common prefix examples:
// Underscore prefix (default)
'getdeps/getdeps-shadowing': ['error', { mode: 'all', prefix: '_' }]
// Result: { logger: _logger, api: _api }
// Mock prefix
'getdeps/getdeps-shadowing': ['error', { mode: 'all', prefix: 'mock' }]
// Result: { logger: mockLogger, api: mockApi }
// Test prefix
'getdeps/getdeps-shadowing': ['error', { mode: 'all', prefix: 'test' }]
// Result: { logger: testLogger, api: testApi }
// No prefix
'getdeps/getdeps-shadowing': ['error', { mode: 'none' }]
// Result: { logger, api }no-multiple-getdeps
Prevents multiple getDeps function declarations within the same module to maintain clarity and avoid confusion.
Why this rule exists:
According to the getDeps pattern, there should only be one getDeps function per module. Multiple declarations can lead to:
- Confusion about which implementation is being used
- Inconsistent dependency injection across the module
- Maintenance issues when updating dependencies
Rule: 'getdeps/no-multiple-getdeps': 'error'
❌ Incorrect:
function getDeps() {
return { logger: console.log };
}
// Later in the same file - this will be flagged
getDeps = () => {
return { api: fetch };
};✅ Correct:
// Only one getDeps function per module
function getDeps() {
return {
logger: console.log,
api: fetch
};
}
// Use the single getDeps function consistently
function myFunction(_getDeps = getDeps) {
const { logger, api } = _getDeps();
// ...
}Configuration options:
'getdeps/no-multiple-getdeps': ['error', {
functionName: 'getDeps' // Custom function name to check (default: 'getDeps')
}]Example with custom function name:
// Will detect multiple 'getDependencies' declarations
'getdeps/no-multiple-getdeps': ['error', { functionName: 'getDependencies' }]no-globals-in-getdeps
Prevents global APIs from being used in getDeps declarations or destructuring, encouraging the use of Jest mocking instead.
Why this rule exists:
Global APIs (like window, document, console, fetch, etc.) should be mocked using Jest rather than injected through getDeps. This ensures:
- Better test isolation
- Consistent mocking across your test suite
- Proper handling of browser and Node.js globals
- Cleaner dependency management
Rule: 'getdeps/no-globals-in-getdeps': 'error'
❌ Incorrect:
function getDeps() {
return {
window: global.window, // ❌ Global API
document: global.document, // ❌ Global API
fetch: global.fetch, // ❌ Global API
console: global.console // ❌ Global API
};
}
function myFunction(_getDeps = getDeps) {
const { window, document } = _getDeps(); // ❌ Destructuring globals
}✅ Correct:
// Use Jest mocking for global APIs
jest.mock('global', () => ({
window: mockWindow,
document: mockDocument,
fetch: mockFetch,
console: mockConsole
}));
function getDeps() {
return {
api: require('./api'), // ✅ Local dependency
utils: require('./utils'), // ✅ Local dependency
config: require('./config') // ✅ Local dependency
};
}
function myFunction(_getDeps = getDeps) {
const { api, utils } = _getDeps(); // ✅ Only local dependencies
}Configuration options:
'getdeps/no-globals-in-getdeps': ['error', {
functionName: 'getDeps', // Custom function name (default: 'getDeps')
callName: '_getDeps', // Custom call name (default: '_getDeps')
allowInTests: false, // Allow globals in test files (default: false)
testFilePatterns: [ // Test file patterns (default: ['**/*.test.*'])
'**/*.test.js',
'**/*.spec.js',
'**/__tests__/**/*'
],
ignoreGlobals: ['customGlobal'] // Globals to ignore (default: [])
}]Example with custom configuration:
// Allow globals in test files and ignore specific globals
'getdeps/no-globals-in-getdeps': ['error', {
allowInTests: true,
ignoreGlobals: ['process', 'Buffer']
}]Resolving Lint Errors
When you encounter lint errors from this plugin, here are quick links to help you understand and fix them:
Common Error Solutions
- no-unused-getdeps-props: Remove unused properties from your
getDeps()return object or use all properties in your destructuring - no-missing-getdeps-props: Add missing properties to your
getDeps()return object that are requested in destructuring patterns - no-nested-getdeps-calls: Move
_getDeps()calls to the top level of your function, outside of any conditional blocks or nested functions - no-getdeps-as-argument: Use default parameters instead of passing
getDepsas a function argument - no-unnecessary-getdeps: Use Jest mocks for framework dependencies and simple utility functions instead of getDeps
- imports-in-getdeps: Ensure imported modules are properly included in getDeps declarations or configure the rule mode
- getdeps-shadowing: Use consistent naming patterns for destructured properties to avoid shadowing
- no-multiple-getdeps: Maintain only one getDeps function declaration per module
- no-globals-in-getdeps: Use Jest mocking for global APIs instead of injecting them through getDeps
Quick Fix Examples
Error: "getDeps should not be passed as an argument"
// ❌ Before
function myFunction(deps) {
const { logger } = deps();
}
// ✅ After
function myFunction(_getDeps = getDeps) {
const { logger } = _getDeps();
}Error: "getDeps call should be at top level"
// ❌ Before
function myFunction() {
if (condition) {
const { logger } = _getDeps();
}
}
// ✅ After
function myFunction() {
const { logger } = _getDeps();
if (condition) {
logger('hello');
}
}Error: "getDeps destructuring requests property that is not returned"
// ❌ Before
function getDeps() {
return {
logger: console.log,
// Missing 'api' property
};
}
function myFunction() {
const { logger, api } = _getDeps(); // Error: 'api' not returned
}
// ✅ After
function getDeps() {
return {
logger: console.log,
api: fetch, // Add the missing property
};
}
function myFunction() {
const { logger, api } = _getDeps(); // Now works
}Error: "Unused property in getDeps return"
// ❌ Before
function getDeps() {
return { logger: console.log, unused: 'never used' };
}
// ✅ After
function getDeps() {
return { logger: console.log };
}Error: "getDeps returns framework dependency that can be mocked with Jest"
// ❌ Before
function getDeps() {
return {
axios: require('axios'),
add: (a, b) => a + b
};
}
// ✅ After
function getDeps() {
return {
processUser: (userId) => {
const user = database.getUser(userId);
return api.updateUser(user);
}
};
}
// Use Jest mocks for simple dependencies
jest.mock('axios');Why the getDeps Pattern?
Ah, the age-old question: "Why another dependency injection pattern?"
The getDeps pattern doesn't just solve problems—it transforms your development experience. While other DI solutions feel like wearing a suit to a beach party, getDeps feels like the perfect pair of jeans: comfortable, practical, and always appropriate.
The Problems We're Solving
- Testing Nightmares: Functions with hardcoded dependencies are like trying to test a black box with no doors
- Tight Coupling: Direct imports create code that's as flexible as a concrete block
- Integration Testing Complexity: Mixing real and mock dependencies shouldn't require a PhD in testing theory
- Dependency Pollution: Unused dependencies are like leaving empty boxes in your attic—they just take up space
Before getDeps (The Dark Ages)
// ❌ Hard to test - dependencies are baked in like concrete
import { logger } from './logger';
import { database } from './database';
import { api } from './api';
function processUser(userId) {
logger(`Processing user ${userId}`);
const user = database.getUser(userId);
return api.updateUser(user);
}
// Testing is painful - you need to mock modules
jest.mock('./logger');
jest.mock('./database');
jest.mock('./api');
// ... lots of setup code that makes you question your career choicesAfter getDeps (The Enlightenment)
// ✅ Easy to test - dependencies are injected with elegance
function getDeps() {
return {
logger: console.log,
database: getDatabase(),
api: getApiClient(),
};
}
function processUser(userId, _getDeps = getDeps) {
const { logger, database, api } = _getDeps();
logger(`Processing user ${userId}`);
const user = database.getUser(userId);
return api.updateUser(user);
}
// Testing is trivial - like butter on warm toast
function testProcessUser() {
const mockDeps = {
logger: jest.fn(),
database: { getUser: jest.fn().mockReturnValue({ id: '123' }) },
api: { updateUser: jest.fn().mockReturnValue({ success: true }) },
};
const result = processUser('123', () => mockDeps);
expect(result).toEqual({ success: true });
}Advanced Testing Patterns
Mixed Mocking & Integration Testing
Here's where getDeps really shines—the ability to mix real and mock dependencies like a master chef combining ingredients. This is the testing equivalent of having your cake and eating it too.
function getDeps() {
return {
logger: console.log,
database: getDatabase(), // Real database
api: getApiClient(), // Real API
cache: getCache(), // Real cache
analytics: getAnalytics(), // Real analytics
};
}
function processUser(userId, _getDeps = getDeps) {
const { logger, database, api, cache, analytics } = _getDeps();
logger(`Processing user ${userId}`);
const user = database.getUser(userId);
const result = api.updateUser(user);
cache.set(`user:${userId}`, result);
analytics.track('user_updated', { userId });
return result;
}
// Integration test: Real database + API, mock analytics
function testProcessUserIntegration() {
const mixedDeps = {
logger: jest.fn(),
database: getDatabase(), // Real database
api: getApiClient(), // Real API
cache: getCache(), // Real cache
analytics: { track: jest.fn() }, // Mock analytics
};
const result = processUser('123', () => mixedDeps);
expect(result).toBeDefined();
expect(mixedDeps.analytics.track).toHaveBeenCalledWith('user_updated', { userId: '123' });
}Testing Complex Async Patterns
function getDeps() {
return {
logger: console.log,
database: getDatabase(),
queue: getQueue(),
email: getEmailService(),
};
}
async function processOrder(orderId, _getDeps = getDeps) {
const { logger, database, queue, email } = _getDeps();
logger(`Processing order ${orderId}`);
// Complex async workflow
const order = await database.getOrder(orderId);
await queue.add('process-payment', { orderId });
await email.sendOrderConfirmation(order.email);
return { success: true, orderId };
}
// Test complex async patterns easily
async function testProcessOrder() {
const mockDeps = {
logger: jest.fn(),
database: {
getOrder: jest.fn().mockResolvedValue({ id: '123', email: '[email protected]' })
},
queue: {
add: jest.fn().mockResolvedValue(true)
},
email: {
sendOrderConfirmation: jest.fn().mockResolvedValue(true)
},
};
const result = await processOrder('123', () => mockDeps);
expect(result).toEqual({ success: true, orderId: '123' });
expect(mockDeps.queue.add).toHaveBeenCalledWith('process-payment', { orderId: '123' });
}Common Questions & Criticisms
We've heard them all. Here are the most common questions and our thoughtful responses.
Q: "Why not just use a dependency injection container?"
A: Ah, the classic DI container question! While DI containers are like Swiss Army knives—powerful but often overkill—getDeps is more like a perfectly crafted chef's knife: simple, elegant, and gets the job done beautifully.
getDeps is:
- Framework agnostic - works with any JavaScript environment (no vendor lock-in)
- Zero configuration - no setup required (because life is complicated enough)
- Explicit dependencies - you can see exactly what each function needs (no hidden surprises)
- Simple to understand - no magic or hidden behavior (what you see is what you get)
Q: "Default parameters seem hacky. Why not constructor injection?"
A: "Hacky" is such a strong word! Let's call it "pragmatically elegant." Constructor injection is wonderful for classes, but most modern JavaScript code is functional. getDeps embraces this reality with style.
getDeps:
- Works with functions - no need to create classes just for DI (because not everything needs to be a class)
- Maintains functional purity - no side effects or state (pure as the driven snow)
- Follows JavaScript conventions - default parameters are a standard feature (we're not reinventing the wheel)
- Reduces boilerplate - no need for constructors or factory functions (less code, more joy)
Q: "Won't this create too many parameters in my functions?"
A: Quite the opposite! The _getDeps parameter is the only additional parameter you need. It's like having a Swiss Army knife instead of carrying around a toolbox.
Compare:
// ❌ Traditional approach - multiple parameters (parameter soup)
function processUser(userId, logger, database, api, cache, analytics) {
// ... implementation
}
// ✅ getDeps approach - single additional parameter (elegant simplicity)
function processUser(userId, _getDeps = getDeps) {
const { logger, database, api, cache, analytics } = _getDeps();
// ... implementation
}Q: "What about performance? Creating objects on every call?"
A: Ah, the performance question—every developer's favorite concern! Modern JavaScript engines are like Olympic athletes: incredibly optimized for object creation. The performance impact is so negligible it's like worrying about the weight of a feather on a sumo wrestler.
- Microseconds vs milliseconds - object creation is extremely fast (faster than you can blink)
- Memory pressure is minimal - objects are small and short-lived (like mayflies, but more useful)
- Garbage collection friendly - objects are immediately eligible for cleanup (no memory leaks here)
- Profiling shows no impact - in real applications, this is never a bottleneck (we've checked)
Q: "How is this different from just passing dependencies as parameters?"
A: Excellent question! Passing dependencies as parameters is like trying to solve a Rubik's cube with one hand tied behind your back—possible, but unnecessarily difficult.
// ❌ Parameter passing approach (the dependency soup)
function processUser(userId, logger, database, api) {
// What if you need to add a new dependency?
// You have to update every call site (maintenance nightmare)
}
// ❌ Object parameter approach (the mystery box)
function processUser(userId, deps) {
// No type safety, no clear contract
// Easy to forget required dependencies (like forgetting your keys)
}
// ✅ getDeps approach (the elegant solution)
function processUser(userId, _getDeps = getDeps) {
const { logger, database, api } = _getDeps();
// Clear contract, type-safe, easy to extend (like a well-designed API)
}The getDeps Pattern
In essence, the getDeps pattern is a dependency injection technique that transforms your code from a testing nightmare into a testing dream.
- Uses default parameters for dependency injection (elegant and simple)
- Keeps dependencies at the top level of functions (clean and organized)
- Makes testing easier by allowing dependency mocking (because testing should be joyful)
- Prevents unused dependencies from being returned (no clutter, no waste)
- Enables mixed testing strategies - unit, integration, and end-to-end (testing flexibility)
- Maintains code clarity with explicit dependency contracts (what you see is what you get)
When NOT to Use getDeps
As with any good thing in life, moderation is key. Here's when getDeps might be overkill:
Framework-Level Dependencies
❌ Don't use getDeps for:
// Framework modules that Jest handles beautifully
import React from 'react';
import { render, screen } from '@testing-library/react';
import { useRouter } from 'next/router';
// Third-party libraries with good mocking support
import axios from 'axios';
import lodash from 'lodash';
import moment from 'moment';✅ Use Jest mocks instead:
// Jest handles these elegantly
jest.mock('axios');
jest.mock('lodash');
jest.mock('next/router', () => ({
useRouter: () => ({ push: jest.fn() })
}));Simple Utility Functions
❌ Overkill:
// This is just a pure function - no need for DI
function formatCurrency(amount, _getDeps = getDeps) {
const { currencyFormatter } = _getDeps();
return currencyFormatter.format(amount);
}✅ Better approach:
// Simple, pure, and testable
function formatCurrency(amount, locale = 'en-US') {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
}).format(amount);
}When You're Just Passing Through
❌ Unnecessary complexity:
function processData(data, _getDeps = getDeps) {
const { logger } = _getDeps();
logger('Processing data');
return data.map(item => item.toUpperCase()); // No external dependencies!
}✅ Keep it simple:
function processData(data) {
console.log('Processing data'); // Or use a simple logger
return data.map(item => item.toUpperCase());
}The Golden Rule
Use getDeps when you have:
- External service calls (APIs, databases, file systems)
- Complex business logic with multiple dependencies
- Functions that need different behavior in tests
- Integration points that benefit from mixed real/mock testing
Skip getDeps when you have:
- Framework dependencies (React, Vue, Express)
- Pure utility functions with no external dependencies
- Simple data transformations without side effects
- Third-party libraries that Jest mocks handle well
Remember: getDeps is a Tool, Not a Religion
The goal is better code, not more getDeps. Sometimes the simplest solution is the best solution.
Contributing
We love contributors! Here's how you can help make getDeps even better:
- Fork the repository (because sharing is caring)
- Create a feature branch (because organization is key)
- Make your changes (because innovation never stops)
- Add tests (because quality matters)
- Submit a pull request (because collaboration makes us stronger)
Development Workflow
This project uses conventional commits and automatic version bumping:
Making Changes:
# Make your changes
git add .
git commit -m "feat: add new configuration option" # Use conventional commit formatVersion Bumping:
# After committing, bump version based on commit type
npm run bumpConventional Commit Types:
feat:- New features (bumps minor version)fix:- Bug fixes (bumps patch version)docs:- Documentation changes (bumps patch version)style:- Code style changes (bumps patch version)refactor:- Code refactoring (bumps patch version)test:- Adding tests (bumps patch version)chore:- Maintenance tasks (bumps patch version)
Example Workflow:
# 1. Make changes
git add .
git commit -m "fix: resolve false positive in test files"
# 2. Bump version (automatically detects 'fix:' and bumps patch)
npm run bump
# 3. Push changes
git pushReporting Issues
Found a bug or have a feature request? We'd love to hear from you!
- 🐛 Report bugs: Create an issue
- 💡 Suggest features: Open a feature request
- ❓ Ask questions: Start a discussion
License
MIT - Because we believe in freedom, both in code and in life.
