Testing
Testing: getting started
Oriel apps are tested with Vitest. A new app has tests ready to run, a TestCase that gives each test a fresh
application, and helpers to send requests, run commands and check the database.
Introduction#
tests has two folders: tests/ for tests that send requests to your app or run its commands, and
tests/ for tests of one piece of code on its own. A test file's name ends in .test.ts, or .test.tsx for one
that renders TSX.
tests/ is the class your tests boot the app with, and vitest.config.ts sets Vitest up for your app. A
test file boots the test case once, at the top, and uses it in each test:
import { test } from 'vitest'import { TestCase } from '../TestCase'const t = TestCase.boot()test('the application returns a successful response', async () => { const response = await t.get('/') response.assertOk()})TestCase.boot() registers Vitest's beforeEach and afterEach hooks: before each test, it creates and boots a new
application; after it, it tears that application down. No state survives from one test to the next.
Environment#
Before each application is created, the test case sets these variables, unless they are already set:
| Variable | Value |
|---|---|
APP_ENV | testing |
BCRYPT_ROUNDS | 4 |
CACHE_STORE | array |
DB_CONNECTION | sqlite |
DB_DATABASE | :memory: |
MAIL_MAILER | array |
QUEUE_CONNECTION | sync |
SESSION_DRIVER | array |
So your tests send no mail, run jobs at once, and each test gets its own empty in-memory database. A new app's
vitest.config.ts sets the same values as Vitest's env, because Bun reads .env before the tests start. Vitest's
env replaces what your shell or your CI sets, so to run your tests with another value, such as a MySQL database,
change it in vitest.config.ts.
With APP_ENV set to testing, a .env.testing file, when you add one, is read in place of .env. For the other
variables, what you set in your shell or your CI wins over both files.
To change the app before it boots, override defineEnvironment() in your TestCase:
import type { Application } from '@orieljs/framework'import { ConfigRepository } from '@orieljs/framework/contracts'import { TestCase as BaseTestCase } from '@orieljs/framework/testing'import { afterEach, beforeEach } from 'vitest'import { createApplication } from '../bootstrap/app'export class TestCase extends BaseTestCase { static override hooks = { beforeEach, afterEach } override createApplication(): Promise<Application> { return createApplication() } protected override defineEnvironment(app: Application): void { app.make(ConfigRepository).set('mail.default', 'array') }}Creating tests#
make:test writes a test in tests/, and with --unit in tests/:
node oriel make:test UserTestnode oriel make:test UserTest --unitThe file is named after the test without its Test: tests/. Both write the same Vitest test, with
test(), describe() and expect(); for a feature test, import your TestCase and add const t = TestCase.boot(). A
unit test that doesn't need the app leaves it out:
import { expect, test } from 'vitest'test('true is true', () => { expect(true).toBe(true)})t.app is the current test's application, inside a test: t.app.make(Service) resolves a service from it.
Running tests#
oriel test runs your tests with Vitest:
node oriel testnpm test does the same, and npm run test:bun runs them on Bun. Name files or folders to run only those, and
--filter runs the tests whose names match:
node oriel test tests/Feature/User.test.tsnode oriel test --filter="the application returns"Anything after -- goes to Vitest as it is, such as node oriel test -- --coverage for a coverage report (with
@vitest/ installed).
Running tests in parallel#
Vitest runs your test files at the same time, in several workers. With the default in-memory SQLite database, each test has its own database, so tests can't see each other's rows.
Test traits#
A trait adds setup to the tests of a file. RefreshDatabase gives each test a migrated database:
import { RefreshDatabase } from '@orieljs/framework/testing'import { test } from 'vitest'import { TestCase } from '../TestCase'const t = TestCase.boot({ uses: [RefreshDatabase] })test('posts can be listed', async () => { ;(await t.get('/posts')).assertOk()})Database testing covers it and the other database traits.
What the test case gives you#
| Helper | Page |
|---|---|
t.get(), t.post(), t.actingAs() and the response's assertions | HTTP tests |
t.artisan() | Testing commands |
Live.test() | Live tests |
Mail.fake(), Queue.fake() and the other fakes | Fakes |
t.assertDatabaseHas() and the database traits | Database testing |
t.travelTo(), t.freezeTime() | Time travel |
While your tests run, the app runs as a console app in the testing environment: CSRF tokens aren't checked, so your
tests can post forms without one.