{"id":14967420,"url":"https://github.com/andrewjo/mocha-slonik","last_synced_at":"2025-10-25T19:31:03.892Z","repository":{"id":40382061,"uuid":"416600201","full_name":"AndrewJo/mocha-slonik","owner":"AndrewJo","description":"Slonik transaction support for Mocha test framework ☕️🐘","archived":false,"fork":false,"pushed_at":"2024-09-13T07:23:27.000Z","size":1878,"stargazers_count":4,"open_issues_count":3,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-10-10T22:42:56.223Z","etag":null,"topics":["mocha","mocha-plugin","mochajs","mock","postgres","postgresql","slonik","testing","transaction"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-2-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AndrewJo.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-10-13T05:23:15.000Z","updated_at":"2024-07-11T22:37:02.000Z","dependencies_parsed_at":"2023-02-15T04:01:06.095Z","dependency_job_id":"cf722367-e7e4-4451-b26d-48d41b6d5277","html_url":"https://github.com/AndrewJo/mocha-slonik","commit_stats":{"total_commits":189,"total_committers":6,"mean_commits":31.5,"dds":0.5079365079365079,"last_synced_commit":"3d20d0e163da291f940036b0f70b20afdd6e64e8"},"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndrewJo%2Fmocha-slonik","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndrewJo%2Fmocha-slonik/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndrewJo%2Fmocha-slonik/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndrewJo%2Fmocha-slonik/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AndrewJo","download_url":"https://codeload.github.com/AndrewJo/mocha-slonik/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":219865150,"owners_count":16555931,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["mocha","mocha-plugin","mochajs","mock","postgres","postgresql","slonik","testing","transaction"],"created_at":"2024-09-24T13:38:02.082Z","updated_at":"2025-10-25T19:31:03.501Z","avatar_url":"https://github.com/AndrewJo.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# mocha-slonik\n\n[![npm](https://img.shields.io/npm/v/mocha-slonik?style=flat-square)][npm]\n[![CircleCI](https://img.shields.io/circleci/build/github/AndrewJo/mocha-slonik/master?style=flat-square)][circleci]\n[![Codecov branch](https://img.shields.io/codecov/c/github/AndrewJo/mocha-slonik/master?style=flat-square)][codecov]\n[![GitHub](https://img.shields.io/github/license/AndrewJo/mocha-slonik?style=flat-square)](./LICENSE)\n[![npm](https://img.shields.io/npm/dw/mocha-slonik?style=flat-square)][npm]\n\n[Slonik][slonik] transaction support for [Mocha][mocha] test framework.\n\n## Table of Contents\n\n- [Why does mocha-slonik exist?](#why-does-mocha-slonik-exist)\n  - [Slonik’s `createMockPool` vs `mocha-slonik`](#sloniks-createmockpool-vs-mocha-slonik)\n- [How it works](#how-it-works)\n- [Version compatibility](#version-compatibility)\n- [Installation](#installation)\n- [Usage](#usage)\n  - [Without Mocha Root Hook plugin](#without-mocha-root-hook-plugin)\n  - [With Mocha Root Hook plugin](#with-mocha-root-hook-plugin)\n- [API](#api)\n  - [`rollback`](#rollback)\n  - [`currentTransaction`](#currenttransaction)\n- [Best practices](#best-practices)\n  - [Always rollback transaction in `after`/`afterEach` block](#always-rollback-transaction-in-afteraftereach-block)\n  - [Use global Root Hook to DRY up boilerplating](#use-global-root-hook-to-dry-up-boilerplating)\n- [Developing](#developing)\n- [Running tests](#running-tests)\n- [Limitations](#limitations)\n  - [Lack of `copyFromBinary` support](#lack-of-copyfrombinary-support)\n\n## Why does mocha-slonik exist?\n\nThis package exists as a way for developers to **run integration tests against a real database\nwithout having to worry about cleaning up the test data after each test**. Since each test cases\nare wrapped in its own transaction that is automatically rolled back, you can ensure that your\ntests are isolated from each other: an earlier test that affects a row does not affect subsequent\ntests that may depend on the same row.\n\n### Slonik’s `createMockPool` vs `mocha-slonik`\n\n[Slonik][slonik] already provides a way to mock queries against the database by using a combination\nof `createMockPool` and `createMockQueryResult`. This is usually sufficient for lightweight unit\ntesting individual functions that calls Slonik query methods and is great for running tests that\ndoes not need a running instance of PostgreSQL.\n\nSee: [Mocking Slonik][slonik-user-content-mocking-slonik]\n\n## How it works\n\nmocha-slonik is a [Root Hook Plugin][root-hook-plugin] for [Mocha][mocha] that utilizes\n[ts-mock-imports][ts-mock-imports] to return a stubbed bindPool function that wrap most of the\n[Slonik query methods][slonik-query-methods] in a transaction that automatically rolls back after\neach test.\n\n## Version compatibility\n\nmocha-slonik follows [Semantic Versioning][semver] specification. Each major version breaks\nbackwards compatibility with [Slonik][slonik] and [Mocha][mocha] versions.\n\nRefer to the compatibility chart below for picking the mocha-slonik version that works with Slonik\nand Mocha versions in your project.\n\n|  mocha-slonik |                            slonik |   mocha |\n| ------------: | --------------------------------: | ------: |\n|        ^9.0.0 |                  \u003e=35.0.0 \u003c38.0.0 | ^10.2.0 |\n|        ^8.0.0 |                  \u003e=34.0.0 \u003c35.0.0 | ^10.1.0 |\n|        ^7.0.0 |                  \u003e=33.1.1 \u003c34.0.0 | ^10.1.0 |\n|        ^6.0.0 |                  \u003e=33.0.0 \u003c33.1.1 | ^10.1.0 |\n|        ^5.0.0 | ^30.0.0 \\|\\| ^31.0.0 \\|\\| ^32.0.0 | ^10.1.0 |\n| ≥4.2.0 \u003c5.0.0 | ^27.0.0 \\|\\| ^28.0.0 \\|\\| ^29.0.0 |  ≥9.2.2 |\n| ≥4.1.0 \u003c4.2.0 |              ^27.0.0 \\|\\| ^28.0.0 |  ^9.1.0 |\n|        ~4.0.0 |                           ^27.0.0 |  ^9.1.0 |\n|        ^3.0.0 |                           ^26.0.0 |  ^9.1.0 |\n|        ^2.0.0 |                           ^25.0.0 |  ^9.1.0 |\n|        ^1.0.0 |                           ^24.0.0 |  ^9.1.0 |\n\n## Installation\n\nInstall this library as a dev dependency in your project:\n\n```sh\nnpm i -D mocha-slonik\n```\n\n## Usage\n\n### Without Mocha Root Hook plugin\n\nThis is recommended for applications that utilize factory design pattern and does not wish to\nintroduce global side effect on the Slonik module itself.\n\nThis is especially useful if you want fine control over when you want to rollback the transactions.\nFor example, if you have a nested `describe` block but only wish to rollback on the inner block,\nyou can choose to rollback in the inner `after` function.\n\n```typescript\nimport { createPool } from \"mocha-slonik\";\n\ndescribe(\"outer block\", function () {\n  let pool;\n\n  before(function () {\n    pool = createPool(/* ... */);\n  });\n\n  describe(\"test group 1\", function () {\n    // Group 1 rolls back after the entire group has completed running.\n    after(async function () {\n      await pool.rollback();\n    });\n\n    it(\"should insert data\", async function () {\n      // ...\n    });\n\n    it(\"should test something else with inserted data\", async function () {\n      // ...\n    });\n  });\n\n  describe(\"test group 2\", function () {\n    // Group 2 rolls back after EACH test completed running.\n    afterEach(async function () {\n      await pool.rollback();\n    });\n\n    it(\"shouldn't be affected by changes to data by tests in group 1\", async function () {\n      // ...\n    })\n  });\n\n});\n```\n\nThis ensures the tests that are grouped by the inner `describe` block see the side effects of\nprevious tests within the group but isolated from other tests outside of the inner `describe`\nblock.\n\n#### Example Express.js project\n\n##### `createServer.ts`\n\n```typescript\nexport const createServer = ({ app, pool }, listenPort) =\u003e {\n  app.use(json);\n  app.post(\"/articles\", async (req, res, next) =\u003e {\n    const { title, body } = req.body;\n    const newArticle = await pool.query(sql`\n      INSERT INTO articles (title, body) VALUES (${title}, ${body}) RETURNING *;\n    `);\n\n    res.status(201).json(newArticle);\n  });\n\n  app.get(\"/articles/:articleId\", async (req, res, next) =\u003e {\n    const article = await pool.one(sql`\n      SELECT * FROM articles WHERE id = ${req.params.articleId} LIMIT 1;\n    `);\n\n    res.json(article);\n  });\n\n  return app.listen(listenPort ?? 8080);\n}\n```\n\n##### `server.ts`\n\n```typescript\nimport express, { json } from \"express\";\nimport { createPool, sql } from \"slonik\";\nimport { createServer } from \"app\";\n\nconst pool = createPool(process.env.DATABASE_URL);\nexport const app = express();\nexport const server = createServer({ app, pool });\n```\n\n##### `server.spec.ts`\n\n```typescript\nimport { expect, use } from \"chai\";\nimport chaiHttp from \"chai-http\";\nimport { createPool } from \"mocha-slonik\";\nimport { sql } from \"slonik\";\nimport { createServer } from \"./app\";\n\nuse(chaiHttp);\n\ndescribe(\"/articles\", function () {\n  let app;\n  let client;\n  let server;\n  let pool;\n\n  before(async function () {\n    app = express();\n    pool = createPool(process.env.DATABASE_URL);\n    server = createServer({ app, createPool });\n    client = chai.request(server).keepOpen();\n  });\n\n  beforeEach(async function () {\n    // Assume 3 items of test fixture data to be inserted into the database.\n    const testFixtures = [\n      [ /* ... */ ],\n      // ...\n    ];\n\n    // Bulk insert test fixture\n    await pool.query(sql`\n      INSERT INTO articles (title, body)\n      SELECT * FROM ${sql.unnest(testFixtures, [\"text\", \"text\"])};\n    `);\n  });\n\n  // Remember to rollback the pool afterEach test.\n  afterEach(async function () {\n    await pool.rollback();\n  });\n\n  after(function (done) {\n    pool.end().then(() =\u003e {\n      client.close();\n      server.close(done);\n    });\n  });\n\n  it(\"should insert a new article\", async function () {\n    const payload = {\n      title: \"Never Gonna Give You Up\",\n      body: \"We're no strangers to love\"\n    };\n\n    // oldCount should be 3 based on test fixture.\n    const oldCount = await pool.oneFirst(sql`SELECT COUNT(*) FROM articles;`);\n\n    const response = await client.post(\"/articles\").send(payload);\n\n    // newCount should now be 4\n    const newCount = await pool.oneFirst(sql`SELECT COUNT(*) FROM articles;`);\n\n    expect(response.body).to.eql(payload);\n    expect(newCount).to.be.above(oldCount);\n  });\n\n  it(\"should get an article by id\", async function () {\n    const response = await client.get(\"/articles/1\").send();\n    const expected = {\n      title: \"Lorem ipsum\",\n      body: \"Lorem ipsum dolor amit\"\n    };\n\n    // Previous test shouldn't affect this test and count return 3\n    const count = await pool.oneFirst(sql`SELECT COUNT(*) FROM articles;`);\n\n    expect(response.body).to.eql(expected);\n  });\n});\n```\n\n### With Mocha Root Hook plugin\n\nThis usage pattern is recommended for applications that cannot utilize factory design pattern.\n\nRequire `mocha-slonik/register` in Mocha CLI:\n\n```sh\nmocha --require mocha-slonik/register tests/**/*.ts\n```\n\nOr update `.mocharc` configuration file:\n\n```json\n{\n  \"require\": [\n    \"mocha-slonik/register\"\n  ]\n}\n```\n\nThat's it! All of your Slonik queries will be wrapped in a transaction in both your application\ncode and your tests that will automatically be rolled back after each test block.\n\n#### Example Express.js project\n\n##### `app.ts`\n\n```typescript\nimport express, { json } from \"express\";\nimport { createPool, sql } from \"slonik\";\n\nconst pool = createPool(process.env.DATABASE_URL);\nexport const app = express();\n\napp.use(json);\napp.post(\"/articles\", async (req, res, next) =\u003e {\n  const { title, body } = req.body;\n  const newArticle = await pool.query(sql`\n    INSERT INTO articles (title, body) VALUES (${title}, ${body}) RETURNING *;\n  `);\n\n  res.status(201).json(newArticle);\n});\n\napp.get(\"/articles/:articleId\", async (req, res, next) =\u003e {\n  const article = await pool.one(sql`\n    SELECT * FROM articles WHERE id = ${req.params.articleId} LIMIT 1;\n  `);\n\n  res.json(article);\n});\n\napp.listen(8080);\n```\n\n##### `app.spec.ts`\n\n```typescript\nimport { expect, use } from \"chai\";\nimport chaiHttp from \"chai-http\";\nimport { createPool, sql } from \"slonik\";\nimport { app } from \"./app\";\n\nuse(chaiHttp);\n\ndescribe(\"/articles\", function () {\n  let client;\n  let server;\n  let pool;\n\n  before(async function () {\n    server = app.listen(9090);\n    client = chai.request(server).keepOpen();\n    pool = createPool(process.env.DATABASE_URL);\n  });\n\n  beforeEach(async function () {\n    // Assume 3 items of test fixture data to be inserted into the database.\n    const testFixtures = [\n      [ /* ... */ ],\n      // ...\n    ];\n\n    // Bulk insert test fixture (rolls back after each test)\n    await pool.query(sql`\n      INSERT INTO articles (title, body)\n      SELECT * FROM ${sql.unnest(testFixtures, [\"text\", \"text\"])};\n    `);\n  });\n\n  after(function (done) {\n    pool.end().then(() =\u003e {\n      client.close();\n      server.close(done);\n    });\n  });\n\n  it(\"should insert a new article\", async function () {\n    const payload = {\n      title: \"Never Gonna Give You Up\",\n      body: \"We're no strangers to love\"\n    };\n\n    // oldCount should be 3 based on test fixture.\n    const oldCount = await pool.oneFirst(sql`SELECT COUNT(*) FROM articles;`);\n\n    const response = await client.post(\"/articles\").send(payload);\n\n    // newCount should now be 4\n    const newCount = await pool.oneFirst(sql`SELECT COUNT(*) FROM articles;`);\n\n    expect(response.body).to.eql(payload);\n    expect(newCount).to.be.above(oldCount);\n  });\n\n  it(\"should get an article by id\", async function () {\n    const response = await client.get(\"/articles/1\").send();\n    const expected = {\n      title: \"Lorem ipsum\",\n      body: \"Lorem ipsum dolor amit\"\n    };\n\n    // Previous test shouldn't affect this test and count return 3\n    const count = await pool.oneFirst(sql`SELECT COUNT(*) FROM articles;`);\n\n    expect(response.body).to.eql(expected);\n  });\n});\n```\n\n## API\n\nThe query methods exposed by mocha-slonik is nearly identical to\n[query methods][slonik-query-methods] exposed by default Slonik pool instance except for few\nadditional methods, properties and [limitations](#limitations).\n\n### `rollback`\n\n`rollback` method is used to rollback the global transaction in mocha-slonik. It should _only_ be\nused in `after` or `afterEach` blocks in your tests. Usage anywhere else is considered\nanti-pattern and may introduce side effects that are hard to debug.\n\nExample:\n\n```typescript\nimport { createPool } from \"mocha-slonik\";\n\ndescribe(\"/articles\", function () {\n  let pool;\n\n  before(async function () {\n    pool = createPool(process.env.DATABASE_URL);\n  });\n\n  // ...\n\n  // Remember to rollback the pool afterEach test.\n  afterEach(async function () {\n    await pool.rollback();\n  });\n\n  // ...\n});\n```\n\n### `currentTransaction`\n\n`currentTransaction` getter is a property that returns the current transaction if one has been\nstarted, `undefined` otherwise.\n\n## Best practices\n\n### Always rollback transaction in `after`/`afterEach` block\n\nUnless you are using the Mocha Root Hook plugin, you should always rollback your transactions in\n`after` or `afterEach` block instead of individual `it` test cases. This prevents unexpected\nsurprises if your test code throws errors or has rejected promises because `after`/`afterEach` is\nguaranteed to run even if your tests fail or contains errors.\n\n**Do:**\n\n```typescript\nimport { createPool } from \"mocha-slonik\";\n\ndescribe(\"Rollback on afterEach\", function () {\n  let pool;\n\n  before(async function () {\n    pool = createPool(process.env.DATABASE_URL);\n  });\n\n  // ...\n\n  // Rollback the transaction after each test.\n  afterEach(async function () {\n    await pool.rollback();\n  });\n\n  // ...\n});\n\ndescribe(\"Rollback on after\", function () {\n  let pool;\n\n  before(async function () {\n    pool = createPool(process.env.DATABASE_URL);\n  });\n\n  // ...\n\n  // Rollback the transaction after the entire group of tests in the describe block.\n  after(async function () {\n    await pool.rollback();\n  });\n\n  // ...\n});\n```\n\n**Don’t:**\n\n```typescript\nimport { expect } from \"chai\";\nimport { createPool } from \"mocha-slonik\";\nimport { sql } from \"slonik\";\n\ndescribe(\"Rollback on individual test cases\", function () {\n  let pool;\n\n  before(async function () {\n    pool = createPool(process.env.DATABASE_URL);\n  });\n\n  it(\"Bad example 1\", async function () {\n    const result = await pool.oneFirst(sql`SELECT 1`);\n    expect(result).to.eq(0);\n\n    // If the assertion above fails, the rollback won't execute.\n    await pool.rollback();\n  });\n\n  it(\"Bad example 2\", async function () {\n    const result = await pool.oneFirst(sql`SELECT 1`);\n\n    // Simulate some thrown error in the test code\n    await Promise.reject(new Error());\n\n    // The rollback won't excute even if the rollback is above the assertion this time.\n    await pool.rollback();\n\n    expect(result).to.eql(1);\n  });\n\n  it(\"Bad example 3\", async function () {\n    try {\n      const result = await pool.oneFirst(sql`SELECT 1`);\n\n      // Simulate some thrown error in the test code\n      await Promise.reject(new Error());\n\n      expect(result).to.eql(1);\n    } catch (error) {\n      throw error;\n    } finally {\n      // Technically works but you have to remember to add the try/finally block for EVERY test case.\n      await pool.rollback();\n    }\n  });\n});\n```\n\n### Use global Root Hook to DRY up boilerplating\n\nIf you’re not using the Root Hook plugin provided by this library but wish to [DRY][dry] up your\nsetup and teardown code across multiple test files, you should define a global Root Hook in your\ntests directory and require it.\n\n**tests/global.ts:**\n\n```typescript\nimport { Server } from \"http\";\nimport express from \"express\";\nimport { RootHookObject } from \"mocha\";\nimport { DatabasePool, createPool } from \"mocha-slonik\";\n\n// See example at: https://github.com/AndrewJo/mocha-slonik#createserverts\nimport { createServer } from \"../src/server\";\n\ndeclare global {\n  namespace Mocha {\n    interface Context {\n      pool: DatabasePool;\n      server: Server;\n    }\n  }\n}\n\nexport const mochaHooks: RootHookObject = {\n  async beforeAll() {\n    // The pool and HTTP server is now available for all of the tests through the context object.\n    this.pool = await createPool(\"postgres://localhost:5432/testdb\");\n    this.server = createServer({ app: express(), pool: this.pool });\n  }\n};\n```\n\n**.mocharc.yaml:**\n\n```yaml\nrequire:\n  - \"ts-node/register\"\n  - \"tests/global\"\n```\n\n## Developing\n\nPlease read [CONTRIBUTING.md](./CONTRIBUTING.md) before making changes to this project.\n\n## Running tests\n\nTo run tests:\n\n```shell\nnpm run build \u0026\u0026 npm test\n```\n\nTo run test with coverage:\n\n```shell\nnpm run test:coverage \u0026\u0026 npx nyc reporter --reporter=lcov\n```\n\nThis will generate an HTML coverage report at: `./coverage/lcov-report/index.html`.\n\n## Limitations\n\nThis library overrides parts of Slonik that is not part of its public API and may break in the\nfuture. Please make sure to check the release notes for compatible Slonik versions before using\nthis library.\n\n### Lack of `copyFromBinary` support\n\n~~Due to the lack of support for transactions in `copyFromBinary` method and\n[the potential for being deprecated in the future versions][slonik-issue-161], calling\n`copyFromBinary` will immediately reject with an error message.~~\n\n`copyFromBinary` support has been [officially removed in Slonik starting from v34.0.0][slonik-copy-from-binary]. This library no longer contains `copyFromBinary` function starting from v8.\n\n[npm]: https://www.npmjs.com/package/mocha-slonik\n[circleci]: https://circleci.com/gh/AndrewJo/mocha-slonik/tree/master\n[codecov]: https://app.codecov.io/gh/AndrewJo/mocha-slonik/\n[mocha]: https://mochajs.org\n[root-hook-plugin]: https://mochajs.org/#root-hook-plugins\n[ts-mock-imports]: https://github.com/EmandM/ts-mock-imports\n[slonik]: https://github.com/gajus/slonik\n[slonik-user-content-mocking-slonik]: https://github.com/gajus/slonik#user-content-mocking-slonik\n[slonik-query-methods]: https://github.com/gajus/slonik#slonik-query-methods\n[slonik-issue-161]: https://github.com/gajus/slonik/issues/161#issuecomment-604770259\n[slonik-copy-from-binary]: https://github.com/gajus/slonik/releases/tag/v34.0.0\n[semver]: https://semver.org/\n[dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandrewjo%2Fmocha-slonik","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandrewjo%2Fmocha-slonik","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandrewjo%2Fmocha-slonik/lists"}