Skip to content

Instantly share code, notes, and snippets.

@statico
Created January 30, 2023 18:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save statico/6519925c5b388a22b17db6f680c1f3ed to your computer and use it in GitHub Desktop.
Save statico/6519925c5b388a22b17db6f680c1f3ed to your computer and use it in GitHub Desktop.
Jest + Node.js + Postgres testing pipeline
import { Knex } from "knex"
export async function seed(knex: Knex): Promise<void> {
// Delete order is specific because of foreign key references
await knex.delete().from("...")
await knex("users").insert(TestUsers)
...
await knex.raw("refresh materialized view ...")
}
import knex, { Knex } from "knex"
import shell from "shelljs"
import * as knexConfig from "../../knexfile"
declare global {
var __KNEX__: Knex
}
// In a GitHub action, process.env.CI will be true, and a Postgres instance will
// already be running. See .github/workflows/test.yml.
//
// Locally, use Docker to start a Postgres + PostGIS server. (It stays running
// after tests end -- maybe we want to change this.)
export default async function () {
let pgContainerName = ""
if (!process.env.CI) {
if (!shell.which("docker"))
throw new Error("Docker is required outside of the CI environment")
if (shell.exec("docker ps", { silent: true }).code !== 0)
throw new Error("Docker is installed but `docker ps` returned an error")
// Try to use and run an existing container since startup time is so slow.
pgContainerName = "test-postgres"
const inspect = shell.exec(`docker inspect ${pgContainerName}`, {
silent: true,
})
if (inspect.code === 0) {
const obj = JSON.parse(inspect)
if (!obj[0]?.State.Running) {
console.log(`Starting Docker container ${pgContainerName}`)
shell.exec(`docker start ${pgContainerName}`)
}
} else {
const cmd = [
"docker run",
`--name ${pgContainerName} -e POSTGRES_PASSWORD=sekrit -p 5499:5432 -d`,
/arm64\s*$/.test(shell.exec("uname -a"))
? "--platform linux/amd64/v8"
: "",
"postgis/postgis:13-3.3",
"-c fsync=off",
].join(" ")
console.log(`Starting Docker container with: ${cmd}`)
shell.exec(cmd)
}
}
const db = knex(knexConfig)
globalThis.__KNEX__ = db
// Try connecting. It takes a few tries.
let tries = 0
while (true) {
try {
await db.select(1)
break
} catch (err) {
console.log(`Connecting to Postgres failed: ${err}`)
if (tries >= 40) {
if (!process.env.CI) {
console.log("------------- DOCKER POSTGRES LOG --------------")
shell.exec(`docker logs ${pgContainerName}`)
console.log("------------------------------------------------")
}
throw new Error("Failed to connect to Postgres")
} else {
tries++
await new Promise((resolve) => setTimeout(resolve, 500))
}
}
}
// Run migrations and seed data
await db.migrate.latest()
await db.seed.run()
}
export default async function () {
globalThis.__KNEX__.destroy()
}
name: Tests
on: [push]
concurrency:
group: build-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
run-tests:
runs-on: ubuntu-latest
services:
# https://docs.github.com/en/actions/using-containerized-services/creating-postgresql-service-containers
postgres:
image: "postgis/postgis:13-3.3-alpine"
env:
POSTGRES_PASSWORD: sekrit
ports:
- 5499:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16"
cache: "yarn"
- run: yarn install --frozen-lockfile
- run: yarn test:ci
env:
DATABASE_URL: "postgresql://postgres:sekrit@postgres:5432"
- name: Tests OK
if: ${{ success() }}
run: |
curl --request POST \
--url https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }} \
--header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \
--header 'content-type: application/json' \
--data '{
"context": "tests",
"state": "success",
"description": "Tests passed",
"target_url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}'
- name: Tests Failed
if: ${{ failure() }}
run: |
curl --request POST \
--url https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }} \
--header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \
--header 'content-type: application/json' \
--data '{
"context": "tests",
"state": "failure",
"description": "Tests failed",
"target_url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}'
jest.mock("lib/sendgrid")
jest.mock("lib/twilio")
const emailFn = jest.mocked(sendEmail)
const smsFn = jest.mocked(sendSMS)
describe("Things", () => {
beforeEach(async () => {
await db.seed.run({ specific: "minimal.ts" })
jest.useRealTimers()
emailFn.mockReset()
smsFn.mockReset()
})
it("tests a thing", async () => {
...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment