or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

content-parsing.mddecoration.mderror-handling.mdhooks.mdindex.mdplugins.mdrouting.mdschema.mdserver-lifecycle.mdtesting.md
tile.json

testing.mddocs/

Testing Utilities

Built-in testing support with HTTP injection capabilities for unit and integration testing.

Capabilities

HTTP Injection

Simulate HTTP requests without starting a server using light-my-request.

/**
 * Inject HTTP request for testing (Promise-based)
 * @param options - Request options or URL string
 * @returns Promise resolving to response object
 */
inject(options: InjectOptions | string): Promise<LightMyRequestResponse>;

/**
 * Inject HTTP request with callback
 * @param options - Request options or URL string
 * @param callback - Callback function receiving error and response
 */
inject(options: InjectOptions | string, callback: (err: Error, response: LightMyRequestResponse) => void): void;

/**
 * Get injection chain for multiple requests
 * @returns Chain object for chaining multiple requests
 */
inject(): LightMyRequestChain;
interface InjectOptions {
  method?: string;
  url: string;
  headers?: object;
  payload?: string | object | Buffer;
  query?: object;
  cookies?: object;
  auth?: {
    strategy: string;
    credentials: any;
  };
  validate?: boolean;
  remoteAddress?: string;
  server?: object;
}

interface LightMyRequestResponse {
  statusCode: number;
  statusMessage: string;
  headers: object;
  payload: string;
  body: string;
  json(): any;
  cookies: Array<{ name: string; value: string; [key: string]: any }>;
}

Basic Testing Examples:

const fastify = require('fastify')();

// Add routes
fastify.get('/hello', async () => ({ hello: 'world' }));
fastify.post('/users', async (request) => ({ 
  id: 1, 
  ...request.body 
}));

// Test GET request
const response = await fastify.inject({
  method: 'GET',
  url: '/hello'
});

console.log(response.statusCode); // 200
console.log(response.json()); // { hello: 'world' }

// Test POST request
const postResponse = await fastify.inject({
  method: 'POST',
  url: '/users',
  payload: { name: 'John', email: 'john@example.com' }
});

console.log(postResponse.statusCode); // 200
console.log(postResponse.json()); // { id: 1, name: 'John', email: 'john@example.com' }

Testing with Test Frameworks

Integration with popular testing frameworks.

Jest Examples:

const { test } = require('@jest/globals');

describe('User API', () => {
  let app;

  beforeAll(async () => {
    app = require('./app'); // Your Fastify app
    await app.ready();
  });

  afterAll(async () => {
    await app.close();
  });

  test('GET /users returns user list', async () => {
    const response = await app.inject({
      method: 'GET',
      url: '/users'
    });

    expect(response.statusCode).toBe(200);
    expect(response.json()).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          id: expect.any(Number),
          name: expect.any(String)
        })
      ])
    );
  });

  test('POST /users creates new user', async () => {
    const userData = { name: 'Jane', email: 'jane@example.com' };
    
    const response = await app.inject({
      method: 'POST',
      url: '/users',
      payload: userData
    });

    expect(response.statusCode).toBe(201);
    expect(response.json()).toMatchObject(userData);
  });

  test('GET /users/:id returns specific user', async () => {
    const response = await app.inject({
      method: 'GET',
      url: '/users/1'
    });

    expect(response.statusCode).toBe(200);
    expect(response.json()).toHaveProperty('id', 1);
  });

  test('returns 404 for non-existent user', async () => {
    const response = await app.inject({
      method: 'GET',
      url: '/users/999'
    });

    expect(response.statusCode).toBe(404);
    expect(response.json()).toHaveProperty('error');
  });
});

Tap Examples:

const tap = require('tap');

tap.test('User routes', async (t) => {
  const app = require('./app');
  t.teardown(() => app.close());

  await t.test('GET /users', async (t) => {
    const response = await app.inject({
      method: 'GET',
      url: '/users'
    });

    t.equal(response.statusCode, 200);
    t.type(response.json(), 'object');
  });

  await t.test('POST /users validation', async (t) => {
    const response = await app.inject({
      method: 'POST',
      url: '/users',
      payload: { name: '' } // Invalid data
    });

    t.equal(response.statusCode, 400);
    t.match(response.json(), { error: /validation/i });
  });
});

Advanced Testing Scenarios

Complex testing scenarios and patterns.

Authentication Testing:

// Helper function for authenticated requests
async function authenticatedRequest(app, options) {
  // Get auth token
  const authResponse = await app.inject({
    method: 'POST',
    url: '/auth/login',
    payload: { username: 'test', password: 'test123' }
  });
  
  const { token } = authResponse.json();
  
  // Make authenticated request
  return app.inject({
    ...options,
    headers: {
      ...options.headers,
      authorization: `Bearer ${token}`
    }
  });
}

test('protected route requires authentication', async () => {
  // Unauthenticated request
  const unauthedResponse = await app.inject({
    method: 'GET',
    url: '/protected'
  });
  expect(unauthedResponse.statusCode).toBe(401);
  
  // Authenticated request
  const authedResponse = await authenticatedRequest(app, {
    method: 'GET',
    url: '/protected'
  });
  expect(authedResponse.statusCode).toBe(200);
});

