A Jest & Pest inspired testing framework for Bun with zero external dependencies. UI coming soon!
Besting is a comprehensive testing framework built exclusively for Bun. It provides a fluent, Pest-like API for writing tests with NO external dependencies - everything runs on pure Bun primitives.
- Zero Dependencies - Lightweight & zero external dependencies
- Virtual DOM - Lightning-fast DOM testing without downloading browsers (competing with happy-dom)
- Blazing Performance - Optimized for Bun's runtime, faster than happy-dom
- Laravel-Inspired - Familiar API from Laravel's testing ecosystem
bun add -d besting- Fluent, chainable assertions - Make multiple assertions on the same value with a chainable API.
- Pest-style syntax - Use a similar style to PHP's Pest testing framework.
- Zero overhead - Built directly on Bun's native test runner for maximum performance.
- Full compatibility - Works with all of Bun's testing features including lifecycle hooks, snapshots, and more.
- Browser Testing - Laravel Dusk-inspired browser testing using Chrome DevTools Protocol (CDP). No Playwright, no Puppeteer - pure Bun!
- API Testing - Laravel-inspired API testing utilities for testing HTTP endpoints.
- Database Testing - Laravel-inspired database testing with migrations, seeders, and factories.
- Authentication Testing - Laravel-inspired authentication testing.
- Event Testing - Laravel-inspired event testing with event dispatching and assertions.
- Command Testing - Laravel-inspired command testing for terminal commands.
- Cache Testing - Utilities for testing cache operations.
- Cookie Testing - Utilities for testing cookies.
- URL Testing - Utilities for testing URL components.
import { expect, test } from 'besting'
test('basic addition', () => {
expect(1 + 1).toBe(2)
})import { expect, test } from 'besting'
test('multiple assertions on same value', () => {
expect('Hello World')
.toContain('Hello')
.toContain('World')
.toHaveLength(11)
.toStartWith('Hello')
.toEndWith('World')
})import { best } from 'besting'
const p = best()
p.describe('Calculator', () => {
p.test('addition works', () => {
p.it(1 + 1).toBe(2)
})
p.test('subtraction works', () => {
p.it(3 - 1).toBe(2)
})
})import { describe, expect, test } from 'besting'
describe('Math operations', () => {
test('addition works', () => {
expect(1 + 1).toBe(2)
})
test('subtraction works', () => {
expect(3 - 1).toBe(2)
})
})import { beforeEach, describe, expect, test } from 'besting'
describe('User', () => {
let user
beforeEach(() => {
user = { name: 'John', email: '[email protected]' }
})
test('has correct properties', () => {
expect(user.name).toBe('John')
expect(user.email).toBe('[email protected]')
})
})import { testGroup } from 'besting'
testGroup('Hello World', (str) => {
// All assertions are against the string 'Hello World'
str.toContain('Hello')
.toContain('World')
.toStartWith('Hello')
.toEndWith('World')
.not
.toBeEmpty()
})Besting uses a pure Bun virtual DOM implementation - NO browser downloads needed!
import { browse, test } from 'besting'
test('test with virtual DOM', async () => {
await browse(async (page) => {
await page.goto('https://example.com')
await page.assertSee('Example Domain')
await page.click('#button')
await page.fill('input[name="email"]', '[email protected]')
})
})Benefits:
- β‘ Lightning fast (no browser overhead)
- π― Zero setup (no downloads)
- πͺ Pure Bun (no dependencies)
- π Faster than happy-dom
No setup required! Just import and use:
import { browse } from 'besting'import { browse, test } from 'besting'
test('visit a website', async () => {
await browse(async (page) => {
await page.goto('https://example.com')
await page.assertSee('Example Domain')
await page.assertTitle('Example Domain')
})
})import { browser, test } from 'besting'
test('full browser control', async () => {
const br = browser({ headless: true })
try {
await br.launch()
const page = await br.newPage()
// Navigate
await page.goto('https://example.com')
// Interact with elements
await page.click('button')
await page.type('input[name="search"]', 'Hello')
await page.fill('input[name="email"]', '[email protected]')
// Select from dropdown
await page.select('select[name="country"]', 'US')
// Handle checkboxes
await page.check('input[type="checkbox"]')
await page.uncheck('input[type="checkbox"]')
// Wait for elements
await page.waitForSelector('.result')
await page.waitForText('Success')
// Get element information
const text = await page.text('h1')
const value = await page.value('input')
const isVisible = await page.isVisible('.modal')
// Execute JavaScript
const result = await page.evaluate(() => {
return document.title
})
// Take screenshots
await page.screenshot({ path: 'screenshot.png' })
await page.screenshot({ fullPage: true })
}
finally {
await br.close()
}
})import { browse, test } from 'besting'
test('Dusk-style assertions', async () => {
await browse(async (page) => {
await page.goto('https://example.com')
// Text assertions
await page.assertSee('Welcome')
await page.assertDontSee('Error')
await page.assertSeeIn('.header', 'Logo')
// Element assertions
await page.assertPresent('button')
await page.assertMissing('.error-message')
await page.assertVisible('.modal')
await page.assertNotVisible('.hidden')
// Form assertions
await page.assertValue('input[name="email"]', '[email protected]')
await page.assertChecked('input[name="terms"]')
await page.assertNotChecked('input[name="newsletter"]')
await page.assertEnabled('button[type="submit"]')
await page.assertDisabled('button[type="submit"]')
// Attribute assertions
await page.assertAttribute('a', 'href', 'https://example.com')
await page.assertHasClass('.button', 'btn-primary')
await page.assertHasNotClass('.button', 'disabled')
// Page assertions
await page.assertTitle('Example Domain')
await page.assertTitleContains('Example')
await page.assertUrlIs('https://example.com/')
await page.assertUrlContains('example')
})
})import { browse, test } from 'besting'
test('fill and submit a form', async () => {
await browse(async (page) => {
await page.goto('https://myapp.com/contact')
// Fill form fields
await page.fill('input[name="name"]', 'John Doe')
await page.fill('input[name="email"]', '[email protected]')
await page.fill('textarea[name="message"]', 'Hello!')
// Select from dropdown
await page.select('select[name="subject"]', 'inquiry')
// Check agreement
await page.check('input[name="agree"]')
// Submit form
await page.click('button[type="submit"]')
// Assert success
await page.waitForText('Thank you')
await page.assertSee('Your message has been sent')
})
})import { browse, test } from 'besting'
test('take screenshots for debugging', async () => {
await browse(async (page) => {
await page.goto('https://example.com')
// Take a regular screenshot
const screenshot = await page.screenshot()
// Save screenshot to file
await page.screenshot({ path: 'example.png' })
// Take full page screenshot
await page.screenshot({
path: 'full-page.png',
fullPage: true
})
// Take JPEG screenshot with quality
await page.screenshot({
path: 'example.jpg',
type: 'jpeg',
quality: 80
})
})
})import { browser, test } from 'besting'
test('configure browser options', async () => {
// Use Chromium (default)
const chromiumBrowser = browser({
browser: 'chromium', // Browser type: 'chromium' or 'firefox'
headless: true, // Run in headless mode (default: true)
width: 1920, // Viewport width (default: 1280)
height: 1080, // Viewport height (default: 720)
timeout: 30000, // Default timeout in ms (default: 30000)
devtools: false, // Open DevTools (default: false)
})
// Use Firefox
const firefoxBrowser = browser({
browser: 'firefox',
headless: true,
})
try {
await chromiumBrowser.launch()
const page = await chromiumBrowser.newPage()
// You can also change viewport size after launch
await page.setViewport(1024, 768)
await page.goto('https://example.com')
}
finally {
await chromiumBrowser.close()
}
})import { browser, test } from 'besting'
test('test with Firefox', async () => {
const br = browser({ browser: 'firefox' })
try {
await br.launch()
const page = await br.newPage()
await page.goto('https://example.com')
await page.assertSee('Example Domain')
await page.assertTitle('Example Domain')
}
finally {
await br.close()
}
})import { browser, test } from 'besting'
test('work with multiple pages', async () => {
const br = browser()
try {
await br.launch()
// Create multiple pages
const page1 = await br.newPage()
const page2 = await br.newPage()
// Navigate independently
await page1.goto('https://example.com')
await page2.goto('https://httpbin.org')
// Interact with each page
await page1.assertSee('Example Domain')
await page2.assertSee('httpbin')
}
finally {
await br.close()
}
})import { browse, test } from 'besting'
test('mouse interactions', async () => {
await browse(async (page) => {
await page.goto('https://example.com')
// Hover over an element
await page.hover('.menu-item')
// Double click
await page.doubleClick('.selectable-text')
// Right click
await page.rightClick('.context-menu-trigger')
// Drag and drop
await page.drag('.draggable', '.drop-zone')
})
})import { browse, test } from 'besting'
test('manage cookies', async () => {
await browse(async (page) => {
await page.goto('https://example.com')
// Set a cookie
await page.setCookie('session', 'abc123', {
domain: 'example.com',
path: '/',
secure: true,
httpOnly: true,
sameSite: 'Strict'
})
// Get all cookies
const cookies = await page.getCookies()
// Get specific cookie
const sessionCookie = await page.getCookie('session')
// Delete a cookie
await page.deleteCookie('session')
// Clear all cookies
await page.clearCookies()
})
})import { browse, test } from 'besting'
test('storage operations', async () => {
await browse(async (page) => {
await page.goto('https://example.com')
// Local Storage
await page.setLocalStorage('theme', 'dark')
const theme = await page.getLocalStorage('theme')
await page.removeLocalStorage('theme')
await page.clearLocalStorage()
// Session Storage
await page.setSessionStorage('tab', 'home')
const tab = await page.getSessionStorage('tab')
await page.removeSessionStorage('tab')
await page.clearSessionStorage()
})
})import { browse, test } from 'besting'
test('scroll operations', async () => {
await browse(async (page) => {
await page.goto('https://example.com')
// Scroll to specific coordinates
await page.scrollTo(0, 500)
// Scroll to an element
await page.scrollToElement('#footer')
// Scroll to top
await page.scrollToTop()
// Scroll to bottom
await page.scrollToBottom()
})
})import { browse, test } from 'besting'
test('handle dialogs', async () => {
await browse(async (page) => {
await page.goto('https://example.com')
// Set up dialog handler
await page.onDialog(async (message) => {
console.log('Dialog message:', message)
// Return true to accept, false to dismiss
if (message.includes('confirm')) {
return true
}
// Return string for prompt dialogs
if (message.includes('name')) {
return 'John Doe'
}
return false
})
// Or accept/dismiss manually
await page.acceptDialog()
await page.dismissDialog()
})
})import { browse, test } from 'besting'
test('upload files', async () => {
await browse(async (page) => {
await page.goto('https://example.com/upload')
// Upload single file
await page.uploadFile('input[type="file"]', '/path/to/file.pdf')
// Upload multiple files
await page.uploadFile(
'input[type="file"][multiple]',
'/path/to/file1.jpg',
'/path/to/file2.jpg'
)
await page.click('button[type="submit"]')
})
})import { browse, test } from 'besting'
test('capture console logs', async () => {
await browse(async (page) => {
// Start capturing console logs
await page.startConsoleCapture()
await page.goto('https://example.com')
// Execute some JavaScript that logs to console
await page.evaluate(() => {
console.log('Hello from the browser!')
console.error('An error occurred')
})
// Get captured logs
const logs = page.getConsoleLogs()
console.log(logs)
// [
// { type: 'log', message: 'Hello from the browser!', timestamp: 1234567890 },
// { type: 'error', message: 'An error occurred', timestamp: 1234567891 }
// ]
// Clear logs
page.clearConsoleLogs()
})
})import { browse, test } from 'besting'
test('generate PDF', async () => {
await browse(async (page) => {
await page.goto('https://example.com')
// Generate PDF
await page.pdf({ path: 'page.pdf' })
// Generate PDF with options
await page.pdf({
path: 'page.pdf',
format: 'A4',
printBackground: true,
landscape: true,
scale: 0.8,
marginTop: 10,
marginBottom: 10,
marginLeft: 10,
marginRight: 10
})
})
})import { browse, test } from 'besting'
test('network control', async () => {
await browse(async (page) => {
// Set offline mode
await page.setOffline(true)
await page.goto('https://example.com') // Will fail
await page.setOffline(false)
// Throttle network
await page.setNetworkThrottle('slow3G')
await page.goto('https://example.com')
// Fast 3G
await page.setNetworkThrottle('fast3G')
// No throttling
await page.setNetworkThrottle('none')
// Intercept requests
await page.interceptRequest('*.jpg', (request) => {
console.log('Image request intercepted:', request.url)
})
})
})import { browse, test } from 'besting'
test('mobile emulation', async () => {
await browse(async (page) => {
// Emulate iPhone
await page.emulateDevice('iPhone')
await page.goto('https://example.com')
// Emulate iPad
await page.emulateDevice('iPad')
// Emulate Pixel
await page.emulateDevice('Pixel')
// Emulate Galaxy
await page.emulateDevice('Galaxy')
// Custom user agent
await page.setUserAgent('Mozilla/5.0 (Custom Device) ...')
// Set geolocation
await page.setGeolocation(37.7749, -122.4194) // San Francisco
// Enable touch emulation
await page.setTouchEmulation(true)
})
})Besting includes Laravel-inspired API testing utilities for testing HTTP endpoints.
import { api, assertResponse, test } from 'besting'
test('Basic API test', async () => {
// Make a GET request to an API
const response = await api('https://api.example.com')
.get('/users/1')
// Assert on the response
const assertion = await assertResponse(response).assertOk()
await assertion.assertStatus(200)
await assertion.assertHeader('content-type')
// Get and assert on JSON data
const data = await response.json()
expect(data).toHaveProperty('id', 1)
})import { api, assertResponse, test } from 'besting'
test('Testing different HTTP methods', async () => {
const baseApi = api('https://api.example.com')
// GET with query parameters
const getResponse = await baseApi
.withQuery({ filter: 'active' })
.get('/users')
// POST with JSON data
const postResponse = await baseApi
.post('/users', { name: 'John', email: '[email protected]' })
// PUT to update a resource
const putResponse = await baseApi
.put('/users/1', { name: 'Updated Name' })
// DELETE a resource
const deleteResponse = await baseApi
.delete('/users/1')
})import { api, test } from 'besting'
test('Authenticated API requests', async () => {
// Using Bearer token
const tokenResponse = await api('https://api.example.com')
.withToken('your-auth-token')
.get('/secured-endpoint')
// Using Basic Authentication
const basicAuthResponse = await api('https://api.example.com')
.withBasicAuth('username', 'password')
.get('/secured-endpoint')
})import { api, assertResponse, test } from 'besting'
test('Testing JSON responses', async () => {
const response = await api('https://api.example.com')
.get('/users/1')
// Assert on specific JSON paths
const assertion = await assertResponse(response)
await assertion.assertJsonPath('name', 'John Doe')
await assertion.assertJsonPath('email')
await assertion.assertJsonPath('address.city', 'New York')
// Assert on the entire JSON structure
await assertion.assertJson({
id: 1,
name: 'John Doe',
email: '[email protected]'
})
})import { api, test } from 'besting'
test('Configuring API requests', async () => {
const response = await api('https://api.example.com')
.withHeaders({
'X-Custom-Header': 'Value',
'Accept-Language': 'en-US'
})
.withTimeout(5000) // 5 seconds timeout
.withJson() // Ensure JSON content type
.get('/endpoint')
})Besting includes utilities for testing cache operations, inspired by Laravel's cache assertions.
import { cache, test } from 'besting'
test('Basic cache operations', async () => {
const cacheStore = cache()
// Store a value in cache
await cacheStore.set('user_id', 1)
// Assert that the key exists
await cacheStore.assertHas('user_id')
// Get a value from cache
const userId = await cacheStore.get('user_id')
// Delete a key
await cacheStore.delete('user_id')
// Assert that the key is gone
await cacheStore.assertMissing('user_id')
})import { cache, test } from 'besting'
test('Cache expiration', async () => {
const cacheStore = cache()
// Set a value with a 1 second TTL
await cacheStore.set('temp', 'value', 1)
// Value should exist initially
await cacheStore.assertExists('temp')
// Wait for expiration
await new Promise(resolve => setTimeout(resolve, 1100))
// Value should be gone after TTL expires
await cacheStore.assertNotExists('temp')
})Besting includes utilities for testing cookies, compatible with both browser and server environments.
import { cookie, test } from 'besting'
test('Basic cookie operations', () => {
const cookieJar = cookie()
// Set cookies
cookieJar
.set('session_id', '123456789')
.set('theme', 'dark')
// Assert cookies exist
cookieJar
.assertHas('session_id')
.assertHas('theme')
// Assert cookie values
cookieJar
.assertValue('session_id', '123456789')
.assertValue('theme', 'dark')
// Remove a cookie
cookieJar.remove('theme')
// Assert cookie is gone
cookieJar.assertMissing('theme')
})Besting includes utilities for testing URL components.
import { test, url } from 'besting'
test('URL component testing', () => {
const testUrl = url('https://example.com/users?sort=asc&page=1#profile')
// Assert URL components
testUrl
.hasProtocol('https')
.hasHost('example.com')
.hasPath('/users')
.hasQuery('sort', 'asc')
.hasQuery('page', '1')
.hasFragment('profile')
// Check for absence of query parameters
testUrl.doesntHaveQuery('filter')
// Get URL components
console.log(testUrl.path) // '/users'
console.log(testUrl.queryParams) // { sort: 'asc', page: '1' }
})Besting includes all matchers from Bun's test runner, plus additional Pest-inspired matchers:
toStartWith(prefix)- Assert that a string starts with a prefixtoEndWith(suffix)- Assert that a string ends with a suffixtoBeEmpty()- Assert that a string, array, or object is emptytoPass(validator, message?)- Assert that a value passes a custom validation function
Besting's virtual DOM is built to outperform happy-dom while maintaining zero dependencies.
# Run performance benchmarks (using mitata)
bun run bench
# Run bun:test benchmarks
bun run bench:bunOur benchmarks test:
- Document creation - Fast initialization of virtual DOM documents
- HTML parsing - Parsing small, medium, and large HTML documents
- Query selectors - getElementById, querySelector, querySelectorAll by ID, class, tag, and attribute
- DOM manipulation - appendChild, removeChild, textContent operations
- Attribute operations - Getting and setting element attributes
- ClassList operations - Adding, removing, toggling CSS classes
- innerHTML operations - Setting and reading HTML content
- Memory efficiency - Large DOM tree creation and manipulation
Running on Apple M3 Pro @ 3.5 GHz with Bun 1.2.24:
| Operation | Performance |
|---|---|
createDocument() |
~120 ns/iter |
createElement |
~26 ns/iter |
querySelector by ID |
~10 ns/iter |
querySelector by class |
~11 ns/iter |
parse small HTML |
~450 ns/iter |
parse medium HTML |
~3.2 Β΅s/iter |
appendChild (1000x) |
~43 Β΅s/iter |
setAttribute |
~4.7 ns/iter |
classList.add |
~436 ns/iter |
innerHTML set |
~2.3 Β΅s/iter |
Result: Blazing fast DOM operations with ZERO dependencies! π
Besting includes Laravel-inspired database testing utilities with migrations, seeders, and factories.
import { db, migration, seeder, test } from 'besting'
// Define a migration
migration(async (connection) => {
await connection.raw('CREATE TABLE users (id INT, name TEXT, email TEXT)')
})
// Define a seeder
seeder(async (connection) => {
await connection.table('users').insert([
{ id: 1, name: 'John', email: '[email protected]' },
{ id: 2, name: 'Jane', email: '[email protected]' },
])
})
test('Basic database operations', async () => {
const database = db().register(yourDatabaseConnection)
// Run migrations and seeders
await database.migrate()
await database.seed()
// Query data
const users = await database.select('users')
expect(users.length).toBe(2)
// Insert data
await database.insert('users', { id: 3, name: 'Alice', email: '[email protected]' })
// Make assertions
await database.assertExists('users', { id: 3 })
await database.assertSame('users', { id: 3 }, { name: 'Alice' })
})import { db, test, useTransaction } from 'besting'
test('Database transactions', async () => {
const database = db().register(yourDatabaseConnection)
// Use transactions to isolate tests
await database.beginTransaction()
// Make changes
await database.insert('users', { id: 3, name: 'Alice', email: '[email protected]' })
// Rollback changes
await database.rollbackTransaction()
// Use the transaction helper
const transactionTest = useTransaction(async (db) => {
// This code runs within a transaction
await db.insert('users', { id: 4, name: 'Bob', email: '[email protected]' })
})
await transactionTest()
})import { db, test } from 'besting'
test('Database factories', async () => {
const database = db().register(yourDatabaseConnection)
// Create a user factory
const userFactory = database.factory('users')
.define({
name: 'Default User',
email: '[email protected]',
})
.state('admin', user => ({
...user,
name: 'Admin User',
email: '[email protected]',
}))
// Create a default user
await userFactory.create({ id: 10 })
// Create an admin user
await userFactory.has('admin').create({ id: 11 })
// Create multiple users
await userFactory.count(3).create()
// Make model instances without persisting
const user = userFactory.make()
})Besting includes Laravel-inspired event testing utilities for testing event dispatching.
import { defineEvent, events, fakeEvents, test } from 'besting'
// Define event classes
class UserCreated {
constructor(public id: number, public name: string) {}
}
// Define an event using the helper
const OrderShipped = defineEvent({
id: 0,
trackingNumber: '',
})
test('Basic event testing', () => {
const fake = fakeEvents()
// Dispatch events
events().dispatch(new UserCreated(1, 'John'))
events().dispatch(new UserCreated(2, 'Jane'))
// Make assertions
fake.assertDispatched('UserCreated')
fake.assertDispatchedTimes('UserCreated', 2)
fake.assertNotDispatched('OrderShipped')
// Check specific events
fake.assertDispatched('UserCreated', event => event.id === 1)
})import { events, listener, test } from 'besting'
class UserCreated {
constructor(public id: number, public name: string) {}
}
class EventListener {
events: any[] = []
@listener(UserCreated.name)
handleUserCreated(event: UserCreated) {
this.events.push(event)
}
}
test('Event listeners', () => {
const listener = new EventListener()
// Dispatch an event
events().dispatch(new UserCreated(1, 'John'))
// Check that the listener received it
expect(listener.events.length).toBe(1)
expect(listener.events[0].name).toBe('John')
})Besting includes Laravel-inspired authentication testing utilities.
import { auth, test } from 'besting'
test('Authentication testing', () => {
// Define a user
const user = {
id: 1,
name: 'Test User',
email: '[email protected]',
}
// Set the authenticated user
auth().actingAs(user)
// Make assertions
auth().assertAuthenticated()
expect(auth().user().id).toBe(1)
// Act as guest
auth().actingAsGuest()
auth().assertGuest()
})import { auth, test, withAuth } from 'besting'
test('With auth helper', () => {
const user = {
id: 1,
name: 'Test User',
email: '[email protected]',
}
// Create request with auth context
const request = withAuth(user)
expect(request.user).toBe(user)
expect(request.auth.check()).toBe(true)
})Besting includes utilities for testing terminal commands, including Laravel-inspired Artisan command testing.
import { command, test } from 'besting'
test('Command testing', async () => {
const cmd = command()
// Execute a command
const result = await cmd.execute('echo', ['Hello, World!'])
// Make assertions
cmd
.assertExitCode(0)
.assertOutputContains('Hello')
.assertOutputNotContains('error')
})Besting uses Bun's native test runner, providing a seamless testing experience with all of Bun's built-in test features.
# Run all tests with Bun's standard test runner
bun test
# Run all tests with our custom runner (ensures all test files are executed)
bun run test:custom
# Run a specific test file
bun test path/to/test.ts
# Run tests with debugging enabled
bun run test:debugBesting seamlessly integrates with Bun's test runner, allowing you to:
- Use all of Bun's test features (snapshots, mocks, etc.)
- Get beautifully formatted test output
- Run tests in parallel for better performance
Note: Bun's test runner may sometimes have issues discovering or executing all test files (see Bun issue #3506). If you notice that some test files are not being executed, you can use our custom test runner with
bun run test:custom, which ensures all test files are discovered and executed individually.
Please see our releases page for more information on what has changed recently.
Please see CONTRIBUTING for details.
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
For casual chit-chat with others using this package:
Join the Stacks Discord Server
"Software that is free, but hopes for a postcard." We love receiving postcards from around the world showing where Stacks is being used! We showcase them on our website too.
Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States π
We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us.
The MIT License (MIT). Please see LICENSE for more information.
Made with π
