npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

saddle-up

v0.5.4

Published

An easy-to-use testing framework for node servers everywhere

Downloads

1,803

Readme

🐴 SaddleUp 🐴

CI status npm version

An easy-to-use testing framework for node servers everywhere.

yarn add saddle-up --dev

✨Features ✨

  • Server framework agnostic (though comes with some extra niceties for Express/connect style middleware, and for koa)
  • Testing framework agnostic (though comes with some handy matchers for jest)
  • Test the way you write code, using the standard fetch API and Response / Request objects
  • Minimal dependencies, take a look at our package.json!
  • Parallel test safe
  • Handles choosing ports intelligently
  • Extensible with adapters
  • Compiled to all the flavours of JS
  • Written in delicious TypeScript and shipped with full types and type inference
  • Supports redirects, persisting cookies between requests, and more!

🔧 Usage 🔧

The Gist

A testcase using saddle tends to have more or less the following structure:

  • saddle your server (or middleware)
  • call fetch on the saddled server
  • make assertions on the resulting Response

You generally also want to call unsaddle after your test suite is complete, to clean up.

Absolute Minimum

You can use SaddleUp to test anything with a listen method that outputs a node Server instance. Even the raw http library itself.

With a basic server such as:

//server.js
const {createServer} = require('http');

export const server = createServer((req, res) => {
  res.writeHead(200);
  res.end('Hello, World!');
});

You could write tests using only SaddleUp and assert like so:

//server.test.js
const assert = require('assert');
const {saddle} = require('saddle-up');
const {server} = require('./server');

async function runTest() {
  const wrapper = await saddle(server);
  const resp = await wrapper.fetch('/');

  assert.equal(resp.status, 200);
}

runTest();

But there is nothing stopping you from importing applications with any framework and testing them. All you need to pass to saddle is an object with a listen method that returns an http server, and you're ready to go on your testing journey.

Jest, Matchers, Frameworks

SaddleUp provides a basis which can be built on for any server and test framework, but it also comes with a number of constructs on top of that basis tailored to work with specific tools. There are several adapters which ship with the library for popular server libraries, which allow saddleing of individual middleware and some other niceties. There are also a number of useful matchers for the popular jest testing framework which can simplify writing assertions.

Express

saddle-up/express provides a wrapped version of saddle for testing Express applications. For example, if we had a middleware in our Express application like so:

//visit-counter.js
let visits = 0;

export function visitCounter(_req, res) {
  visits += 1;
  res.setHeader('x-visits', visits);
}

We can write a simple test with SaddleUp for it like:

//visit-counter.test.js
import 'saddle-up/matchers';
import {saddle} from 'saddle-up/express';
import {visitCounter} from './visit-counter';

