Testing: Graph-Based Approach
With Pumped-fn, you test entire dependency graphs, not individual units. Change one node, the entire graph adapts.
The Power of Graph Testing
Your application is a dependency graph. Test it that way:
ts
import { test, expect } from 'vitest'
import { provide, derive, createScope, preset } from '@pumped-fn/core-next'
// Your application dependency graph
const env = provide(() => process.env.NODE_ENV || 'production')
const config = derive(env, (environment) => ({
apiUrl: environment === 'production'
? 'https://api.prod.com'
: 'https://api.test.com',
cache: environment === 'production' ? 'redis' : 'memory',
timeout: environment === 'production' ? 5000 : 100
}))
const httpClient = derive(config, (cfg) => ({
get: async (path: string) => {
const url = `${cfg.apiUrl}${path}`
// In tests, this would use cfg.timeout of 100ms
// In production, this would use 5000ms
return fetch(url, { signal: AbortSignal.timeout(cfg.timeout) })
}
}))
const cache = derive(config, (cfg) => ({
type: cfg.cache,
get: (key: string) => cfg.cache === 'redis'
? `redis:${key}`
: `memory:${key}`
}))
const userRepository = derive([httpClient, cache], (client, cache) => ({
async getUser(id: number) {
const cacheKey = cache.get(`user:${id}`)
// Would check cache first...
return {
source: cache.type,
endpoint: `${client.get}/users/${id}`,
cached: cacheKey
}
}
}))
// Test 1: Change the root, entire graph adapts
test('development environment setup', async () => {
const scope = createScope(
preset(env, 'development')
)
const repo = await scope.resolve(userRepository)
const user = await repo.getUser(123)
// Everything automatically configured for development
expect(user.source).toBe('memory')
expect(user.cached).toBe('memory:user:123')
// API calls would go to test server with 100ms timeout
})
// Test 2: Production environment (isolated scope)
test.concurrent('production environment', async () => {
const scope = createScope(
preset(env, 'production')
)
const repo = await scope.resolve(userRepository)
const user = await repo.getUser(456)
// Completely different configuration, same code
expect(user.source).toBe('redis')
expect(user.cached).toBe('redis:user:456')
// API calls would go to production with 5s timeout
})
// Test 3: Custom scenario - slow network simulation
test.concurrent('slow network scenario', async () => {
const scope = createScope(
// Override just the config, everything downstream updates
preset(config, {
apiUrl: 'https://api.slow.com',
cache: 'memory',
timeout: 30000 // 30 second timeout
})
)
const client = await scope.resolve(httpClient)
// Client automatically uses 30s timeout
const repo = await scope.resolve(userRepository)
// Repository uses slow client and memory cache
})
// Test 4: Failure scenario
test('api failure handling', async () => {
const scope = createScope(
// Mock just the HTTP client
preset(httpClient, {
get: async () => { throw new Error('Network error') }
})
)
const repo = await scope.resolve(userRepository)
// Test error handling with mocked failure
await expect(repo.getUser(1)).rejects.toThrow('Network error')
})
// The key insight: One preset can change the entire behavior
// No need to mock fetch, cache, config separately
// The graph propagates changes automatically
// Each test has an isolated scope - run them all concurrently!
Core Concepts United
1. Graph Propagation
Change the root → entire graph updates. No manual mock wiring.
2. Scope Isolation
Each test gets its own scope. Run all tests concurrently without interference.
3. Preset Power
One preset()
can simulate entire environments. No mock framework needed.
Why Graph Testing Wins
Traditional Approach:
typescript
// Mock everything individually
mockFetch.mockResolvedValue(...)
mockCache.mockImplementation(...)
mockConfig.mockReturnValue(...)
mockDatabase.mockResolvedValue(...)
// Hope they work together correctly
Graph Approach:
typescript
// Change the environment, everything adapts
const scope = createScope(
preset(env, 'test')
)
// Entire system configured for testing
Testing Patterns
typescript
// Test different environments
const prodScope = createScope(preset(env, 'production'))
const testScope = createScope(preset(env, 'test'))
// Test failure scenarios
const failScope = createScope(
preset(httpClient, { get: async () => { throw error } })
)
// Test with specific data
const dataScope = createScope(
preset(database, { users: testData })
)
One line changes entire system behavior. That's graph testing.