Testing Guide
Strategies and examples for testing rate-limited applications.
Unit Testing the Limiter
import { describe, it, expect, beforeEach } from 'vitest'
import { hitlimit, memoryStore } from '@joint-ops/hitlimit'
describe('Rate Limiter', () => {
let store: ReturnType<typeof memoryStore>
let limiter: ReturnType<typeof hitlimit>
beforeEach(() => {
store = memoryStore()
limiter = hitlimit({
limit: 3,
window: '1m',
store
})
})
it('allows requests under limit', async () => {
const req = { ip: '127.0.0.1' }
const res = { set: vi.fn() }
const next = vi.fn()
await limiter(req, res, next)
expect(next).toHaveBeenCalled()
})
it('blocks requests over limit', async () => {
const req = { ip: '127.0.0.1' }
const res = {
set: vi.fn(),
status: vi.fn().mockReturnThis(),
json: vi.fn()
}
const next = vi.fn()
// Exhaust the limit
for (let i = 0; i < 3; i++) {
await limiter(req, res, next)
}
// This should be blocked
await limiter(req, res, next)
expect(res.status).toHaveBeenCalledWith(429)
})
}) Integration Testing
import { describe, it, expect } from 'vitest'
import supertest from 'supertest'
import { app } from './app'
describe('API Rate Limiting', () => {
it('returns rate limit headers', async () => {
const res = await supertest(app)
.get('/api/data')
.expect(200)
expect(res.headers['ratelimit-limit']).toBe('100')
expect(res.headers['ratelimit-remaining']).toBeDefined()
expect(res.headers['ratelimit-reset']).toBeDefined()
})
it('returns 429 when rate limited', async () => {
const requests = Array(101).fill(null).map(() =>
supertest(app).get('/api/data')
)
const responses = await Promise.all(requests)
const blocked = responses.filter(r => r.status === 429)
expect(blocked.length).toBeGreaterThan(0)
})
}) Mocking the Store
import { vi } from 'vitest'
const mockStore = {
get: vi.fn(),
increment: vi.fn(),
reset: vi.fn(),
close: vi.fn()
}
// Simulate rate limited state
mockStore.increment.mockResolvedValue({
count: 101,
resetTime: Date.now() + 60000
})
const limiter = hitlimit({
limit: 100,
window: '1m',
store: mockStore
}) Test Helpers
// test/helpers/ratelimit.ts
import { memoryStore } from '@joint-ops/hitlimit'
export function createTestStore() {
return memoryStore({ cleanupInterval: 0 })
}
export async function exhaustLimit(
store: Store,
key: string,
limit: number,
windowMs: number
) {
for (let i = 0; i < limit; i++) {
await store.increment(key, windowMs)
}
}
export async function waitForReset(resetTime: number) {
const waitMs = resetTime - Date.now() + 100
if (waitMs > 0) {
await new Promise(r => setTimeout(r, waitMs))
}
} Skipping Rate Limits in Tests
// Disable rate limiting in test environment
const limiter = hitlimit({
limit: 100,
window: '1m',
skip: () => process.env.NODE_ENV === 'test'
})
// Or use a very high limit
const testLimit = process.env.NODE_ENV === 'test'
? 1000000
: 100
const limiter = hitlimit({
limit: testLimit,
window: '1m'
}) E2E Testing
import { test, expect } from '@playwright/test'
test('shows rate limit error message', async ({ page }) => {
// Make requests until rate limited
for (let i = 0; i < 101; i++) {
await page.goto('/api/test')
}
// Verify error message is displayed
await expect(page.locator('text=Too many requests'))
.toBeVisible()
}) Testing Best Practices
- Use fresh store instances for each test
- Test both success and rate-limited scenarios
- Verify correct headers are returned
- Test custom key extraction functions
- Test skip conditions work as expected
- Use time mocking for window-based tests