Test utils for Flink
npm install @flink-app/test-utilsA comprehensive testing utility library for Flink applications. This package provides helper functions and mocks to simplify writing tests for your Flink handlers, repositories, and plugins.
- HTTP testing utilities with automatic JSON handling
- Mock request objects for unit testing handlers
- No-op authentication plugin for testing
- Type-safe test helpers
- Support for query strings, headers, and authentication
- Integration testing support
- Works with Jasmine, Jest, and other testing frameworks
``bash`
npm install --save-dev @flink-app/test-utils
The HTTP utilities allow you to make test requests to your running Flink app without needing external HTTP clients in most cases.
Initialize the test utilities with your Flink app instance:
`typescript
import { FlinkApp } from "@flink-app/flink";
import * as http from "@flink-app/test-utils";
describe("My API", () => {
let app: FlinkApp
beforeAll(async () => {
app = new FlinkApp
name: "Test App",
port: 3001,
// ... your configuration
});
await app.start();
// Initialize test utilities
http.init(app);
});
afterAll(async () => {
await app.stop();
});
// Your tests here
});
`
`typescript
import { get } from "@flink-app/test-utils";
it("should get all items", async () => {
const response = await get("/items");
expect(response.status).toBe(200);
expect(response.data).toBeDefined();
expect(response.data.items).toBeInstanceOf(Array);
});
`
`typescript
import { post } from "@flink-app/test-utils";
it("should create a new item", async () => {
const newItem = {
name: "Test Item",
description: "A test item",
};
const response = await post("/items", newItem);
expect(response.status).toBe(200);
expect(response.data.item._id).toBeDefined();
expect(response.data.item.name).toBe("Test Item");
});
`
`typescript
import { put } from "@flink-app/test-utils";
it("should update an item", async () => {
const updates = {
name: "Updated Name",
};
const response = await put("/items/123", updates);
expect(response.status).toBe(200);
expect(response.data.item.name).toBe("Updated Name");
});
`
`typescript
import { del } from "@flink-app/test-utils";
it("should delete an item", async () => {
const response = await del("/items/123");
expect(response.status).toBe(200);
expect(response.success).toBe(true);
});
`
All HTTP methods support an optional options object:
`typescript`
const response = await get("/items", {
qs: {
limit: "10",
offset: "0",
category: "electronics",
},
});
// Requests: /items?limit=10&offset=0&category=electronics
`typescript`
const response = await post("/items", newItem, {
headers: {
"X-Custom-Header": "value",
"X-Request-ID": "12345",
},
});
The test utilities support automatic authentication using the app's auth plugin:
`typescript
const user = {
_id: "user123",
username: "testuser",
roles: ["admin"],
};
const response = await get("/admin/users", {
user: user,
});
// Automatically adds: Authorization: Bearer
`
`typescript`
const response = await post(
"/items",
{ name: "New Item" },
{
qs: { draft: "true" },
headers: { "X-Request-ID": "123" },
user: currentUser,
}
);
For unit testing handlers without a running server:
Create a mock FlinkRequest object:
`typescript
import { mockReq } from "@flink-app/test-utils";
it("should handle request correctly", async () => {
const req = mockReq({
body: { name: "Test" },
params: { id: "123" },
query: { include: "details" },
headers: { "content-type": "application/json" },
});
const result = await myHandler(req, ctx);
expect(result.status).toBe(200);
});
`
`typescript
const req = mockReq
body: { name: "Test" }, // Request body
params: { id: "123" }, // URL parameters
query: { page: "1" }, // Query string
headers: { // HTTP headers
"content-type": "application/json",
"authorization": "Bearer token",
},
});
// Mock request automatically provides:
// - req.get(headerName) method
// - JSON serialization of body/params
// - Default empty objects for omitted properties
`
A no-op authentication plugin that allows all requests:
`typescript
import { noOpAuthPlugin } from "@flink-app/test-utils";
const app = new FlinkApp
name: "Test App",
port: 3001,
plugins: [],
});
// Set the no-op auth plugin for testing
app.auth = noOpAuthPlugin();
await app.start();
`
This plugin:
- Always authenticates successfully
- Returns a mock token: "mock-token"
- Useful for testing handlers without setting up real authentication
`typescript
import { FlinkApp } from "@flink-app/flink";
import * as http from "@flink-app/test-utils";
import { mockReq, noOpAuthPlugin } from "@flink-app/test-utils";
describe("Todo API", () => {
let app: FlinkApp
beforeAll(async () => {
app = new FlinkApp
name: "Test App",
port: 3001,
mongo: {
url: "mongodb://localhost:27017/test",
},
});
app.auth = noOpAuthPlugin();
await app.start();
http.init(app);
});
afterAll(async () => {
await app.stop();
});
describe("GET /todos", () => {
it("should return all todos", async () => {
const response = await http.get("/todos");
expect(response.status).toBe(200);
expect(response.data.todos).toBeInstanceOf(Array);
});
it("should filter todos by status", async () => {
const response = await http.get("/todos", {
qs: { status: "completed" },
});
expect(response.status).toBe(200);
expect(response.data.todos.every((t) => t.status === "completed")).toBe(true);
});
});
describe("POST /todos", () => {
it("should create a new todo", async () => {
const newTodo = {
title: "Test Todo",
description: "Test Description",
};
const response = await http.post("/todos", newTodo);
expect(response.status).toBe(200);
expect(response.data.todo._id).toBeDefined();
expect(response.data.todo.title).toBe("Test Todo");
});
it("should require authentication", async () => {
const response = await http.post("/todos", {
title: "Test",
});
// With noOpAuthPlugin, this would pass
// With real auth, this would return 401
expect(response.status).toBe(200);
});
it("should validate required fields", async () => {
const response = await http.post("/todos", {});
expect(response.status).toBe(400);
expect(response.error).toBe("validation_error");
});
});
describe("PUT /todos/:id", () => {
let todoId: string;
beforeEach(async () => {
const created = await http.post("/todos", {
title: "Todo to Update",
});
todoId = created.data.todo._id;
});
it("should update a todo", async () => {
const response = await http.put(/todos/${todoId}, {
title: "Updated Title",
status: "completed",
});
expect(response.status).toBe(200);
expect(response.data.todo.title).toBe("Updated Title");
});
});
describe("DELETE /todos/:id", () => {
it("should delete a todo", async () => {
const created = await http.post("/todos", {
title: "Todo to Delete",
});
const response = await http.del(/todos/${created.data.todo._id});
expect(response.status).toBe(200);
expect(response.success).toBe(true);
});
});
});
`
Test individual handlers without a running server:
`typescript
import { mockReq } from "@flink-app/test-utils";
import GetTodoById from "./handlers/GetTodoById";
describe("GetTodoById Handler", () => {
let mockContext: AppContext;
beforeEach(() => {
mockContext = {
repos: {
todoRepo: {
findById: jasmine.createSpy("findById"),
},
},
} as any;
});
it("should return todo when found", async () => {
const mockTodo = {
_id: "123",
title: "Test Todo",
status: "active",
};
(mockContext.repos.todoRepo.findById as any).and.returnValue(
Promise.resolve(mockTodo)
);
const req = mockReq({
params: { id: "123" },
});
const result = await GetTodoById(req, mockContext);
expect(result.status).toBe(200);
expect(result.data.todo).toEqual(mockTodo);
expect(mockContext.repos.todoRepo.findById).toHaveBeenCalledWith("123");
});
it("should return 404 when not found", async () => {
(mockContext.repos.todoRepo.findById as any).and.returnValue(
Promise.resolve(null)
);
const req = mockReq({
params: { id: "999" },
});
const result = await GetTodoById(req, mockContext);
expect(result.status).toBe(404);
expect(result.error).toBe("not_found");
});
});
`
All HTTP methods return a FlinkResponse object:
`typescript`
interface FlinkResponse
status: number; // HTTP status code
success?: boolean; // Success flag (if present in response)
data?: T; // Response data (if present)
error?: string; // Error code (if error occurred)
message?: string; // Error message (if error occurred)
// ... any other fields from the response
}
The test utilities are fully typed:
`typescript
interface CreateTodoRequest {
title: string;
description?: string;
}
interface CreateTodoResponse {
todo: {
_id: string;
title: string;
description?: string;
createdAt: string;
};
}
const response = await post
"/todos",
{ title: "New Todo" }
);
// response.data is typed as CreateTodoResponse
expect(response.data?.todo._id).toBeDefined();
`
1. Initialize once: Call http.init(app) once in beforeAll, not before each testapp.stop()
2. Clean up: Always call in afterAll
3. Use separate database: Use a test database, not your production database
4. Reset data: Clear test data between tests if needed
5. Mock external services: Use mocks for external API calls
6. Test error cases: Don't just test happy paths
7. Use authentication: Test both authenticated and unauthenticated scenarios
`typescript
describe("My tests", () => {
let app: FlinkApp
beforeAll(async () => {
app = createTestApp();
await app.start();
http.init(app);
});
afterAll(async () => {
await app.stop();
});
it("should work", async () => {
const response = await http.get("/endpoint");
expect(response.status).toBe(200);
});
});
`
`typescript
describe("My tests", () => {
let app: FlinkApp
beforeAll(async () => {
app = createTestApp();
await app.start();
http.init(app);
});
afterAll(async () => {
await app.stop();
});
test("should work", async () => {
const response = await http.get("/endpoint");
expect(response.status).toBe(200);
});
});
`
Use a different port for testing:
`typescript`
const app = new FlinkApp
port: 3001, // Different from dev/prod port
// ...
});
Make sure to call app.stop() in afterAll:
`typescript`
afterAll(async () => {
await app.stop();
});
Use noOpAuthPlugin() for testing or provide valid user objects:
`typescript
app.auth = noOpAuthPlugin();
// OR
const response = await http.get("/endpoint", {
user: { _id: "123", roles: ["admin"] },
});
``
MIT