A comprehensive testing framework built on Bun's native test runner, providing assertions, database testing utilities, feature testing, factories, and mocking capabilities.
Installation
bunadd@stacksjs/testing
Basic Usage
import{describe,it,expect,beforeAll,afterAll}from'@stacksjs/testing'describe('User',()=>{it('should create a new user',async()=>{constuser=awaitUser.create({name:'John',email:'john@test.com'})expect(user.name).toBe('John')expect(user.email).toBe('john@test.com')})})
Test Structure
Describe and It
import{describe,it,expect}from'@stacksjs/testing'describe('Calculator',()=>{describe('add',()=>{it('should add two positive numbers',()=>{expect(add(2,3)).toBe(5)})it('should handle negative numbers',()=>{expect(add(-1,1)).toBe(0)})})describe('divide',()=>{it('should divide two numbers',()=>{expect(divide(10,2)).toBe(5)})it('should throw on division by zero',()=>{expect(()=>divide(10,0)).toThrow('Division by zero')})})})
Lifecycle Hooks
import{describe,it,beforeAll,beforeEach,afterAll,afterEach}from'@stacksjs/testing'describe('Database Tests',()=>{beforeAll(async()=>{// Run once before all tests in this describe blockawaitdatabase.connect()})beforeEach(async()=>{// Run before each testawaitdatabase.beginTransaction()})afterEach(async()=>{// Run after each testawaitdatabase.rollback()})afterAll(async()=>{// Run once after all testsawaitdatabase.disconnect()})it('should insert record',async()=>{awaitUser.create({name:'John'})constcount=awaitUser.count()expect(count).toBe(1)})})
Assertions
Basic Matchers
import{expect}from'@stacksjs/testing'// Equalityexpect(value).toBe(42)// Strict equalityexpect(value).toEqual({a:1})// Deep equalityexpect(value).not.toBe(0)// Negation// Truthinessexpect(value).toBeTruthy()expect(value).toBeFalsy()expect(value).toBeNull()expect(value).toBeUndefined()expect(value).toBeDefined()// Numbersexpect(value).toBeGreaterThan(5)expect(value).toBeGreaterThanOrEqual(5)expect(value).toBeLessThan(10)expect(value).toBeLessThanOrEqual(10)expect(value).toBeCloseTo(0.3,2)// For floating point// Stringsexpect(str).toContain('hello')expect(str).toMatch(/pattern/)expect(str).toHaveLength(5)// Arraysexpect(arr).toContain('item')expect(arr).toHaveLength(3)expect(arr).toEqual(['a','b','c'])// Objectsexpect(obj).toHaveProperty('name')expect(obj).toHaveProperty('address.city','NYC')expect(obj).toMatchObject({name:'John'})
Exceptions
// Expect function to throwexpect(()=>throwingFunction()).toThrow()expect(()=>throwingFunction()).toThrow('error message')expect(()=>throwingFunction()).toThrow(CustomError)expect(()=>throwingFunction()).toThrow(/pattern/)// Async throwawaitexpect(async()=>awaitasyncThrow()).rejects.toThrow()
import{describe,it,expect,useDatabaseTransactions}from'@stacksjs/testing'describe('User Model',()=>{// Wrap each test in a transaction that rolls backuseDatabaseTransactions()it('should create user',async()=>{constuser=awaitUser.create({name:'Test User',email:'test@test.com'})expect(user.id).toBeDefined()// Automatically rolled back after test})})
Database Assertions
import{assertDatabaseHas,assertDatabaseMissing}from'@stacksjs/testing'it('should save user to database',async()=>{awaitUser.create({name:'John',email:'john@test.com'})// Assert record existsawaitassertDatabaseHas('users',{email:'john@test.com'})})it('should delete user',async()=>{constuser=awaitUser.create({name:'John',email:'john@test.com'})awaituser.delete()// Assert record doesn't existawaitassertDatabaseMissing('users',{email:'john@test.com'})})
Database Count Assertions
import{assertDatabaseCount}from'@stacksjs/testing'it('should have correct number of users',async()=>{awaitUser.create({name:'User 1'})awaitUser.create({name:'User 2'})awaitassertDatabaseCount('users',2)})
import{UserFactory,PostFactory}from'tests/factories'it('should create user with factory',async()=>{// Create single recordconstuser=awaitUserFactory.create()expect(user.id).toBeDefined()// Create multiple recordsconstusers=awaitUserFactory.createMany(5)expect(users).toHaveLength(5)// Create with overridesconstadmin=awaitUserFactory.create({role:'admin',email:'admin@test.com'})expect(admin.role).toBe('admin')})
exportconstPostFactory=Factory.define(()=>({title:faker.lorem.sentence(),content:faker.lorem.paragraphs()})).hasMany('comments',CommentFactory,3).belongsTo('author',UserFactory)// Creates post with author and 3 commentsconstpost=awaitPostFactory.create()
Mocking
Mock Functions
import{mock,spyOn}from'@stacksjs/testing'it('should call function with correct args',()=>{constmockFn=mock(()=>'result')constresult=mockFn('arg1','arg2')expect(mockFn).toHaveBeenCalled()expect(mockFn).toHaveBeenCalledWith('arg1','arg2')expect(mockFn).toHaveBeenCalledTimes(1)expect(result).toBe('result')})
Spy on Methods
it('should spy on method',()=>{constobj={method:()=>'original'}constspy=spyOn(obj,'method')obj.method()expect(spy).toHaveBeenCalled()})
import{mock}from'@stacksjs/testing'// Mock entire modulemock.module('@stacksjs/email',()=>({send:mock(()=>Promise.resolve({sent:true}))}))// In testit('should send email',async()=>{constresult=awaitsendWelcomeEmail('user@test.com')expect(result.sent).toBe(true)})
Time Testing
Freezing Time
import{freezeTime,travelTo}from'@stacksjs/testing'it('should test time-dependent code',()=>{// Freeze timefreezeTime('2024-01-15 10:00:00')constnow=newDate()expect(now.toISOString()).toBe('2024-01-15T10:00:00.000Z')// Travel to specific timetravelTo(newDate('2024-06-01'))constfuture=newDate()expect(future.getMonth()).toBe(5)// June})
Test Utilities
Skip and Only
// Skip a testit.skip('should be skipped',()=>{// This test won't run})// Run only this testit.only('should run only this',()=>{// Only this test runs})// Skip describe blockdescribe.skip('Skipped Suite',()=>{// All tests skipped})
Todo Tests
it.todo('should implement this feature')
Test Timeout
it('should complete within timeout',async()=>{awaitlongRunningOperation()},10000)// 10 second timeout
Running Tests
CLI Commands
# Run all testsbuntest# Run specific filebuntesttests/user.test.ts# Run tests matching patternbuntest--filter"User"# Watch modebuntest--watch# With coveragebuntest--coverage