Skip to content

Commit 8a5cd06

Browse files
authored
feat: assertion testing helpers (#1161)
1 parent 0035574 commit 8a5cd06

7 files changed

Lines changed: 820 additions & 10 deletions

File tree

commands/migration/_base.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,10 @@ export default abstract class MigrationsBase extends BaseCommand {
231231
* Run migrations
232232
*/
233233
await migrator.run()
234+
if (migrator.error) {
235+
this.error = migrator.error
236+
this.exitCode = 1
237+
}
234238

235239
/**
236240
* Log all pending files. This will happen, when one of the migration

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"./orm": "./build/src/orm/main.js",
2929
"./orm/relations": "./build/src/orm/relations/main.js",
3030
"./seeders": "./build/src/seeders/main.js",
31+
"./plugins/db": "./build/src/plugins/japa/db.js",
3132
"./services/*": "./build/services/*.js",
3233
"./types/*": "./build/src/types/*.js",
3334
"./migration": "./build/src/migration/main.js",

src/plugins/japa/db.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* @adonisjs/lucid
3+
*
4+
* (c) AdonisJS
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import type { PluginFn } from '@japa/runner/types'
11+
import { TestContext } from '@japa/runner/core'
12+
import type { ApplicationService } from '@adonisjs/core/types'
13+
import { DatabaseTestAssertions } from '../../test_utils/assertions.js'
14+
15+
declare module '@japa/runner/core' {
16+
interface TestContext {
17+
db: DatabaseTestAssertions
18+
}
19+
}
20+
21+
/**
22+
* Japa plugin that adds database assertion methods
23+
* to the test context via `({ db }) => { ... }`.
24+
*/
25+
export function dbAssertions(app: ApplicationService): PluginFn {
26+
return function () {
27+
TestContext.getter('db', () => new DatabaseTestAssertions(app), true)
28+
}
29+
}

src/test_utils/assertions.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* @adonisjs/lucid
3+
*
4+
* (c) AdonisJS
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import { AssertionError } from 'node:assert'
11+
import { type ApplicationService } from '@adonisjs/core/types'
12+
import { type LucidRow, type LucidModel } from '../types/model.js'
13+
14+
/**
15+
* Provides database assertion methods for use inside test bodies.
16+
* Exposed on Japa's TestContext as `db`.
17+
*/
18+
export class DatabaseTestAssertions {
19+
constructor(
20+
protected app: ApplicationService,
21+
protected connectionName?: string
22+
) {}
23+
24+
/**
25+
* Returns a new instance of assertions configured for the given connection
26+
*/
27+
connection(connectionName: string) {
28+
return new DatabaseTestAssertions(this.app, connectionName)
29+
}
30+
31+
/**
32+
* Returns the Database instance from the container
33+
*/
34+
async #getDb() {
35+
return this.app.container.make('lucid.db')
36+
}
37+
38+
/**
39+
* Returns a query client for the configured connection
40+
*/
41+
async #getConnection() {
42+
const db = await this.#getDb()
43+
return db.connection(this.connectionName)
44+
}
45+
46+
/**
47+
* Assert that the given table has rows matching the provided data.
48+
* When `count` is provided, asserts that exactly that many matching rows exist.
49+
*/
50+
async assertHas(table: string, data: Record<string, any>, count?: number) {
51+
const connection = await this.#getConnection()
52+
const result: any = await connection.query().from(table).where(data).count('* as count')
53+
const actualCount = Number(result[0].count)
54+
55+
if (count !== undefined && actualCount !== count) {
56+
throw new AssertionError({
57+
message: `Expected table '${table}' to have ${count} rows matching ${JSON.stringify(data)}, but found ${actualCount}`,
58+
actual: actualCount,
59+
expected: count,
60+
operator: 'assertHas',
61+
})
62+
}
63+
64+
if (count === undefined && actualCount === 0) {
65+
throw new AssertionError({
66+
message: `Expected table '${table}' to have rows matching ${JSON.stringify(data)}, but none were found`,
67+
actual: 0,
68+
expected: '>= 1',
69+
operator: 'assertHas',
70+
})
71+
}
72+
}
73+
74+
/**
75+
* Assert that the given table has no rows matching the provided data.
76+
*/
77+
async assertMissing(table: string, data: Record<string, any>) {
78+
const connection = await this.#getConnection()
79+
const result: any = await connection.query().from(table).where(data).count('* as count')
80+
const actualCount = Number(result[0].count)
81+
82+
if (actualCount > 0) {
83+
throw new AssertionError({
84+
message: `Expected table '${table}' to have no rows matching ${JSON.stringify(data)}, but found ${actualCount}`,
85+
actual: actualCount,
86+
expected: 0,
87+
operator: 'assertMissing',
88+
})
89+
}
90+
}
91+
92+
/**
93+
* Assert that the given table has exactly the expected number of rows.
94+
*/
95+
async assertCount(table: string, expectedCount: number) {
96+
const connection = await this.#getConnection()
97+
const result: any = await connection.query().from(table).count('* as count')
98+
const actualCount = Number(result[0].count)
99+
100+
if (actualCount !== expectedCount) {
101+
throw new AssertionError({
102+
message: `Expected table '${table}' to have ${expectedCount} rows, but found ${actualCount}`,
103+
actual: actualCount,
104+
expected: expectedCount,
105+
operator: 'assertCount',
106+
})
107+
}
108+
}
109+
110+
/**
111+
* Assert that the given table is empty (has no rows).
112+
*/
113+
async assertEmpty(table: string) {
114+
return this.assertCount(table, 0)
115+
}
116+
117+
/**
118+
* Assert that a model instance exists in the database.
119+
*/
120+
async assertModelExists(model: LucidRow) {
121+
const Model = model.constructor as LucidModel
122+
const primaryKeyValue = model.$primaryKeyValue
123+
124+
if (primaryKeyValue === undefined) {
125+
throw new Error(`Cannot assert model existence: primary key value is undefined`)
126+
}
127+
128+
const result = await Model.find(primaryKeyValue, { connection: this.connectionName })
129+
130+
if (!result) {
131+
throw new AssertionError({
132+
message: `Expected '${Model.name}' model with primary key ${primaryKeyValue} to exist, but it was not found`,
133+
actual: 'missing',
134+
expected: 'exists',
135+
operator: 'assertModelExists',
136+
})
137+
}
138+
}
139+
140+
/**
141+
* Assert that a model instance does not exist in the database.
142+
*/
143+
async assertModelMissing(model: LucidRow) {
144+
const Model = model.constructor as LucidModel
145+
const primaryKeyValue = model.$primaryKeyValue
146+
147+
if (primaryKeyValue === undefined) {
148+
throw new Error(`Cannot assert model absence: primary key value is undefined`)
149+
}
150+
151+
const result = await Model.find(primaryKeyValue, { connection: this.connectionName })
152+
153+
if (result) {
154+
throw new AssertionError({
155+
message: `Expected '${Model.name}' model with primary key ${primaryKeyValue} to not exist, but it was found`,
156+
actual: 'exists',
157+
expected: 'missing',
158+
operator: 'assertModelMissing',
159+
})
160+
}
161+
}
162+
}

