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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@atlex/testing

v0.1.5

Published

Test utilities, fakes, and Vitest matchers for Atlex apps

Downloads

661

Readme

@atlex/testing

First-class testing toolkit: HTTP client, fakes, database helpers, and assertions.

npm TypeScript

Buy Me A Coffee

Installation

npm install @atlex/testing
# or
yarn add @atlex/testing

Quick Start

import { test } from 'vitest'
import { TestClient } from '@atlex/testing'

test('can retrieve home page', async () => {
  const response = await TestClient.get('/')
  response.assertOk()
  response.assertSee('Welcome')
})

test('can create a user', async () => {
  const response = await TestClient.post('/users', {
    name: 'John Doe',
    email: '[email protected]',
  })

  response.assertCreated()
  response.json().id // Access response body
})

Features

  • TestClient: Fluent HTTP client for API testing
  • Request Builders: Chain methods for headers, auth, and request customization
  • Response Assertions: Assert status codes, headers, and content
  • Fake Services: Mock mail, queue, events, storage, notifications, cache, and logs
  • Database Helpers: Seed databases, refresh state between tests
  • Factories: Generate test data with minimal code
  • Time Helpers: Freeze, travel, and manipulate time in tests
  • Custom Matchers: Additional assertion helpers for common patterns

TestClient: Making Requests

HTTP Methods

import { TestClient } from '@atlex/testing'

// GET request
const getResponse = await TestClient.get('/users')

// POST request
const postResponse = await TestClient.post('/users', {
  name: 'Jane Doe',
  email: '[email protected]',
})

// PUT request
const putResponse = await TestClient.put('/users/1', {
  name: 'Jane Smith',
})

// PATCH request
const patchResponse = await TestClient.patch('/users/1', {
  email: '[email protected]',
})

// DELETE request
const deleteResponse = await TestClient.delete('/users/1')

Request Customization

import { TestClient } from '@atlex/testing'

const response = await TestClient.get('/api/users')
  .withHeaders({
    'X-Custom-Header': 'value',
    Authorization: 'Bearer token',
  })
  .withToken('auth-token')
  .actingAs(user)
  .withoutExceptionHandling()

Acting As a User

import { TestClient } from '@atlex/testing'

const user = await User.find(1)

const response = await TestClient.actingAs(user).get('/dashboard')

response.assertOk()

TestResponse: Assertions

Status Assertions

import { TestClient } from '@atlex/testing'

const response = await TestClient.get('/users')

response.assertOk() // 200
response.assertCreated() // 201
response.assertNoContent() // 204
response.assertRedirect() // 3xx
response.assertBadRequest() // 400
response.assertUnauthorized() // 401
response.assertForbidden() // 403
response.assertNotFound() // 404
response.assertStatus(200) // Specific status
response.assertStatus(200, 201) // One of statuses

Content Assertions

const response = await TestClient.get('/users')

// Get JSON response
const json = response.json()
const users = response.json().data

// Get text response
const text = response.text()

// Access headers
const contentType = response.headers()['content-type']

JSON Assertions

const response = await TestClient.post('/users', {
  name: 'John Doe',
})

response.assertJson({
  success: true,
  data: {
    name: 'John Doe',
  },
})

response.assertJsonPath('data.name', 'John Doe')
response.assertJsonCount(10, 'data') // Count array length

Content Matching

const response = await TestClient.get('/users')

response.assertSee('John Doe') // Contains text
response.assertDontSee('Admin') // Doesn't contain text
response.assertSeeInOrder(['John', 'Doe']) // In order
response.assertSeeJson({ name: 'John' }) // Contains JSON

Fakes: Mocking Services

Mail Fake

import { test } from 'vitest'
import { TestClient, MailFake } from '@atlex/testing'

test('sends welcome email', async () => {
  const mailFake = new MailFake()

  await TestClient.post('/register', {
    name: 'John Doe',
    email: '[email protected]',
  })

  // Assert email was sent
  mailFake.assertSent('[email protected]', WelcomeEmail)

  // Assert email count
  mailFake.assertCount(1)

  // Get sent mails
  const mails = mailFake.sent()
})

Queue Fake

import { QueueFake } from '@atlex/testing'

