
Tap Test
- 3 installs
- 8 repo stars
- Updated February 13, 2026
- aviz85/tap-test-skill
tap-test is a Claude skill that generates HTTP-level integration and e2e tests that exercise a live API server and real database like a real client, verifying responses and database state.
About
This skill generates production-grade HTTP-level integration and e2e tests that call a live API and a real database like an actual client would. A developer uses it to test the real stack instead of mocked function calls, spinning up a lightweight Fastify server, sending real requests, and verifying both responses and database state. It mocks only external services such as a messaging API or LLM.
- Generates HTTP-level integration/e2e tests that hit a real Fastify server and real database, not mocks
- Spins up a test server on port 3999 with send/state/history/reset endpoints and captures responses via EventEmitter
- Ships an isolation pattern with unique prefixes plus cleanup before and after each test
Tap Test by the numbers
- 3 all-time installs (skills.sh)
- Ranked #1,647 of 2,154 Testing & QA skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
tap-test capabilities & compatibility
- Capabilities
- testing · api development
- Works with
- supabase
- Use cases
- testing · api development
What tap-test says it does
Create real API/HTTP integration tests that simulate actual client behavior. Spins up a test server, sends real HTTP requests, captures responses, verifies DB state, cleans up.
npx skills add https://github.com/aviz85/tap-test-skill --skill tap-testAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 8 |
| Last updated | February 13, 2026 |
| Repository | aviz85/tap-test-skill ↗ |
What it does
Generate HTTP-level integration and e2e tests that run against a live API server and real database.
Who is it for?
Testing an API against a real server and database instead of mocked function calls
Skip if: Unit tests that mock core logic
When should I use this skill?
You need an api test, integration test, e2e test, or http test against a real database
What you get
A suite of HTTP-level integration tests running against a real Fastify server and database with isolated test data and cleanup.
- HTTP-level integration/e2e test files
- Test server, cleanup and seed helpers
- Vitest config
By the numbers
- 6-step step-by-step process
- 6 test categories to cover
- Test server runs on port 3999
Files
Tap Test - Real API Integration Test Generator
Generate production-grade HTTP-level integration/e2e tests that simulate real client behavior against a live API + real database.
What is a Tap Test?
A "tap test" taps into your API exactly like a real client would:
- Real HTTP requests (not function calls)
- Real database (not mocks)
- Real server (lightweight Fastify instance)
- Real responses captured via EventEmitter or HTTP endpoints
- Real cleanup after each test
Pattern
1. beforeAll: cleanup DB → seed test data → start Fastify server
2. beforeEach: reset test users + clear captured responses
3. test: POST to API → wait for responses → verify via HTTP state endpoint + DB queries
4. afterAll: cleanup DB → stop serverArchitecture
Test File
├── Fastify test server (port 3999)
│ ├── POST /api/send → simulate incoming message
│ ├── GET /api/state/:id → read user/session state
│ ├── GET /api/chat/:id → read conversation history
│ └── DELETE /api/user/:id → reset user for next test
├── EventEmitter capture → collect outgoing responses
├── Direct DB queries → verify transactions, records
└── Cleanup helpers → isolated test data (prefix/marker pattern)Step-by-Step Process
1. Explore the project
- Find the main message processing function (router, handler, engine)
- Find the gateway/provider layer (WhatsApp, Slack, etc.)
- Find existing test setup (cleanup, seed functions)
- Identify the DB client and tables
2. Create test data isolation
- Use a phone prefix or unique marker for test data (e.g.,
97259900%) - Use a category/area marker for content data (e.g.,
area='test') - Create
cleanupTestData()that deletes ALL test data across all tables - Create
seedTestData()that inserts minimal required test content
3. Build the test server
import Fastify from 'fastify';
const app = Fastify({ logger: false });
const TEST_PORT = 3999;
// Capture responses via provider's EventEmitter
const captured: Response[] = [];
provider.onResponse((r) => captured.push(r));
// POST endpoint - simulate incoming messages
app.post('/api/send', async (req, reply) => {
const msg = provider.parseWebhook(req.body);
await engine.handleMessage(msg);
return { success: true };
});
// GET endpoint - read state
app.get('/api/state/:phone', async (req) => {
const user = await getUser(req.params.phone);
const session = await getSession(user.id);
return { user, session };
});
// DELETE endpoint - reset user
app.delete('/api/user/:phone', async (req) => {
// cascade delete all user data
});
await app.listen({ port: TEST_PORT, host: '127.0.0.1' });4. Write HTTP helper functions
async function sendText(phone: string, text: string) {
return fetch(`${BASE}/api/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, type: 'text', text }),
});
}
async function waitForResponses(n: number, timeout = 15000) {
const start = Date.now();
while (captured.length < n && Date.now() - start < timeout) {
await new Promise(r => setTimeout(r, 100));
}
return captured;
}
async function getState(phone: string) {
const res = await fetch(`${BASE}/api/state/${phone}`);
return res.json();
}5. Write test cases
Each test should:
clearCaptured()before sending- Send via HTTP (not direct function call)
waitForResponses(n)for async processing- Verify response content
- Verify state via HTTP GET
- Verify DB records via direct Supabase queries
6. Test categories to cover
- Happy path flows (full user journey start to finish)
- Error handling (invalid input, missing data)
- State transitions (verify session state after each step)
- DB verification (transactions, records created)
- Multi-user concurrent (parallel requests)
- Chat history (conversation recorded correctly)
Key Principles
- No mocks for core logic - the whole point is testing the real stack
- Mock only external services (WhatsApp API, LLM) via provider pattern
- Isolated test data - unique prefixes, cleanup before AND after
- Sequential execution - use
singleFork: truein vitest for DB consistency - Generous timeouts - real DB operations take time (10-30s per test)
- Wait, don't assume - use
waitForResponses()pattern for async processing
Vitest Config
// vitest.config.ts
export default defineConfig({
test: {
pool: 'forks',
poolOptions: { forks: { singleFork: true } },
env: { WHATSAPP_PROVIDER: 'mock' }, // or equivalent
},
});Example Test
it('should onboard new user via HTTP', async () => {
clearCaptured();
await sendText(PHONE, 'hello');
const responses = await waitForResponses(1);
expect(responses[0].content).toContain('Welcome');
const state = await getState(PHONE);
expect(state.session.state).toBe('onboarding');
expect(state.user).not.toBeNull();
});.DS_Store
.env
node_modules/
*.log
Tap Test - Real API Integration Tests for AI-Era Development
## 🚨 DANGER: AI Agents Can Damage Your Database
>
When tests fail, AI agents try to remove obstacles. Your database is the obstacle. Agents can and will: disable RLS policies, delete real data, drop tables, widen DELETE statements, or grant themselves permissions - whatever it takes to make tests green.
>
This skill is a prompt. A prompt cannot prevent this. It works well in happy-path scenarios, but when things get hard, no amount of instructions guarantees safe behavior.
>
### Protect Yourself
>
- Use read-only DB credentials. You can run against production if the agent has no write access. Many devs already work with a read-only user - that's safe.
- Or use a separate test database if tests need write access.
- Add [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks) to block destructive commands (DROP,TRUNCATE,ALTER,GRANT).
- Lock down RLS so the agent can't disable it.
- Review generated tests before running them.
>
Bottom line: Don't give the agent write access to data you care about.
The Problem: AI-Generated Tests Are Often Useless
When you ask an AI coding agent to "write tests", you typically get:
- Mock-heavy tests that test nothing real - they verify your mocks work, not your code
- Unit tests for internal functions that pass even when the actual API is broken
- Snapshot tests that just freeze current behavior without understanding it
- Tests that check implementation details instead of actual user-facing behavior
The result? A green test suite that gives you false confidence. Your tests pass, but your API is broken in production because no test ever sent a real HTTP request to a real server with a real database.
"100% test coverage, 0% confidence." - Every team that relied on mocked integration tests
The Solution: Tap Tests
A Tap Test "taps" into your API exactly like a real client would:
| Traditional AI Tests | Tap Tests |
|---|---|
| Mock HTTP layer | Real HTTP requests |
| Mock database | Real database |
| Call functions directly | Real server instance |
| Assert on mock returns | Assert on actual responses + DB state |
| Fast but meaningless | Slightly slower but actually useful |
What Makes This Different
Traditional test: Tap test:
mock(db.query) → return fake data Client → HTTP POST → Real Server → Real DB
call handler(fakeReq) ← HTTP Response ←
assert handler returned something + verify DB state
+ verify side effectsArchitecture
Test File
|
├── Fastify test server (port 3999)
│ ├── POST /api/send → simulate incoming message
│ ├── GET /api/state/:id → read user/session state
│ ├── GET /api/chat/:id → read conversation history
│ └── DELETE /api/user/:id → reset user for next test
│
├── EventEmitter capture → collect outgoing responses
├── Direct DB queries → verify transactions, records
└── Cleanup helpers → isolated test data (prefix/marker)Who Is This For?
Any application with a server that has an API layer - which is the most popular architecture in modern development:
- REST APIs (Express, Fastify, Hono, etc.)
- WhatsApp/Telegram bots with webhook handlers
- SaaS backends with CRUD operations
- Microservices with HTTP communication
- Any system where clients talk to your server via HTTP
If your app has routes that handle requests and interact with a database, tap tests are for you.
Quick Start
1. Install the Skill
Copy the SKILL.md to your Claude Code skills directory:
# Personal (available in all projects)
mkdir -p ~/.claude/skills/tap-test
cp SKILL.md ~/.claude/skills/tap-test/SKILL.md
# Or project-level (available only in this project)
mkdir -p .claude/skills/tap-test
cp SKILL.md .claude/skills/tap-test/SKILL.md2. Use It
In Claude Code, just say:
/tap-testOr describe what you want naturally:
"Write real integration tests for my API"
"Create tap tests for the user registration flow"
"Test my WhatsApp bot with real HTTP requests"Claude will automatically: 1. Explore your project structure 2. Find your routes, handlers, and DB client 3. Create a test server that mirrors your API 4. Write tests that send real HTTP requests 5. Verify both responses AND database state 6. Set up proper cleanup for test isolation
The Pattern
1. beforeAll: cleanup DB → seed test data → start Fastify server
2. beforeEach: reset test users → clear captured responses
3. test: POST to API → wait for responses → verify HTTP state + DB
4. afterAll: cleanup DB → stop serverExample Test
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import Fastify from 'fastify';
const TEST_PORT = 3999;
const BASE = `http://127.0.0.1:${TEST_PORT}`;
const captured: Response[] = [];
// Helper: send a message like a real client
async function sendText(phone: string, text: string) {
return fetch(`${BASE}/api/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, type: 'text', text }),
});
}
// Helper: wait for async responses
async function waitForResponses(n: number, timeout = 15000) {
const start = Date.now();
while (captured.length < n && Date.now() - start < timeout) {
await new Promise(r => setTimeout(r, 100));
}
return captured;
}
// Helper: check state via HTTP (like a real client would)
async function getState(phone: string) {
const res = await fetch(`${BASE}/api/state/${phone}`);
return res.json();
}
describe('User Onboarding Flow', () => {
beforeAll(async () => {
await cleanupTestData();
await seedTestData();
await startTestServer();
});
beforeEach(() => {
captured.length = 0;
});
afterAll(async () => {
await cleanupTestData();
await stopTestServer();
});
it('should onboard new user via HTTP', async () => {
await sendText('972599001234', 'hello');
const responses = await waitForResponses(1);
// Verify response content
expect(responses[0].content).toContain('Welcome');
// Verify state via HTTP
const state = await getState('972599001234');
expect(state.session.state).toBe('onboarding');
// Verify database directly
const { data } = await supabase
.from('users')
.select()
.eq('phone', '972599001234')
.single();
expect(data).not.toBeNull();
expect(data.created_at).toBeDefined();
});
});Key Principles
| Principle | Why |
|---|---|
| No mocks for core logic | The whole point is testing the real stack |
| Mock only external services | WhatsApp API, LLM providers - things you can't control |
| Isolated test data | Use unique prefixes (97259900%), cleanup before AND after |
| Sequential execution | DB consistency requires singleFork: true in Vitest |
| Generous timeouts | Real DB operations take time (10-30s per test) |
| Wait, don't assume | Use waitForResponses() for async processing |
Vitest Configuration
// vitest.config.ts
export default defineConfig({
test: {
pool: 'forks',
poolOptions: { forks: { singleFork: true } },
testTimeout: 30000,
env: { WHATSAPP_PROVIDER: 'mock' },
},
});Test Data Isolation
const TEST_PHONE_PREFIX = '97259900';
async function cleanupTestData() {
// Delete ALL test data across all tables
await supabase.from('messages').delete().like('phone', `${TEST_PHONE_PREFIX}%`);
await supabase.from('sessions').delete().like('phone', `${TEST_PHONE_PREFIX}%`);
await supabase.from('users').delete().like('phone', `${TEST_PHONE_PREFIX}%`);
// Also clean content test data
await supabase.from('content').delete().eq('area', 'test');
}
async function seedTestData() {
await supabase.from('content').insert([
{ area: 'test', title: 'Test Item 1', body: 'Test content' },
{ area: 'test', title: 'Test Item 2', body: 'More test content' },
]);
}What Tests Should Cover
1. Happy path flows - Full user journey from start to finish 2. Error handling - Invalid input, missing data, edge cases 3. State transitions - Verify session state after each step 4. DB verification - Transactions created, records updated 5. Multi-user concurrent - Parallel requests don't interfere 6. Chat history - Conversations recorded correctly
---
Using Skills in Claude Code & Coding Agents
What Are Skills?
Skills are modular, filesystem-based capabilities that extend Claude Code's functionality. They package instructions, metadata, and optional resources that Claude uses automatically when relevant to your task.
How Skills Work
Skills operate on a progressive disclosure model:
| Layer | When Loaded | Content |
|---|---|---|
| Metadata | Always at startup | Skill name + description (~100 tokens) |
| Instructions | When triggered | Full SKILL.md content |
| Resources | On-demand | Supporting files, scripts, templates |
This means you can bundle comprehensive documentation and examples without paying context cost for unused content.
Skill File Structure
my-skill/
├── SKILL.md # Required: instructions + metadata
├── reference.md # Optional: detailed guidance
├── examples.md # Optional: sample outputs
├── templates/ # Optional: templates
└── scripts/ # Optional: executable codeSKILL.md Anatomy
---
name: my-skill
description: "What it does. When to use it."
disable-model-invocation: false # true = only /command triggers it
user-invocable: true # true = user can type /my-skill
allowed-tools: Read, Grep, Bash # tools the skill can use
---
# Instructions for Claude to follow when skill is activeInstalling Skills
# Personal (all projects)
cp -r my-skill ~/.claude/skills/
# Project-level (one project)
cp -r my-skill .claude/skills/
# As a plugin
claude plugin install https://github.com/user/my-skillsImplementing Skills in Your Coding Agent
If you're building a coding agent (with Claude Code, Agent SDK, or API), skills let you:
1. Encapsulate domain knowledge - Package testing strategies, deployment procedures, code review checklists 2. Standardize workflows - Every team member's agent follows the same patterns 3. Share across projects - Personal skills work everywhere, project skills travel with the repo 4. Progressive loading - Only load what's needed, keeping context lean
Best Practices for Skill Authors
1. Single Responsibility - One skill = one focused capability 2. Clear Descriptions - Include keywords users would naturally say 3. Progressive Disclosure - Core in SKILL.md, details in supporting files 4. Include Examples - Show concrete inputs and expected outputs 5. Test Isolation - Skills shouldn't depend on global state
---
Why Tap Tests Matter for AI Development
As AI coding agents write more of our tests, the quality of test instructions matters more than ever. Without clear guidance, AI agents default to:
- Mocking everything (path of least resistance)
- Testing implementation details (fragile, breaks on refactor)
- Generating high coverage with low value (vanity metrics)
Tap Test as a skill solves this by giving the AI agent a clear, opinionated framework for writing tests that actually verify your system works.
When you say /tap-test, the agent doesn't guess - it follows a proven pattern that produces tests you can trust.
---
License
MIT
Author
Created by Aviz - Building tools for the AI-native developer workflow.
Related skills
FAQ
Does tap-test use mocks?
No mocks for core logic; it mocks only external services like the WhatsApp API or the LLM via a provider pattern.
What framework does it use for the test server?
A lightweight Fastify instance listening on port 3999 with send, state, chat-history, and reset endpoints.