Mock Socket.IO without a server.
smocket reimplements Socket.IO's rooms, broadcasts, and acknowledgements in memory — and every release is verified against the real library.
Sweet setup, rocket speed.
- v1.0.0
- dual-run CI
- browser-tested

Three players, one page, no server.
One person draws. The other two watch the lines arrive and say what they think it is. Every stroke and every guess is a Socket.IO event, routed here in the page.
Use your Socket.IO handlers without starting a network server.
import { once } from 'node:events';
import { createServer } from 'node:http';
import type { AddressInfo } from 'node:net';
import { Server } from 'socket.io';
import { io as connect } from 'socket.io-client';
const httpServer = createServer();
const io = new Server(httpServer);
registerApplication(io);
httpServer.listen(0, '127.0.0.1');
await once(httpServer, 'listening');
const { port } = httpServer.address() as AddressInfo;
const url = `http://127.0.0.1:${port}`;
const a = connect(url);
const b = connect(url);
const c = connect(url);
// Then disconnect every client, close Socket.IO,
// and close the HTTP server after the test.import { Server } from 'smocket';
import { connect } from 'smocket-client';
const url = 'http://localhost:3000';
const io = new Server(url);
registerApplication(io);
const a = connect(url);
const b = connect(url);
const c = connect(url);registerApplication(io) stays the same.
Built around Socket.IO behavior.
Delivery fidelity
Rooms and socket ids live in the same bidirectional maps socket.io uses. Fan-out is a set operation, not a loop over guesses.
Checked against the real thing
Oracle-backed conformance cases run against both Socket.IO and smocket. Browser, SharedWorker, and packaging checks cover smocket-specific behavior.
No server, no ports
Nothing binds, nothing listens. Tests start and finish in the same process.
Honest about its limits
What a mock cannot have, smocket does not pretend to have. The list is short and written down.
Switch in three steps.
Install
npm install -D smocket smocket-clientAlias the client
// vitest.config.ts import { defineConfig } from 'vitest/config'; export default defineConfig({ resolve: { alias: { 'socket.io-client': 'smocket-client' }, }, });Run your app code
import { expect, it } from 'vitest'; import { Server, type ServerSocketContract } from 'smocket'; import { joinChat } from '../src/chat'; it('delivers a room message to the other member', async () => { const url = 'http://localhost:3000'; const io = new Server(url); io.on('connection', (socket: ServerSocketContract) => { socket.on('join', (room: string, ack: () => void) => { void socket.join(room); ack(); socket.on('message', (text: string) => { socket.to(room).emit('message', text); }); }); }); // joinChat still imports io from socket.io-client. const a = joinChat(url, 'alice', 'general'); const b = joinChat(url, 'bob', 'general'); await Promise.all([a.ready, b.ready]); const heard = new Promise((resolve) => b.onMessage(resolve)); a.send('hello'); await expect(heard).resolves.toBe('hello'); });
See who received what.
Rooms, exclusions, and targeted emits follow the supported delivery semantics verified against Socket.IO. Here is the delivery record.
- A
- B
- C
Scope
What smocket does
- Rooms and namespaces
- Broadcast, with and without exclusions
- Targeted emits by socket id
- Acknowledgements, including timeouts
- Middleware, handshake, and per-socket data
- Disconnect cleanup
What a mock cannot have
- Reconnection behaviour — there is no "later" to wait for
- Transport fallback — there is no transport
- Heartbeat — there is no connection to check
- Multi-server adapters — there is one process
- Binary encoding — nothing is serialised