describe('MyApp', () => {
  it('iterates visitors every visit', async () => {
    const wrapper = await saddle(visitCounter);
    const resp = await wrapper.fetch('/');
    expect(resp).toHaveHeaders({'x-visits': 1}
  });
});

You'll notice that we didn't need to create a server at all. That is because saddle-up/koa is smart enough to handle creating a server for us and mounting our middleware.

Note: This also still works with full applications just like the default saddle, and is compatible with all the same matchers.

Koa

saddle-up/koa provides a wrapped version of saddle for testing Koa applications. For example, if we had a middleware in our Koa application like so:

let visits = 0;

//visit-counter.js
export function visitCounter(ctx, next) {
  visits += 1;
  ctx.state.visits = visits;
  await next();
}

We can write a simple test with SaddleUp for it like:

//visit-counter.test.js
import 'saddle-up/matchers';
import 'saddle-up/matchers/koa';
import {saddle} from 'saddle-up/koa';
import {visitCounter} from './visit-counter';

describe('MyApp', () => {
  it('iterates visitors every visit', async () => {
    const wrapper = await saddle(visitCounter);
    const resp = await wrapper.fetch('/');
    expect(resp).toHaveKoaState({visits: 1});
  });
});

You'll notice that we didn't need to create a server at all. That is because saddle-up/koa is smart enough to handle creating a server for us and mounting our middleware. It even knows how to check the state value of the corresponding Koa context object to our response.

Note: This also still works with full applications just like the default saddle, and is compatible with all the same matchers.

API

Core

saddle

Wraps a Listenable (anything with a listen method that returns an HTTP server) in a SaddleUp instance for testing.

import {saddle} from 'saddle-up';
import app from './app';

const wrapper = saddle(app);

A number of options are also available.

interface Options {
  // specify a custom host (defaults to `127.0.0.1`)
  host: string;
  // specify a custom port (defaults to finding a free one automatically)
  port: number;
  // specify starting cookies (defaults to an empty object)
  cookies: Record<string, string>;
}

Additionally, you can also pass a ListenerFactory to facilitate wrapping unusual server APIs which don't have an adapter pre-built.

const wrapper = await saddle((port, host) =>
  somethingThatReturnsAServer(port, host),
);

unsaddle()

Runs stop on every instance of saddle which has been created.

SaddleUp

The class instantiated and customized by the saddle function.

fetch()

Exposes a wrapped node-fetch for use against the listenable the instance wraps.

const resp = await wrapper.fetch('/some-path');
address

The fully qualified hostname (with protocol) of the running server

cookies

A vanilla object representing the cookie key and values which have been persisted

setCookie(name, value)

Persists a cookie with the given name and value

clearCookies()

Clears all the persisted cookies on the current instance

stop()

Closes the underlying Listenable.

Adapters

Express

saddle()

Wraps an Express application or middleware in a SaddleUp instance for testing.

import {saddle} from 'saddle-up/express';
import app from './app';

const wrapper = saddle(app);

All of the usual options are available, but you may also decorate the server that ends up mounted by adding beforeMiddleware and afterMiddleware.

unsaddle()

Runs stop on every instance of saddle which has been created.

Connect

saddle()

Wraps a Connect application or middleware in a SaddleUp instance for testing.

import {saddle} from 'saddle-up/connect';
import app from './app';

const wrapper = saddle(app);

All of the usual options are available, but you may also decorate the server that ends up mounted by adding beforeMiddleware and afterMiddleware.

unsaddle()

Runs stop on every instance of saddle which has been created.

Koa

saddle()

Wraps an Express application or middleware in a SaddleUp instance for testing.

import {saddle} from 'saddle-up/koa';
import app from './app';

const wrapper = saddle(app);

All of the usual options are available, but you may also decorate the server that ends up mounted by adding beforeMiddleware and afterMiddleware, or provide custom initial ctx.state.

unsaddle()

Runs stop on every instance of saddle which has been created.

Matchers

SaddleUp provides a number of matchers for Jest. They are divided between the core set which works for anything and adapter specific bundles.

Core

import 'saddle-up/matches'

To register all the default matchers to your expect, just add this line to your test setup file or to individual tests. The matchers below will become available when this is imported.

toHaveBodyJson(response)

This matcher checks if the response JSON matches a subset of key/value pairs.

await expect(response).toHaveBodyJson({foo: 'bar'});
toHaveBodyText(response)

This matcher checks if the response text contains a substring.

await expect(response).toHaveBodyText('something');
toHaveStatus(response)

This matcher checks if the response status is a specific number.

await expect(response).toHaveStatus(200);
toHaveHeaders(response)

This matcher checks if the response headers match a subset of key/value pairs.

await expect(response).toHaveHeaders({'x-foo': 'bar'});
toHaveStatusText(response)

This matcher checks if the response's statusText is a specific string.

await expect(response).toHaveStatusText('OK');
toHaveSetCookie(response)

This matcher checks if a specific cookie was set on the response, optionally it can also check if it was set to a specific value.

await expect(response).toHaveSetCookie('user');
await expect(response).toHaveSetCookie('user', 'maru');
toHaveCookies(wrapper)

This matcher checks if the wrapper has persisted a subset of key/value pairs.

await expect(wrapper).toHaveCookies({user: 'maru'});

Koa

toHaveKoaState(response)

This matcher checks if the corresponding ctx object for a given request has a subset of key/value pairs on it's state.

await expect(response).toHaveKoaState({user: 'maru'});

FAQ

How do I make this work with a framework that doesn't expose .listen?

Some libraries do not use the standard .listen API to generate a server. That's no problem for SaddleUp though. You can use the alternative API to pass a function to your saddle instance.

For example, we could hook up hapi like so:

import Hapi from '@hapi/hapi';
import {saddle} from 'saddle-up';
import someHapiRoute from './route';

const wrapper = await saddle((port, host) => {
  const server = Hapi.server({
    port,
    host,
  });

  server.route(someHapiRoute);

  server.start();
  return server.listener;
});

const resp = await wrapper.fetch('/');

With this function API you can make it work for all sorts of frameworks.

How can I write a custom matcher?

This part of the docs is under construction. if you are really excited to make a thing, check out the existing adapters for inspiration.

Contributing

Feel free to open issues or PRs, I may or may not have time to get to the promptly but they are definitely appreciated.