Skip to the content
OrielDocs
0.1.0-alpha.10GitHub

Testing

HTTP tests

Your tests send requests to your app and check the responses. The requests run inside the test, through your routes and middleware, with no server and no network.

Introduction#

t.get() sends a request and returns a TestResponse, whose assertions check the status, the headers, the content, the session and more:

tests/Feature/Example.test.ts
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()})

Await the request; the assertions that follow are synchronous, and each returns the response, so you can chain them.

Making requests#

t.get(), t.post(), t.put(), t.patch(), t.delete(), t.options() and t.head() send a request with that method. The ones with a body take the data as their second argument, sent as a form:

const response = await t.post('/posts', { title: 'Hello', body: 'My first post' })response.assertRedirect('/posts')

call(method, uri, data) sends any method. Within one test, the requests share their cookies and session, as one browser's would: a user who signs in on one request is signed in on the next.

Customizing request headers#

withHeaders() adds headers to every request that follows in the test, and withHeader() one:

const response = await t.withHeaders({ 'X-Header': 'Value' }).post('/user', { name: 'Sally' })response.assertCreated()

withToken('secret') sends Authorization: Bearer secret, withBasicAuth(username, password) a basic one, and withoutToken() takes it out again.

Cookies#

withCookie() and withCookies() send cookies, encrypted as your app's own cookies are; withUnencryptedCookie() sends one as it is:

const response = await t.withCookie('color', 'blue').get('/')

Session and authentication#

withSession() puts data in the session before a request, and flushSession() empties it. actingAs() signs a user in for every request that follows:

tests/Feature/Dashboard.test.ts
import { RefreshDatabase } from '@orieljs/framework/testing'import { test } from 'vitest'import { User } from '../../app/Models/User'import { TestCase } from '../TestCase'const t = TestCase.boot({ uses: [RefreshDatabase] })test('signed-in users can see the dashboard', async () => {  const user = await User.factory().create()  const response = await t.actingAs(user).withSession({ banned: false }).get('/dashboard')  response.assertOk()})

actingAs(user, 'token') signs the user in on another guard, here the token guard, which becomes the default for the requests.

Redirects and the previous page#

After followingRedirects(), each request of the test follows its redirects, with GET, and returns the last response. from(url) sets the page the request comes from, so a back() redirect goes there:

const response = await t.from('/posts/create').post('/posts', {})response.assertRedirect('/posts/create')

Debugging responses#

dump() prints the response's content, or with a path, a value of its JSON. response.content is the body, response.json() the decoded JSON, and response.status and response.headers what they say.

Exception handling#

An error your code throws is rendered by your app's exception handler, as it would be for a visitor, so a test can check the 500 or 404 it gets. response.exception is the error. withoutExceptionHandling() lets errors escape from the request instead, so the test fails with the error and its stack:

const response = await t.withoutExceptionHandling().get('/')

Testing JSON APIs#

getJson(), postJson(), putJson(), patchJson(), deleteJson() and json(method, uri, data) ask for JSON back, and the ones with a body send it as JSON:

tests/Feature/Api.test.ts
import { test } from 'vitest'import { TestCase } from '../TestCase'const t = TestCase.boot()test('making an API request', async () => {  const response = await t.postJson('/api/user', { name: 'Sally' })  response.assertCreated().assertJson({ created: true })})

assertJson() passes when the response's JSON contains the data, whatever else it has. assertExactJson() wants the JSON to be exactly the data, and assertJsonPath() checks one value:

response.assertJsonPath('team.owner.name', 'Darian')

response.json('team.owner.name') reads a value, to check it with Vitest's expect().

Fluent JSON testing#

assertJson() also takes a function, which checks the JSON one key at a time:

response.assertJson((json) =>  json    .where('id', 1)    .where('name', 'Victoria Faith')    .where('email', (email: string) => email.endsWith('@example.com'))    .whereNot('status', 'pending')    .missing('password')    .etc(),)

Each key of the JSON must be checked, or the assertion fails with "Unexpected properties were found on the root level.". etc() allows the keys you don't check.

MethodChecks
has(key), has(key, count)The key is there, with that many items
hasAll(keys), hasAny(keys)Every key is there, or one of them is
missing(key), missingAll(keys)The key isn't there
count(key, n), countBetween(key, min, max)The number of items
where(key, value), whereNot(key, value)The value is that value, or isn't; with a function, it returns true, or false
whereNull(key), whereNotNull(key)The value is null, or isn't
whereAll(values)Several values at once
whereType(key, type), whereAllType(types)The type: 'string', 'integer', 'number', 'boolean', 'array' (a list or an object, as in Laravel), 'object' or 'null', or a list of them
whereContains(key, value)A list or string contains the value

A key can be a dotted path: where('user.name', 'Sally').

Asserting against JSON collections#

has(3) checks the root's size, and first() scopes the checks to its first item:

response.assertJson((json) =>  json.has(3).first((user) => user.where('id', 1).where('name', 'Victoria Faith').etc()),)

has(3) checks the root is a list of 3. has('users', 3, (user) => ...) checks the users list and its first item, first() the first item, and each() every item.

Scoping JSON assertions#

has() with a function scopes the checks to one key:

response.assertJson((json) =>  json.has('meta').has('users', 3, (user) => user.where('id', 1).missing('password').etc()),)

A scope must check each of its keys too, unless it calls etc().

Testing Inertia pages#

assertInertia() checks that a response is an Inertia page, and checks its component and props with the same fluent methods:

tests/Feature/Posts.test.ts
import { RefreshDatabase } from '@orieljs/framework/testing'import { test } from 'vitest'import { User } from '../../app/Models/User'import { TestCase } from '../TestCase'const t = TestCase.boot({ uses: [RefreshDatabase] })test('the posts page lists the posts', async () => {  const user = await User.factory().create()  const response = await t.actingAs(user).get('/posts')  response.assertOk().assertInertia((page) => page.component('Posts/Index').has('posts', 0).where('auth.user.id', user.id))})

It works on a first visit, whose page is in the HTML, and on an Inertia visit, whose page is JSON. component() also checks that the page's file exists in resources/js/pages; inertia.testing.ensure_pages_exist set to false turns that off.

MethodChecks
component(name)The page component
url(url), version(version)The page's URL and asset version
hasFlash(key, value?), missingFlash(key)A flashed value, in page.flash
reload(callback), reloadOnly(props, callback), reloadExcept(props, callback)Requests the page again, as a partial reload, and checks it
loadDeferredProps(callback), loadDeferredProps(groups, callback)Loads the deferred props, and checks them

The reloads are requests, so the callback that uses them is async, and assertInertia() returns a promise to await:

await response.assertInertia((page) => page.component('Posts/Index').loadDeferredProps((reloaded) => reloaded.has('stats')))

response.assertInertiaFlash('status', 'Saved.') checks a flashed value without a callback.

Testing file uploads#

FakeFile makes files to upload. Give one in the request's data, and the request is sent as multipart/form-data:

tests/Feature/Avatar.test.ts
import { Storage } from '@orieljs/framework'import { FakeFile } from '@orieljs/framework/testing'import { test } from 'vitest'import { TestCase } from '../TestCase'const t = TestCase.boot()test('avatars can be uploaded', async () => {  const disk = Storage.fake('avatars')  const file = FakeFile.image('avatar.jpg')  const response = await t.post('/avatar', { avatar: file })  response.assertRedirect()  await disk.assertExists('avatar.jpg')})

The route here stores the file under its own name. FakeFile.image(name, width, height) makes a real image, in the format its name's extension asks for: PNG, JPEG, GIF, WebP or BMP, so the image, mimes and dimensions rules pass. FakeFile.create(name, kilobytes) makes a file of that size, typed from its name, or from a MIME type you give as a third argument:

import { FakeFile } from '@orieljs/framework/testing'const document = FakeFile.create('document.pdf', 500)const sheet = FakeFile.create('data.csv', 10, 'text/csv')

Testing views#

A response rendered from a view knows it: assertViewIs() checks its name, and assertViewHas() its data:

const response = await t.get('/')response.assertViewIs('welcome').assertViewHas('name', 'Taylor')

assertViewHas() takes a key, a key and value, a function that checks the value, or several keys and values. response.viewData('name') reads the view's data, and response.original is the view.

Available assertions#

Response assertions#

AssertionPasses when
assertStatus(code)The response has that status
assertOk(), assertCreated(), assertAccepted(), assertNoContent()200, 201, 202, or 204 with no body
assertMovedPermanently(), assertFound(), assertNotModified()301, 302, 304
assertBadRequest(), assertUnauthorized(), assertPaymentRequired(), assertForbidden(), assertNotFound()400, 401, 402, 403, 404
assertMethodNotAllowed(), assertNotAcceptable(), assertRequestTimeout(), assertConflict(), assertGone()405, 406, 408, 409, 410
assertUnsupportedMediaType(), assertUnprocessable(), assertTooManyRequests()415, 422, 429
assertInternalServerError(), assertServiceUnavailable()500, 503
assertSuccessful(), assertClientError(), assertServerError()A 2xx, 4xx or 5xx status
assertRedirect(uri?), assertRedirectContains(text), assertRedirectToRoute(name, params?)A redirect, to that URI or route
assertLocation(uri)The Location header is that URI
assertHeader(name, value?), assertHeaderMissing(name)The header is there, with that value, or isn't
assertDownload(filename?)The response is a file download
assertContent(text)The body is exactly the text
assertSee(text), assertDontSee(text), assertSeeInOrder(texts)The body has the text, escaped unless you pass false
assertSeeText(text), assertDontSeeText(text), assertSeeTextInOrder(texts)The same, with the HTML tags taken out
assertJson(data), assertExactJson(data), assertJsonPath(path, value), assertJsonMissingPath(path)The JSON
assertJsonFragment(data), assertJsonMissing(data)The data appears somewhere in the JSON, or doesn't
assertJsonStructure(structure), assertJsonCount(count, key?)The JSON's keys, or the number of items
assertCookie(name, value?), assertPlainCookie(name, value?)The response sets the cookie, encrypted or not
assertCookieExpired(name), assertCookieNotExpired(name), assertCookieMissing(name)The cookie
assertViewIs(name), assertViewHas(key, value?), assertViewHasAll(data), assertViewMissing(key)The view

The Session page lists the session assertions: assertSessionHas(), assertSessionMissing(), assertSessionHasInput() and the others.

Authentication assertions#

These are on the test case, and check the user after the requests so far. They are async, so await them:

AssertionPasses when
await t.assertAuthenticated(guard?)A user is signed in
await t.assertGuest(guard?)No user is signed in
await t.assertAuthenticatedAs(user, guard?)That user is signed in

Validation assertions#

AssertionPasses when
assertValid(keys?)The response has no validation errors, for those keys or at all
assertInvalid(errors)It has errors for the keys, or with the messages: { name: 'The name field is required.' }
assertSessionHasErrors(keys), assertSessionHasNoErrors()The session has the errors a form's redirect carries
assertJsonValidationErrors(keys), assertJsonMissingValidationErrors(keys?)A JSON response has the errors

assertValid() and assertInvalid() read the errors from the session, or from a JSON response's errors, so they work for both kinds of request.

response.assertInvalid({ email: 'The email field must be a valid email address.' })

Privacy policy ·

esc