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]') }) })
- โก 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:bun
Our 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:debug
Besting 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 ๐
