Testing
Fakes
A fake stands in for a service in your tests: it records what your code asks of it, does nothing else, and lets you check what was asked. Oriel has fakes for the bus, events, mail, notifications, queues and storage.
Introduction#
Mail.fake() and the other fake() methods put a fake in place of the service, for the rest of the test, and return
it. Check your code's work on the fake they return:
import { Mail } from '@orieljs/framework'import { test } from 'vitest'import { OrderShipped } from '../../app/Mail/OrderShipped'import { TestCase } from '../TestCase'const t = TestCase.boot()test('orders can be shipped', async () => { const mail = Mail.fake() await t.post('/orders/1/shipment') mail.assertSent(OrderShipped)})Each test has a new application, so a fake ends with its test.
Bus fake#
Bus.fake() records the jobs your code dispatches, and runs none of them:
import { Bus } from '@orieljs/framework'import { RefreshDatabase } from '@orieljs/framework/testing'import { test } from 'vitest'import { OptimizePodcast } from '../../app/Jobs/OptimizePodcast'import { ProcessPodcast } from '../../app/Jobs/ProcessPodcast'import { TestCase } from '../TestCase'const t = TestCase.boot({ uses: [RefreshDatabase] })test('podcasts can be published', async () => { const bus = Bus.fake() await t.post('/podcasts', { title: 'Episode 1' }) bus.assertDispatched(ProcessPodcast, (job) => job.podcast.title === 'Episode 1') bus.assertDispatched(ProcessPodcast, 1) bus.assertNotDispatched(OptimizePodcast)})The second argument is a function that receives each job and returns true for the one you expect, or a number of
dispatches. Give fake() a list of jobs to fake only those, and the others run as usual: Bus.fake([ProcessPodcast]).
BusFake.fakeExcept([ShipOrder]), from @orieljs/, fakes all but those.
| Assertion | Passes when |
|---|---|
assertDispatched(job, filterOrCount?), assertDispatchedTimes(job, times) | The job was dispatched, matching the filter, or that many times |
assertNotDispatched(job, filter?), assertNothingDispatched() | It wasn't, or no job was |
assertDispatchedSync(job), assertNotDispatchedSync(job) | It was dispatched with dispatchSync(), or wasn't |
assertDispatchedAfterResponse(job), assertNotDispatchedAfterResponse(job) | It was dispatched to run after the response, or wasn't |
assertDispatchedWithoutChain(job) | It was dispatched with no chain |
bus.dispatched(ProcessPodcast) returns the dispatched jobs, to check them with Vitest's expect(), and
dispatchedSync() and dispatchedAfterResponse() the others.
Job chains#
assertChained() checks a chain, by class or by instance:
bus.assertChained([ProcessPodcast, OptimizePodcast, PublishPodcast])An instance in the list matches a job of its class with the same fields.
Job batches#
assertBatched() receives each batch, with its name and jobs, and passes when the function returns true for one:
bus.assertBatched((batch) => batch.name === 'import-csv' && batch.jobs.length === 10)assertBatchCount(count) checks the number of batches, and assertNothingBatched() that there are none.
Event fake#
Event.fake() records the events your code dispatches, and runs none of their listeners:
import { OrderShipped } from './app/Events/OrderShipped'const events = Event.fake()// Ship the order...events.assertDispatched(OrderShipped, (event) => event.orderId === 1)events.assertDispatchedTimes(OrderShipped, 1)events.assertNotDispatched('order.cancelled')assertNothingDispatched() checks that no event was dispatched at all.
assertListening(OrderShipped, SendShipmentNotification) checks that a listener is registered for an event, and
dispatched(event) returns the dispatched events.
EventFake.fakeExcept([OrderCreated]), from @orieljs/, fakes every event but those.
Mail fake#
Mail.fake() records each mail your code sends or queues, and sends nothing:
import { OrderShipped } from './app/Mail/OrderShipped'const mail = Mail.fake()// Ship the order...mail.assertSent(OrderShipped, (mailable, record) => record.hasTo('taylor@example.com'))mail.assertSent(OrderShipped, 1)mail.assertNotQueued(OrderShipped)The filter receives the mailable and its record, which has hasTo(), hasCc(), hasBcc() and hasSubject(). A
mailable that would be queued counts as queued, not sent.
| Assertion | Passes when |
|---|---|
assertSent(mailable, filterOrCount?), assertSentTimes(mailable, times) | The mail was sent, matching the filter, or that many times |
assertNotSent(mailable), assertNothingSent(), assertSentCount(count) | It wasn't sent, no mail was, or that many were |
assertQueued(mailable, filterOrCount?), assertNotQueued(mailable) | It was queued, or wasn't |
assertNothingQueued(), assertQueuedCount(count) | No mail was queued, or that many were |
assertOutgoingCount(count), assertNothingOutgoing() | That many mails were sent or queued, or none |
mail.sent(OrderShipped) and mail.queued(OrderShipped) return the mailables. Nothing is rendered, so the fake
doesn't check your views.
Notification fake#
Notification.fake() records each notification your code sends, and delivers none:
import { User } from './app/Models/User'import { InvoicePaid } from './app/Notifications/InvoicePaid'const notifications = Notification.fake()const user = await User.findOrFail(1)// Pay the invoice...notifications.assertSentTo(user, InvoicePaid, (notification, channels) => channels.includes('mail'))notifications.assertNotSentTo(user, InvoicePaid, (notification) => notification.amount === '0')notifications.assertCount(1)The filter receives the notification, its channels and the notifiable. assertSentTo() also takes a list of
notifiables, which must each have the notification.
| Assertion | Passes when |
|---|---|
assertSentTo(notifiable, notification, filterOrCount?) | The notifiable was sent the notification |
assertSentToTimes(notifiable, notification, times) | It was sent it that many times |
assertNotSentTo(notifiable, notification, filter?), assertNothingSentTo(notifiable) | It wasn't sent it, or anything |
assertSentTimes(notification, times), assertCount(count) | That many were sent, of that class or of all |
assertNothingSent() | No notification was sent |
assertSentOnDemand(notification, filterOrCount?) | It was sent to a route, with Notification.route() |
notifications.sent(user, InvoicePaid) returns the notifications the user was sent.
Queue fake#
Queue.fake() records the jobs pushed on the queue, and runs none of them. Unlike Bus.fake(), it sees the queue
each job is pushed on:
import { ProcessPodcast } from './app/Jobs/ProcessPodcast'const queue = Queue.fake()// Publish the podcast...queue.assertPushed(ProcessPodcast, 1)queue.assertPushedOn('audio', ProcessPodcast)queue.assertPushed(ProcessPodcast, (job, queueName) => queueName === 'audio')| Assertion | Passes when |
|---|---|
assertPushed(job, filterOrCount?), assertPushedTimes(job, times) | The job was pushed, matching the filter, or that many times |
assertPushedOn(queue, job, filter?) | It was pushed on that queue |
assertNotPushed(job, filter?), assertNothingPushed() | It wasn't, or no job was |
assertCount(count) | That many jobs were pushed |
assertPushedWithChain(job, chain), assertPushedWithoutChain(job) | It was pushed with that chain after it, or with none |
queue.pushed(ProcessPodcast) returns the pushed jobs. Give fake() a list of jobs to fake only those.
Storage fake#
Storage.fake() puts a disk in memory in place of one of yours, so a test writes no files, and returns it:
import { Storage } from '@orieljs/framework'import { FakeFile } from '@orieljs/framework/testing'import { test } from 'vitest'import { TestCase } from '../TestCase'const t = TestCase.boot()test('albums can be uploaded', async () => { const disk = Storage.fake('photos') await t.post('/photos', { photos: [FakeFile.image('photo1.jpg'), FakeFile.image('photo2.jpg')] }) await disk.assertExists(['photo1.jpg', 'photo2.jpg']) await disk.assertMissing('missing.jpg') await disk.assertCount('', 2)})With no name, it fakes the default disk. The assertions read the disk, so they are async: await each one.
| Assertion | Passes when |
|---|---|
assertExists(paths, contents?) | Each file is on the disk, with those contents |
assertMissing(paths) | No file is at the paths |
assertCount(folder, count, recursive?) | The folder has that many files |
assertDirectoryEmpty(folder) | The folder has no files |
Interacting with time#
t.travelTo(), t.travel() and t.freezeTime() move the clock your code reads. Time travel covers
them.