Cookie Testing:

test('session cookie handling', async () => {
  // Login and get session cookie
  const loginResponse = await app.inject({
    method: 'POST',
    url: '/login',
    payload: { username: 'test', password: 'test123' }
  });
  
  const sessionCookie = loginResponse.cookies.find(c => c.name === 'session');
  
  // Use session cookie in subsequent request
  const profileResponse = await app.inject({
    method: 'GET',
    url: '/profile',
    cookies: { session: sessionCookie.value }
  });
  
  expect(profileResponse.statusCode).toBe(200);
});

File Upload Testing:

test('file upload', async () => {
  const FormData = require('form-data');
  const form = new FormData();
  form.append('file', Buffer.from('test file content'), 'test.txt');
  form.append('description', 'Test file');
  
  const response = await app.inject({
    method: 'POST',
    url: '/upload',
    headers: form.getHeaders(),
    payload: form
  });
  
  expect(response.statusCode).toBe(201);
  expect(response.json()).toHaveProperty('fileId');
});

Test Utilities and Helpers

Common testing utilities and helper functions.

// Test data factory
function createTestUser(overrides = {}) {
  return {
    name: 'Test User',
    email: 'test@example.com',
    age: 25,
    ...overrides
  };
}

// Database cleanup helper
async function cleanupDatabase() {
  await db.query('DELETE FROM users WHERE email LIKE "test%"');
  await db.query('DELETE FROM posts WHERE title LIKE "Test%"');
}

// Response assertion helpers
function expectSuccessResponse(response, expectedData) {
  expect(response.statusCode).toBe(200);
  expect(response.json()).toMatchObject({
    success: true,
    data: expectedData
  });
}

function expectErrorResponse(response, statusCode, errorMessage) {
  expect(response.statusCode).toBe(statusCode);
  expect(response.json()).toMatchObject({
    success: false,
    error: expect.stringContaining(errorMessage)
  });
}

// Usage in tests
test('user creation success', async () => {
  const userData = createTestUser();
  const response = await app.inject({
    method: 'POST',
    url: '/users',
    payload: userData
  });
  
  expectSuccessResponse(response, userData);
});

Injection Chain

Chain multiple requests for complex testing scenarios.

interface LightMyRequestChain {
  get(url: string): LightMyRequestChain;
  post(url: string, payload?: any): LightMyRequestChain;
  put(url: string, payload?: any): LightMyRequestChain;
  delete(url: string): LightMyRequestChain;
  headers(headers: object): LightMyRequestChain;
  query(query: object): LightMyRequestChain;
  end(callback: (err, response) => void): void;
  end(): Promise<LightMyRequestResponse>;
}

Chain Examples:

// Chain requests
const response = await app
  .inject()
  .get('/users/1')
  .headers({ 'x-custom-header': 'value' })
  .end();

// Chain with query parameters
const searchResponse = await app
  .inject()
  .get('/users')
  .query({ page: 2, limit: 10 })
  .end();

// Complex chain
const chainResponse = await app
  .inject()
  .post('/auth/login')
  .payload({ username: 'test', password: 'test123' })
  .end();

const token = chainResponse.json().token;

const profileResponse = await app
  .inject()
  .get('/profile')
  .headers({ authorization: `Bearer ${token}` })
  .end();

Mock and Stub Testing

Testing with mocked dependencies.

const sinon = require('sinon');

describe('User service integration', () => {
  let app, dbStub;

  beforeEach(async () => {
    app = require('./app');
    
    // Stub database calls
    dbStub = sinon.stub(app.db, 'query');
    
    await app.ready();
  });

  afterEach(async () => {
    dbStub.restore();
    await app.close();
  });

  test('handles database error gracefully', async () => {
    // Mock database error
    dbStub.rejects(new Error('Database connection failed'));

    const response = await app.inject({
      method: 'GET',
      url: '/users'
    });

    expect(response.statusCode).toBe(500);
    expect(response.json()).toHaveProperty('error');
  });

  test('returns mocked user data', async () => {
    // Mock successful database response
    dbStub.resolves([
      { id: 1, name: 'Mock User', email: 'mock@example.com' }
    ]);

    const response = await app.inject({
      method: 'GET',
      url: '/users'
    });

    expect(response.statusCode).toBe(200);
    expect(response.json()).toHaveLength(1);
    expect(dbStub.calledWith('SELECT * FROM users')).toBe(true);
  });
});

Performance Testing

Basic performance testing with injection.

test('endpoint performance', async () => {
  const iterations = 100;
  const responses = [];
  const startTime = Date.now();

  // Run multiple requests
  for (let i = 0; i < iterations; i++) {
    const response = await app.inject({
      method: 'GET',
      url: '/users'
    });
    responses.push(response);
  }

  const endTime = Date.now();
  const avgResponseTime = (endTime - startTime) / iterations;

  // All requests should succeed
  responses.forEach(response => {
    expect(response.statusCode).toBe(200);
  });

  // Performance assertion
  expect(avgResponseTime).toBeLessThan(100); // < 100ms average

  console.log(`Average response time: ${avgResponseTime}ms`);
});