Skip to content

Commit

Permalink
feat: add DatabaseTestUtils class
Browse files Browse the repository at this point in the history
  • Loading branch information
Julien-R44 committed Jan 25, 2024
1 parent 84a6455 commit cb0dda0
Show file tree
Hide file tree
Showing 2 changed files with 288 additions and 0 deletions.
78 changes: 78 additions & 0 deletions src/test_utils/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* @adonisjs/lucid
*
* (c) AdonisJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import type { Kernel } from '@adonisjs/core/ace'

import type { Database } from '../database/main.js'

/**
* Database test utils are meant to be used during testing to
* perform common tasks like running migrations, seeds, etc.
*/
export class DatabaseTestUtils {
constructor(
protected kernel: Kernel,
protected db: Database,
protected connectionName?: string
) {}

/**
* Runs a command through Ace
*/
async #runCommand(commandName: string, args: string[] = []) {
if (this.connectionName) {
args.push(`--connection=${this.connectionName}`)
}

const command = await this.kernel.exec(commandName, args)
if (!command.exitCode) return

if (command.error) {
throw command.error
} else {
throw new Error(`"${commandName}" failed`)
}
}

/**
* Testing hook for running migrations ( if needed )
* Return a function to truncate the whole database but keep the schema
*/
async truncate() {
await this.#runCommand('migration:run', ['--compact-output'])
return () => this.#runCommand('db:truncate')
}

/**
* Testing hook for running seeds
*/
async seed() {
await this.#runCommand('db:seed')
}

/**
* Testing hook for running migrations
* Return a function to rollback the whole database
*
* Note that this is slower than truncate() because it
* has to run all migration in both directions when running tests
*/
async migrate() {
await this.#runCommand('migration:run', ['--compact-output'])
return () => this.#runCommand('migration:rollback', ['--compact-output'])
}

/**
* Testing hook for creating a global transaction
*/
async withGlobalTransaction() {
await this.db.beginGlobalTransaction(this.connectionName)
return () => this.db.rollbackGlobalTransaction(this.connectionName)
}
}
210 changes: 210 additions & 0 deletions test/test_utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
* @adonisjs/lucid
*
* (c) AdonisJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { test } from '@japa/runner'
import { ListLoader } from '@adonisjs/core/ace'
import { AceFactory } from '@adonisjs/core/factories'

import DbSeed from '../commands/db_seed.js'
import { getDb } from '../test-helpers/index.js'
import Migrate from '../commands/migration/run.js'
import DbTruncate from '../commands/db_truncate.js'
import Rollback from '../commands/migration/rollback.js'
import { DatabaseTestUtils } from '../src/test_utils/database.js'

test.group('Database Test Utils', () => {
test('truncate() should run migration:run and db:truncate commands', async ({ fs, assert }) => {
let migrationRun = false
let truncateRun = false

const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} })

class FakeMigrate extends Migrate {
override async run() {
migrationRun = true
}
}

class FakeDbTruncate extends DbTruncate {
override async run() {
truncateRun = true
}
}

ace.addLoader(new ListLoader([FakeMigrate, FakeDbTruncate]))

const dbTestUtils = new DatabaseTestUtils(ace, getDb())
const truncate = await dbTestUtils.truncate()

await truncate()

assert.isTrue(migrationRun)
assert.isTrue(truncateRun)
})

test('truncate() with custom connectionName', async ({ fs, assert }) => {
assert.plan(2)

const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} })

class FakeMigrate extends Migrate {
override async run() {
assert.equal(this.connection, 'secondary')
}
}

class FakeDbTruncate extends DbTruncate {
override async run() {
assert.equal(this.connection, 'secondary')
}
}

ace.addLoader(new ListLoader([FakeMigrate, FakeDbTruncate]))

const dbTestUtils = new DatabaseTestUtils(ace, getDb(), 'secondary')
const truncate = await dbTestUtils.truncate()

await truncate()
})

test('seed() should run db:seed command', async ({ fs, assert }) => {
assert.plan(1)

const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} })

class FakeDbSeed extends DbSeed {
override async run() {
assert.isTrue(true)
}
}

ace.addLoader(new ListLoader([FakeDbSeed]))

const dbTestUtils = new DatabaseTestUtils(ace, getDb())
await dbTestUtils.seed()
})

test('seed() with custom connectionName', async ({ fs, assert }) => {
assert.plan(1)

const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} })

class FakeDbSeed extends DbSeed {
override async run() {
assert.equal(this.connection, 'secondary')
}
}

ace.addLoader(new ListLoader([FakeDbSeed]))

const dbTestUtils = new DatabaseTestUtils(ace, getDb(), 'secondary')
await dbTestUtils.seed()
})

test('migrate() should run migration:run and migration:rollback commands', async ({
fs,
assert,
}) => {
let migrationRun = false
let rollbackRun = false

const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} })

class FakeMigrate extends Migrate {
override async run() {
migrationRun = true
}
}

class FakeMigrationRollback extends Rollback {
override async run() {
rollbackRun = true
}
}

ace.addLoader(new ListLoader([FakeMigrate, FakeMigrationRollback]))

const dbTestUtils = new DatabaseTestUtils(ace, getDb())
const rollback = await dbTestUtils.migrate()

await rollback()

assert.isTrue(migrationRun)
assert.isTrue(rollbackRun)
})

test('migrate() with custom connectionName', async ({ fs, assert }) => {
assert.plan(2)

const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} })

class FakeMigrate extends Migrate {
override async run() {
assert.equal(this.connection, 'secondary')
}
}

class FakeMigrationRollback extends Rollback {
override async run() {
assert.equal(this.connection, 'secondary')
}
}

ace.addLoader(new ListLoader([FakeMigrate, FakeMigrationRollback]))

const dbTestUtils = new DatabaseTestUtils(ace, getDb(), 'secondary')
const rollback = await dbTestUtils.migrate()

await rollback()
})

test('should throw error when command has an exitCode = 1', async ({ fs }) => {
const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} })

class FakeMigrate extends Migrate {
override async run() {
this.exitCode = 1
}
}

ace.addLoader(new ListLoader([FakeMigrate]))

const dbTestUtils = new DatabaseTestUtils(ace, getDb())
await dbTestUtils.migrate()
}).throws('"migration:run" failed')

test('should re-use command.error message if available', async ({ fs }) => {
const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} })

class FakeMigrate extends Migrate {
override async run() {
this.exitCode = 1
this.error = new Error('Custom error message')
}
}

ace.addLoader(new ListLoader([FakeMigrate]))

const dbTestUtils = new DatabaseTestUtils(ace, getDb())
await dbTestUtils.migrate()
}).throws('Custom error message')

test('withGlobalTransaction should wrap and rollback a transaction', async ({ fs, assert }) => {
const db = getDb()
const ace = await new AceFactory().make(fs.baseUrl, { importer: () => {} })
const dbTestUtils = new DatabaseTestUtils(ace, db)
const rollback = await dbTestUtils.withGlobalTransaction()

assert.isDefined(db.connectionGlobalTransactions.get(db.primaryConnectionName))

await rollback()

assert.isUndefined(db.connectionGlobalTransactions.get(db.primaryConnectionName))
})
})

0 comments on commit cb0dda0

Please sign in to comment.