test-helpers/index.ts

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ import { Chance } from 'chance'
1212
import { join } from 'node:path'
1313
import knex, { type Knex } from 'knex'
1414
import { fileURLToPath } from 'node:url'
15-
import { getActiveTest, getActiveTestOrFail } from '@japa/runner'
1615
import { Logger } from '@adonisjs/core/logger'
1716
import { Emitter } from '@adonisjs/core/events'
1817
import { type Application } from '@adonisjs/core/app'
1918
import { AppFactory } from '@adonisjs/core/factories/app'
19+
import { getActiveTest, getActiveTestOrFail } from '@japa/runner'
2020

2121
import {
2222
type DatabaseConfig,
@@ -55,10 +55,10 @@ export const logger = new Logger({})
5555
export const createEmitter = () => new Emitter<any>(app)
5656

5757
/**
58-
* Returns config based upon DB set in environment variables
58+
* Returns config for a given database dialect
5959
*/
60-
export function getConfig(): ConnectionConfig {
61-
switch (process.env.DB) {
60+
export function getConfigForDb(db: string): ConnectionConfig {
61+
switch (db) {
6262
case 'sqlite':
6363
return {
6464
client: 'sqlite3',
@@ -145,10 +145,17 @@ export function getConfig(): ConnectionConfig {
145145
},
146146
}
147147
default:
148-
throw new Error(`Missing test config for ${process.env.DB} connection`)
148+
throw new Error(`Missing test config for ${db} connection`)
149149
}
150150
}
151151

152+
/**
153+
* Returns config based upon DB set in environment variables
154+
*/
155+
export function getConfig(): ConnectionConfig {
156+
return getConfigForDb(process.env.DB!)
157+
}
158+
152159
/**
153160
* Returns an instance of knex for testing
154161
*/
@@ -166,11 +173,9 @@ export function getKnex(config: Knex.Config): Knex {
166173
}
167174

168175
/**
169-
* Does base setup by creating databases
176+
* Creates all test tables on the given knex connection
170177
*/
171-
export async function setup(destroyDb: boolean = true) {
172-
const db = getKnex(Object.assign({}, getConfig(), { debug: false }))
173-
178+
export async function setupDb(db: Knex, destroyDb: boolean = true) {
174179
const hasUsersTable = await db.schema.hasTable('users')
175180
if (!hasUsersTable) {
176181
await db.schema.createTable('users', (table) => {
@@ -315,6 +320,14 @@ export async function setup(destroyDb: boolean = true) {
315320
}
316321
}
317322

323+
/**
324+
* Does base setup by creating databases
325+
*/
326+
export async function setup(destroyDb: boolean = true) {
327+
const db = getKnex(Object.assign({}, getConfig(), { debug: false }))
328+
await setupDb(db, destroyDb)
329+
}
330+
318331
/**
319332
* Does cleanup removes database
320333
*/
@@ -489,6 +502,40 @@ export function getDb(eventEmitter?: Emitter<any>, config?: DatabaseConfig) {
489502
return db
490503
}
491504

505+
/**
506+
* Returns a Database instance with two connections backed by different
507+
* databases (SQLite for primary, PostgreSQL for secondary).
508+
* Both databases are set up with all test tables.
509+
*/
510+
export async function getMultiConnectionDb() {
511+
const primaryConfig = getConfigForDb('sqlite')
512+
const secondaryConfig = getConfigForDb('pg')
513+
514+
const primaryKnex = getKnex(Object.assign({}, primaryConfig, { debug: false }))
515+
const secondaryKnex = getKnex(Object.assign({}, secondaryConfig, { debug: false }))
516+
await setupDb(primaryKnex)
517+
await setupDb(secondaryKnex)
518+
519+
const db = new Database(
520+
{
521+
connection: 'primary',
522+
connections: {
523+
primary: primaryConfig,
524+
secondary: secondaryConfig,
525+
},
526+
},
527+
logger,
528+
createEmitter()
529+
)
530+
531+
const test = getActiveTest()
532+
test?.cleanup(async () => {
533+
await db.manager.closeAll()
534+
})
535+
536+
return db
537+
}
538+
492539
/**
493540
* Returns true when the current test database dialect supports schema dump
494541
* creation/restoration via external CLI tools.

test/bindings/transformer.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ test.group('Transformer | withCount | HasMany', (group) => {
9999
{ user_id: 2, title: 'Adonis 102' },
100100
])
101101

102-
const users = await User.query().withCount('posts')
102+
const users = await User.query().orderBy('id', 'asc').withCount('posts')
103103
const result = await serializer.serialize(UserTransformer.transform(users))
104104

105105
assert.lengthOf(result, 2)

0 commit comments

Comments
 (0)