test('dispatches background job', async () => {
  const queueFake = new QueueFake()

  await TestClient.post('/orders', orderData)

  // Assert job was dispatched
  queueFake.assertDispatched(ProcessOrderJob)

  // Assert with payload
  queueFake.assertDispatched(ProcessOrderJob, (job) => {
    return job.orderId === 123
  })

  // Get dispatched jobs
  const jobs = queueFake.dispatched()
})

Event Fake

import { EventFake } from '@atlex/testing'

test('fires user created event', async () => {
  const eventFake = new EventFake()

  await TestClient.post('/users', userData)

  // Assert event was fired
  eventFake.assertDispatched(UserCreated)

  // Assert with payload
  eventFake.assertDispatched(UserCreated, (event) => {
    return event.user.email === '[email protected]'
  })
})

Storage Fake

import { StorageFake } from '@atlex/testing'

test('uploads file', async () => {
  const storageFake = new StorageFake()

  await TestClient.post('/upload', formData)

  // Assert file was stored
  storageFake.assertStored('avatars/user.jpg')

  // Assert missing
  storageFake.assertMissing('old/avatar.jpg')

  // Get stored files
  const files = storageFake.stored()
})

Notification Fake

import { NotificationFake } from '@atlex/testing'

test('sends notification', async () => {
  const notificationFake = new NotificationFake()

  const user = await User.find(1)
  await user.notify(new WelcomeNotification())

  // Assert notification sent
  notificationFake.assertSentTo(user, WelcomeNotification)

  // Assert count
  notificationFake.assertCount(1)
})

Cache Fake

import { CacheFake } from '@atlex/testing'

test('caches user data', async () => {
  const cacheFake = new CacheFake()

  await TestClient.get('/users/1')

  // Assert value was cached
  cacheFake.assertHas('user:1')

  // Get cached value
  const cached = cacheFake.get('user:1')
})

Log Fake

import { LogFake } from '@atlex/testing'

test('logs errors', async () => {
  const logFake = new LogFake()

  await TestClient.get('/invalid-route')

  // Assert log entry
  logFake.assertLogged('error', (log) => {
    return log.message.includes('Route not found')
  })
})

Database Testing

Using Test Database

import { test } from 'vitest'
import { useDatabase, refreshDatabase } from '@atlex/testing'

test.beforeEach(async () => {
  await useDatabase('testing')
  await refreshDatabase()
})

test('can retrieve users', async () => {
  await User.create({ name: 'John Doe' })

  const response = await TestClient.get('/users')

  response.assertJson({
    data: [{ name: 'John Doe' }],
  })
})

Database Seeding

import { test } from 'vitest'
import { seed } from '@atlex/testing'

test.beforeEach(async () => {
  await seed(async (factory) => {
    // Create test data
    await factory.model(User).create({
      name: 'Admin',
      role: 'admin',
    })

    await factory.model(Post).times(5).create()
  })
})

Factories: Test Data Generation

Creating Test Data

import { Factory } from '@atlex/testing'

// Create single model
const user = await Factory.make(User)

// Create with attributes
const user = await Factory.make(User, {
  name: 'John Doe',
  email: '[email protected]',
})

// Create and persist
const user = await Factory.create(User)

// Create multiple
const users = await Factory.times(5).create(User)

// Chain methods
const users = await Factory.times(3).create(User, { role: 'admin' })

Defining Factories

import { Factory } from '@atlex/testing'

class UserFactory extends Factory {
  model() {
    return User
  }

  definition() {
    return {
      name: this.faker.person.fullName(),
      email: this.faker.internet.email(),
      password: 'password',
      emailVerifiedAt: new Date(),
    }
  }
}

// Use factory
const user = await UserFactory.create()

// With overrides
const admin = await UserFactory.create({ role: 'admin' })

Time Helpers

Freezing Time

import { test } from 'vitest'
import { freezeTime, unfreezeTime, now } from '@atlex/testing'

test('handles time-based logic', () => {
  freezeTime('2024-03-15 14:30:00')

  const timestamp = now() // March 15, 2024 14:30

  unfreezeTime()
})

Traveling Time

import { travelTo, travelForward, travelBack } from '@atlex/testing'

