Client
Learn how to use Probitas clients to interact with various services and protocols. This guide covers available clients, common patterns, and best practices for effective testing.
Overview
Probitas provides unified client APIs for connecting to external services during scenario testing. All clients share common patterns:
- Unified namespace: Access all clients via
client.* - Automatic cleanup: Clients implement
AsyncDisposablefor resource management - Consistent options: Common settings like timeout, retry, and abort signal
- Built-in assertions: Use
expect()for response validation
import { client, expect, scenario } from "jsr:@probitas/probitas";
export default scenario("API Test")
.resource("http", () =>
client.http.createHttpClient({
url: "http://localhost:8080",
}))
.step("Make request", async (ctx) => {
const { http } = ctx.resources;
const res = await http.get("/health");
expect(res).toBeOk();
})
.build();
Common Options
All clients accept common options like timeout, signal, and retry. See
Configuration for detailed retry
settings.
Resource Lifecycle
Register clients as resources for automatic lifecycle management. Resources are disposed in reverse order after the scenario completes.
import { client, expect, scenario } from "jsr:@probitas/probitas";
scenario("Example")
.resource(
"http",
() => client.http.createHttpClient({ url: "http://localhost:8080" }),
)
.step(() => {})
.build();
// Resource automatically disposed when scenario ends
For manual control outside scenarios, use await using:
import { client } from "jsr:@probitas/probitas";
await using http = client.http.createHttpClient({
url: "http://localhost:8080",
});
// Automatically closed when scope exits
HTTP Client
The HTTP client provides a fluent API for making HTTP requests with built-in JSON handling and response assertions.
import { client } from "jsr:@probitas/probitas";
await using http = client.http.createHttpClient({
url: "http://localhost:8080",
headers: { "Content-Type": "application/json" },
throwOnError: true,
});
See Configuration for all options.
Making Requests
The client supports all standard HTTP methods:
import { client } from "jsr:@probitas/probitas";
await using http = client.http.createHttpClient({
url: "http://localhost:8080",
});
// GET with query parameters
const res1 = await http.get("/users", {
query: { page: 1, limit: 10 },
});
// POST with JSON body
const res2 = await http.post("/users", {
body: {
name: "Alice",
email: "alice@example.com",
},
});
// PUT, PATCH, DELETE
await http.put("/users/1", { body: { name: "Alice Updated" } });
await http.patch("/users/1", {
body: { email: "new@example.com" },
});
await http.delete("/users/1");
Override client settings per request:
import { client } from "jsr:@probitas/probitas";
await using http = client.http.createHttpClient({
url: "http://localhost:8080",
});
// Custom headers for authenticated request
const res1 = await http.get("/protected", {
headers: { Authorization: "Bearer token123" },
});
// Disable error throwing for expected failures
const res2 = await http.get("/maybe-404", {
throwOnError: false,
});
if (!res2.ok) {
console.log("Status:", res2.status);
}
Assertions
Validate responses with chainable assertions:
import { client, expect } from "jsr:@probitas/probitas";
await using http = client.http.createHttpClient({
url: "http://localhost:8080",
});
const res = await http.get("/users/1");
expect(res)
.toBeOk() // Status 2xx
.toHaveStatus(200) // Exact status code
.toHaveHeadersPropertyContaining("content-type", "application/json") // Content-Type contains
.toHaveJsonMatching({ name: "Alice" }) // Partial JSON match
.toHaveDurationLessThan(1000); // Response time limit
// Additional assertions examples
const res2 = await http.get("/error", { throwOnError: false });
expect(res2).not.toBeOk(); // Status not 2xx
expect(res).toHaveHeadersProperty("X-Request-Id");
expect(res).toHaveTextContaining("success");
expect(res).toHaveJsonPresent(); // Check if response has JSON data
SQL Clients
Probitas supports multiple SQL databases with a consistent query interface.
| Database | Client Factory |
|---|---|
| PostgreSQL | `client.sql.postgres.createPostgresClient()` |
| MySQL | `client.sql.mysql.createMySqlClient()` |
| SQLite | `client.sql.sqlite.createSqliteClient()` |
| DuckDB | `client.sql.duckdb.createDuckDbClient()` |
import { client } from "jsr:@probitas/probitas";
await using pg = await client.sql.postgres.createPostgresClient({
url: "postgres://testuser:testpass@localhost:5432/testdb",
});
See Configuration for all options.
Queries
Run queries with parameterized values:
import { client } from "jsr:@probitas/probitas";
await using pg = await client.sql.postgres.createPostgresClient({
url: "postgres://testuser:testpass@localhost:5432/testdb",
});
// Simple query
const result1 = await pg.query("SELECT * FROM users");
// Parameterized query (type-safe)
const userId = 1;
const result2 = await pg.query<{ id: number; name: string }>(
"SELECT * FROM users WHERE id = $1",
[userId],
);
// Access results
const allRows = result2.rows ?? []; // All rows
const first = result2.rows?.[0]; // First row
const rowCount = result2.rowCount; // Row count
Transactions
Wrap multiple queries in a transaction:
import { client } from "jsr:@probitas/probitas";
await using pg = await client.sql.postgres.createPostgresClient({
url: "postgres://testuser:testpass@localhost:5432/testdb",
});
const result = await pg.transaction(async (tx) => {
const insert = await tx.query<{ id: number }>(
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
["Alice", "alice@example.com"],
);
await tx.query(
"INSERT INTO profiles (user_id, bio) VALUES ($1, $2)",
[insert.rows?.[0]!.id, "Hello!"],
);
return insert.rows?.[0];
});
Assertions
Validate query results:
import { client, expect } from "jsr:@probitas/probitas";
await using pg = await client.sql.postgres.createPostgresClient({
url: "postgres://testuser:testpass@localhost:5432/testdb",
});
const result = await pg.query("SELECT * FROM users WHERE name = $1", ["Alice"]);
expect(result)
.toBeOk()
.toHaveRowCount(1)
.toHaveRowsMatching({ name: "Alice" });
// Match multiple rows
const allResult = await pg.query("SELECT * FROM users");
expect(allResult).toHaveRowsMatching([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
]);
// Additional row assertions
expect(result).not.toHaveRowsEmpty(); // Check if query returned any rows
expect(result).toHaveRowCountGreaterThan(0); // More than 0 rows
expect(result).toHaveRowCountLessThanOrEqual(10); // At most 10 rows
gRPC Client
The gRPC client supports unary calls, server streaming, client streaming, and bidirectional streaming.
import { client } from "jsr:@probitas/probitas";
await using grpc = client.grpc.createGrpcClient({
url: "localhost:50051",
metadata: { authorization: "Bearer token" },
});
See Configuration for all options.
Unary Calls
Standard request-response pattern:
import { client, expect } from "jsr:@probitas/probitas";
await using grpc = client.grpc.createGrpcClient({ url: "localhost:50051" });
const res = await grpc.call("echo.EchoService", "Echo", {
message: "Hello",
});
expect(res).toBeOk().toHaveDataMatching({ message: "Hello" });
// Note: For gRPC responses, use toHaveDataMatching (not toHaveJsonMatching)
const data = res.data;
Server Streaming
Receive multiple responses from a single request:
import { client, expect } from "jsr:@probitas/probitas";
await using grpc = client.grpc.createGrpcClient({ url: "localhost:50051" });
const messages: unknown[] = [];
for await (
const res of grpc.serverStream("echo.EchoService", "ServerStream", {
count: 3,
})
) {
expect(res).toBeOk();
messages.push(res.data);
}
Client Streaming
Send multiple requests, receive a single response:
import { client, expect } from "jsr:@probitas/probitas";
await using grpc = client.grpc.createGrpcClient({ url: "localhost:50051" });
const res = await grpc.clientStream(
"echo.EchoService",
"ClientStream",
(async function* () {
yield { message: "First" };
yield { message: "Second" };
yield { message: "Third" };
})(),
);
expect(res).toBeOk();
Bidirectional Streaming
Stream in both directions simultaneously:
import { client, expect } from "jsr:@probitas/probitas";
await using grpc = client.grpc.createGrpcClient({ url: "localhost:50051" });
for await (
const res of grpc.bidiStream(
"echo.EchoService",
"BidiStream",
(async function* () {
yield { message: "Ping 1" };
yield { message: "Ping 2" };
})(),
)
) {
expect(res).toBeOk();
console.log("Received:", res.data);
}
ConnectRPC Client
The ConnectRPC client supports Connect, gRPC, and gRPC-Web protocols with a unified API.
import { client } from "jsr:@probitas/probitas";
await using connect = client.connectrpc.createConnectRpcClient({
url: "localhost:8080",
});
Unary Calls
import { client, expect } from "jsr:@probitas/probitas";
await using connect = client.connectrpc.createConnectRpcClient({
url: "localhost:8080",
});
const res = await connect.call("echo.EchoService", "Echo", {
message: "Hello",
});
expect(res).toBeOk().toHaveDataMatching({ message: "Hello" });
// Note: For gRPC responses, use toHaveDataMatching (not toHaveJsonMatching)
Server Streaming
import { client, expect } from "jsr:@probitas/probitas";
await using connect = client.connectrpc.createConnectRpcClient({
url: "localhost:8080",
});
for await (
const res of connect.serverStream("echo.EchoService", "ServerStream", {
count: 3,
})
) {
expect(res).toBeOk();
console.log("Received:", res.data);
}
GraphQL Client
The GraphQL client provides methods for queries, mutations, and subscriptions.
import { client } from "jsr:@probitas/probitas";
await using graphql = client.graphql.createGraphqlClient({
url: "http://localhost:4000/graphql",
headers: { Authorization: "Bearer token" },
});
See Configuration for all options.
Queries
Fetch data with GraphQL queries:
import { client, expect, outdent } from "jsr:@probitas/probitas";
await using graphql = client.graphql.createGraphqlClient({
url: "http://localhost:4000/graphql",
});
const res = await graphql.query(
outdent`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
{ id: "1" },
);
expect(res).toBeOk().toHaveDataMatching({
user: { name: "Alice" },
});
const user =
res.data<{ user: { id: string; name: string; email: string } }>()!.user;
Mutations
Modify data with GraphQL mutations:
import { client, expect, outdent } from "jsr:@probitas/probitas";
await using graphql = client.graphql.createGraphqlClient({
url: "http://localhost:4000/graphql",
});
const res = await graphql.mutation(
outdent`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
}
}
`,
{ input: { name: "Alice", email: "alice@example.com" } },
);
expect(res).toBeOk();
const newUser =
res.data<{ createUser: { id: string; name: string } }>()!.createUser;
Subscriptions
Listen for real-time updates:
import { client, expect, outdent } from "jsr:@probitas/probitas";
await using graphql = client.graphql.createGraphqlClient({
url: "http://localhost:4000/graphql",
});
const subscription = graphql.subscribe(outdent`
subscription OnUserCreated {
userCreated {
id
name
}
}
`);
for await (const res of subscription) {
expect(res).toBeOk();
console.log(
"New user:",
res.data<{ userCreated: { id: string; name: string } }>()!.userCreated,
);
}
Error and Extension Assertions
Check for GraphQL errors and extensions:
import { client, expect } from "jsr:@probitas/probitas";
await using graphql = client.graphql.createGraphqlClient({
url: "http://localhost:4000/graphql",
});
const res = await graphql.query("query { user(id: 1) { name } }");
// Check for errors
expect(res).toHaveErrorPresent(); // Has GraphQL errors
expect(res).toHaveError({ message: "Not found" }); // Specific error
// Or check for no errors
expect(res).toHaveErrorNullish(); // No GraphQL errors
// Check for extensions
expect(res).toHaveExtensionsProperty("tracing"); // Has tracing extension
expect(res).toHaveExtensionsPropertyContaining("tracing", { version: 1 }); // Extension with value
Redis Client
The Redis client provides operations for strings, hashes, lists, and sets.
import { client } from "jsr:@probitas/probitas";
await using redis = await client.redis.createRedisClient({
url: "redis://localhost:6379",
});
See Configuration for all options.
Operations
Common Redis operations:
import { client, expect } from "jsr:@probitas/probitas";
await using redis = await client.redis.createRedisClient({
url: "redis://localhost:6379",
});
// Strings
await redis.set("key", "value");
await redis.set("key", "value", { ex: 3600 }); // With TTL
const result = await redis.get("key");
expect(result).toBeOk().toHaveValue("value");
// Hashes
await redis.hset("user:1", "name", "Alice");
await redis.hset("user:1", "age", "30");
const user = await redis.hgetall("user:1");
// Lists
await redis.lpush("queue", ["task1", "task2"]);
const task = await redis.rpop("queue");
// Sets
await redis.sadd("tags", ["typescript", "deno"]);
const tags = await redis.smembers("tags");
// Delete
await redis.del(["key"]);
// Value assertions
expect(result).toHaveValuePresent(); // Check if value exists
expect(result).toHaveValue("expected"); // Match exact value
expect(result).toHaveValueContaining("substring"); // Value contains substring
// expect(result).toHaveValueCount(5); // String length or collection size
MongoDB Client
The MongoDB client provides document operations with a familiar API.
import { client } from "jsr:@probitas/probitas";
await using mongo = await client.mongodb.createMongoClient({
url: "mongodb://localhost:27017",
database: "testdb",
});
See Configuration for all options.
Operations
Work with collections and documents:
import { client, expect } from "jsr:@probitas/probitas";
interface User {
name: string;
email: string;
age?: number;
}
await using mongo = await client.mongodb.createMongoClient({
url: "mongodb://localhost:27017",
database: "testdb",
});
const users = mongo.collection<User>("users");
// Insert
const result = await users.insertOne({
name: "Alice",
email: "alice@example.com",
});
expect(result).toBeOk();
// Find
const user = await users.findOne({ _id: result.insertedId });
expect(user).toBeOk().toHaveDocMatching({ name: "Alice" });
// Find many
const allUsersResult = await users.find({ age: { $gte: 18 } });
const allUsers = allUsersResult.docs;
// Update
await users.updateOne(
{ _id: result.insertedId },
{ $set: { name: "Bob" } },
);
// Delete
const deleteResult = await users.deleteOne({ _id: result.insertedId });
// Document assertions
expect(result).toHaveInsertedId(result.insertedId); // Check inserted ID
expect(allUsersResult).toHaveDocsCount(5); // Exactly 5 documents
expect(allUsersResult).toHaveDocsMatching([{ name: "Alice" }]); // Match multiple documents
expect(deleteResult).toHaveDeletedCount(1); // 1 document deleted
Deno KV Client
The Deno KV client provides access to Deno's built-in key-value store.
import { client } from "jsr:@probitas/probitas";
await using kv = await client.deno_kv.createDenoKvClient();
By default, an in-memory database is used for testing.
Operations
import { client, expect } from "jsr:@probitas/probitas";
await using kv = await client.deno_kv.createDenoKvClient();
// Set and get
await kv.set(["users", "1"], { name: "Alice" });
const result = await kv.get(["users", "1"]);
expect(result).toBeOk().toHaveValue({ name: "Alice" });
// List by prefix
const listResult = await kv.list({ prefix: ["users"] });
for (const entry of listResult.entries) {
console.log(entry.key, entry.value);
}
// Atomic operations
const atomic = kv.atomic();
atomic
.check({ key: ["users", "1"], versionstamp: null })
.set(["users", "1"], { name: "Alice" });
const commitResult = await atomic.commit();
// Delete
await kv.delete(["users", "1"]);
RabbitMQ Client
The RabbitMQ client provides AMQP messaging for publish/subscribe patterns.
import { client } from "jsr:@probitas/probitas";
await using rabbitmq = await client.rabbitmq.createRabbitMqClient({
url: "amqp://guest:guest@localhost:5672",
});
See Configuration for all options.
Operations
import { client, expect } from "jsr:@probitas/probitas";
await using rabbitmq = await client.rabbitmq.createRabbitMqClient({
url: "amqp://guest:guest@localhost:5672",
});
const channel = await rabbitmq.channel();
// Declare queue
await channel.assertQueue("my-queue", { durable: false });
// Send message
const content = new TextEncoder().encode(JSON.stringify({ message: "Hello" }));
await channel.sendToQueue("my-queue", content);
// Receive message
const result = await channel.get("my-queue");
expect(result).toBeOk();
if (result.message) {
await channel.ack(result.message);
}
await channel.close();
SQS Client
The AWS SQS client provides cloud message queue operations.
import { client } from "jsr:@probitas/probitas";
await using sqs = await client.sqs.createSqsClient({
url: "http://localhost:4566", // LocalStack or AWS endpoint
region: "us-east-1",
credentials: {
accessKeyId: "test",
secretAccessKey: "test",
},
});
See Configuration for all options.
Operations
import { client, expect } from "jsr:@probitas/probitas";
await using sqs = await client.sqs.createSqsClient({
url: "http://localhost:4566",
region: "us-east-1",
credentials: { accessKeyId: "test", secretAccessKey: "test" },
});
// Ensure queue exists
await sqs.ensureQueue("my-queue");
// Send message
const result = await sqs.send(JSON.stringify({ event: "user.created" }));
expect(result).toBeOk().toHaveMessageId(result.messageId);
// Receive and process
const receiveResult = await sqs.receive({ maxMessages: 10 });
for (const msg of receiveResult.messages ?? []) {
console.log("Received:", msg.body);
await sqs.delete(msg.receiptHandle);
}
Available Clients
| Client | Factory Function | Use Case |
|---|---|---|
| HTTP | `client.http.createHttpClient()` | REST APIs, webhooks |
| PostgreSQL | `client.sql.postgres.createPostgresClient()` | PostgreSQL databases |
| MySQL | `client.sql.mysql.createMySqlClient()` | MySQL databases |
| SQLite | `client.sql.sqlite.createSqliteClient()` | Embedded databases |
| DuckDB | `client.sql.duckdb.createDuckDbClient()` | Analytics databases |
| gRPC | `client.grpc.createGrpcClient()` | gRPC services |
| ConnectRPC | `client.connectrpc.createConnectRpcClient()` | Connect/gRPC-Web |
| GraphQL | `client.graphql.createGraphqlClient()` | GraphQL APIs |
| Redis | `client.redis.createRedisClient()` | Cache, pub/sub |
| MongoDB | `client.mongodb.createMongoClient()` | Document databases |
| Deno KV | `client.deno_kv.createDenoKvClient()` | Deno KV store |
| RabbitMQ | `client.rabbitmq.createRabbitMqClient()` | AMQP message queues |
| SQS | `client.sqs.createSqsClient()` | AWS message queues |
Best Practices
Register Clients as Resources
Always register clients as resources for automatic cleanup:
import { client, expect, scenario } from "jsr:@probitas/probitas";
// Good - automatic lifecycle management
scenario("Example")
.resource(
"http",
() => client.http.createHttpClient({ url: "http://localhost:8080" }),
)
.step(() => {})
.build();
// Avoid - manual cleanup required
scenario("Bad Example")
.step("Make request", async () => {
const http = client.http.createHttpClient({ url: "http://localhost:8080" });
// Must manually dispose
})
.build();
Use Environment-Driven Endpoints
Parameterize service URLs so scenarios run consistently across environments and avoid hard-coded localhost values.
import { client, expect, scenario } from "jsr:@probitas/probitas";
const apiUrl = Deno.env.get("API_URL") ?? "http://localhost:8080";
scenario("API check")
.resource("http", () => client.http.createHttpClient({ url: apiUrl }))
.step("Ping service", async (ctx) => {
const res = await ctx.resources.http.get("/health");
expect(res).toBeOk();
})
.build();
Use Type Parameters
Provide type parameters for type-safe responses:
import { client } from "jsr:@probitas/probitas";
await using pg = await client.sql.postgres.createPostgresClient({
url: "postgres://testuser:testpass@localhost:5432/testdb",
});
const userId = 1;
// Good - typed response
const result1 = await pg.query<{ id: number; name: string }>(
"SELECT id, name FROM users WHERE id = $1",
[userId],
);
const user = result1.rows?.[0]; // Type: { id: number; name: string } | undefined
// Avoid - untyped response
const result2 = await pg.query("SELECT * FROM users");
Handle Errors Appropriately
Use assertions for expected successes, explicit checks for expected failures:
import { client, expect } from "jsr:@probitas/probitas";
await using http = client.http.createHttpClient({
url: "http://localhost:8080",
});
// Expected success - use assertions
const res1 = await http.get("/users/1");
expect(res1).toBeOk().toHaveStatus(200);
// Expected failure - disable throwing, check manually
const res2 = await http.get("/users/nonexistent", { throwOnError: false });
expect(res2).toHaveStatus(404);
Configure Retries
Use retry configuration for network-dependent operations. See Configuration for all retry options.
import { client, expect, scenario } from "jsr:@probitas/probitas";
scenario("Retry Example")
.resource(
"http",
() => client.http.createHttpClient({ url: "http://localhost:8080" }),
)
.step(
"External API call",
async (ctx) => {
const { http } = ctx.resources;
const res = await http.get("/external-api", {
retry: { maxAttempts: 3, backoff: "exponential" },
});
expect(res).toBeOk();
},
{
timeout: 10000, // Allow time for retries
},
)
.build();