test('schedules task', async () => {
  const scheduled = new Date('2024-03-15')

  // Travel to specific time
  travelTo('2024-03-15 10:00:00')

  // Travel forward
  travelForward('1 day')
  travelForward('2 hours')

  // Travel backward
  travelBack('30 minutes')
})

Complete Example

import { test } from 'vitest'
import {
  TestClient,
  useDatabase,
  refreshDatabase,
  Factory,
  MailFake,
  freezeTime,
  unfreezeTime,
} from '@atlex/testing'

test('user registration flow', async () => {
  await useDatabase('testing')
  await refreshDatabase()

  freezeTime('2024-03-15 10:00:00')

  const mailFake = new MailFake()

  // Register user
  const response = await TestClient.post('/register', {
    name: 'Jane Doe',
    email: '[email protected]',
    password: 'password123',
  })

  // Assert response
  response.assertCreated()
  response.assertJson({
    message: 'Registration successful',
  })

  // Assert email sent
  mailFake.assertSent('[email protected]', VerifyEmailNotification)

  // Assert user created in database
  const user = await User.where('email', '[email protected]').first()
  expect(user).toBeDefined()

  unfreezeTime()
})

test('can edit user profile', async () => {
  const user = await Factory.create(User)

  const response = await TestClient.actingAs(user).put(`/users/${user.id}`, {
    name: 'Jane Doe',
    bio: 'Test bio',
  })

  response.assertOk()

  const updated = await User.find(user.id)
  expect(updated.name).toBe('Jane Doe')
})

test('requires authentication', async () => {
  const response = await TestClient.get('/dashboard')

  response.assertUnauthorized()
})

test('enforces authorization', async () => {
  const user = await Factory.create(User, { role: 'user' })
  const admin = await Factory.create(User, { role: 'admin' })

  const response = await TestClient.actingAs(user).delete(`/users/${admin.id}`)

  response.assertForbidden()
})

Custom Matchers

import { expect } from 'vitest'
import { addCustomMatchers } from '@atlex/testing'

addCustomMatchers({
  toBeValidEmail: (email: string) => {
    const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
    return {
      pass: valid,
      message: () => `Expected ${email} to be a valid email`,
    }
  },
})

test('validates email', () => {
  expect('[email protected]').toBeValidEmail()
})

API Overview

TestClient

| Method | Description | | ---------------------------- | -------------------------- | | get(path) | Send GET request | | post(path, data) | Send POST request | | put(path, data) | Send PUT request | | patch(path, data) | Send PATCH request | | delete(path) | Send DELETE request | | actingAs(user) | Set authenticated user | | withHeaders(headers) | Add request headers | | withToken(token) | Add authorization token | | withoutExceptionHandling() | Disable exception handling |

TestResponse

| Method | Description | | ---------------------- | ----------------------- | | assertStatus(code) | Assert HTTP status | | assertOk() | Assert 200 OK | | assertCreated() | Assert 201 Created | | assertNoContent() | Assert 204 No Content | | assertRedirect() | Assert 3xx Redirect | | assertNotFound() | Assert 404 Not Found | | assertUnauthorized() | Assert 401 Unauthorized | | assertForbidden() | Assert 403 Forbidden | | json() | Get JSON body | | text() | Get text body | | headers() | Get response headers |

Fakes

| Fake | Description | | ------------------ | ------------------ | | MailFake | Mock mail sending | | QueueFake | Mock job dispatch | | EventFake | Mock events | | StorageFake | Mock file storage | | NotificationFake | Mock notifications | | CacheFake | Mock cache | | LogFake | Mock logging |

Database & Factories

| Helper | Description | | ----------------------- | ------------------------- | | useDatabase(name) | Switch test database | | refreshDatabase() | Clear and reset database | | seed(callback) | Seed test data | | Factory.make(Model) | Create in-memory instance | | Factory.create(Model) | Create and persist | | Factory.times(n) | Create multiple |

Time

| Function | Description | | ------------------------- | ------------------------------ | | freezeTime(time) | Freeze time at specific moment | | unfreezeTime() | Resume normal time | | travelTo(time) | Travel to specific time | | travelForward(duration) | Move time forward | | travelBack(duration) | Move time backward | | now() | Get current frozen time |

Documentation

For complete documentation, visit https://atlex.dev/guide/testing

License

MIT

Part of Atlex — A modern framework for Node.js.