Testing

Testing is built into the core of Keel. The testing package is generated from your project schema and gives full type safety for writing end-to-end tests against built-in actions, custom functions, and jobs defined in your schema.

Tests run locally on your machine against a temporary Docker-based database, so there are no nasty surprises when you come to deploy your schema!

Getting started

Take the following .keel schema:

schema.keel
model Product {
  fields {
    title Text
  }
 
  actions {
    get getProduct(id) {
      @permission(expression: true)
    }
 
    list listProducts() {
      @permission(expression: true)
    }
 
    create createProduct() with(title) {
      @permission(expression: ctx.isAuthenticated)
    }
  }
}

First thing you will need to do is to run Keel's code generation command in order to dynamically generate TypeScript types for your schema so you can interact with the Keel testing framework in a type-safe manner:

$ keel generate

Once the supporting TypeScript code for your schema has been generated, you can create a file at the root called main.test.ts with the following code:

main.test.ts
import { test, expect } from 'vitest';
import { actions, models } from '@teamkeel/testing';
 
test('a passing test', async () => {
  // create the product in the database
  const product = await models.product.create({
    title: 'cool product',
  });
 
  // call the action
  const fetchedRecord = await actions.getProduct({ id: product.id });
 
  // assert that calling the action with the id returns the expected product
  expect(fetchedRecord).toEqual(product);
});

To run the test, run the following command in the same folder where your .keel schema file is located:

keel test

keel test will run any test files within the current directory matching the pattern **/*.test.ts.

💡

Should your application utilize secrets and you operate a separate testing environment, ensure you replicate those secrets in the test environment for proper functionality.

Diving deeper

We have written a test using the classical Arrange-Act-Assert (AAA) pattern. You may notice that the code imports from the @teamkeel/testing NPM package.

@teamkeel/testing is a dynamically generated NPM package (you won't find it on npmjs.org!) that was generated when you ran keel generate. It contains all of the TypeScript definitions of any models and their actions as you defined them in the schema.

In the test above we are most interested in the following named exports from that package:

actions

The actions export contains all of the actions defined in your schema, whether they are built-in actions or functions!

In the schema above, you would find that the actions export has two actions that are callable:

import { actions } from '@teamkeel/testing';
 
const product = await actions.createProduct({ title: 'a cool product' });
const fetchedProduct = await actions.getProduct({ id: product.id });

models

The models export allows you to query the application's database directly using our completely type-safe query builder. models will contain every model defined in your schema file, and each entry in models will expose a database client:

import { models } from '@teamkeel/testing';
 
const product = await models.product.create({ title: 'a cool product' });

You can find out more about the built-in model API by visiting the Model API documentation.

jobs

Although not explicitly mentioned in the sample schema above, it is also possible to test the execution of jobs. Visit the Jobs Testing documentation to find out more!

events

Events are also supporting in tests. Please see this section for how to test events and subscribers.

Assertions

As we use Vitest under the hood, you can leverage Vitest's impressive expectation and assertion APIs to write your tests. For more documentation on the Vitest assertion API, visit their documentation (opens in a new tab).

Keel provides a set of custom Vitest assertions that will help you write tests for your actions; they include:

toHaveError

Asserts that the result of an action returned a certain type of error:

main.test.ts
import { actions } from '@teamkeel/testing';
 
test("test not found error", async () => {
  await expect(
    actions.getProduct({ id: "non-existent" })
  ).toHaveError({
    code: "ERR_RECORD_NOT_FOUND",
    message: "record not found",
  });
});

It is also possible to negate the toHaveError matcher like so:

main.test.ts
import { actions } from '@teamkeel/testing';
 
test("it should exist", async () => {
  await expect(
    actions.getProduct({ id: "it-exists" })
  ).not.toHaveError({
    code: "ERR_RECORD_NOT_FOUND",
    message: "record not found",
  });
});

A full list of all of the potential error codes returned by actions can be found in the table below:

Error CodeDescription
ERR_INVALID_INPUTThe input data to the action was incorrect. The message field in the response of the action will provide the exact reason.
ERR_PERMISSION_DENIEDThe given user was not authorized to execute the action. Examine any @permission attributes that apply to the action.
ERR_RECORD_NOT_FOUNDCould not find any existing record in the database for the unique constraints provided to the action to find the record (usually id)
ERR_INTERNALInternal Keel runtime error. This should not happen so we are very sorry! If it does, please contact us.

toHaveAuthorizationError

Asserts that the result of an action did not return an authorization error:

main.test.ts
import { actions } from '@teamkeel/testing';
 
test("test authorization error", async () => {
  await expect(
    actions.getSecretProduct({ id: "secret" })
  ).toHaveAuthorizationError();
});

toHaveAuthorizationError can also be negated.

Helpers

Test Setup

resetDatabase

@teamkeel/testing exposes the resetDatabase method that can be used to clear the database between test cases:

main.test.ts
import { test, expect, beforeEach } from 'vitest';
import { models, actions, resetDatabase } from '@teamkeel/testing';
 
beforeEach(resetDatabase);
 
test("first test", async () => {
  const product = await models.product.create({
    title: "hello"
  });
 
  const results = await actions.listProducts();
 
  expect(results.length).toEqual(1);
});
 
test("second test", async () => {
  const product = await models.product.create({
    title: "world"
  });
 
  const results = await actions.listProducts();
 
  expect(results.length).toEqual(1);
});

Authentication

The actions export provides two special modifier operations that allow you to impersonate a particular user when executing an action:

withIdentity

Given the following .keel schema that enforces that the createProduct action can only be executed by authenticated users:

schema.keel
model Product {
  fields {
    title Text
  }
 
  actions {
    create createProduct() with(title) {
      @permission(expression: ctx.isAuthenticated)
    }
  }
}

In your test, you can create an identity record using our built-in Model API and then assume that user's identity whilst executing the action:

main.test.ts
import { test, expect } from 'vitest';
import { actions, models } from '@teamkeel/testing';
 
test('a permission test', async () => {
  const identity = await models.identity.create({
    email: 'user@example.com',
  });
 
  // authenticated call will succeed
  await expect(
    actions.withIdentity(identity).createProduct({ title: "cool product" })
  ).not.toHaveAuthorizationError();
 
  // unauthenticated will not succeed
  await expect(
    actions.createProduct({ title: "cool product" })
  ).toHaveAuthorizationError();
});

withAuthToken

Alternatively, if you have a JWT (opens in a new tab), you can execute the action using that instead:

main.test.ts
import { test, expect } from 'vitest';
import { actions, models } from '@teamkeel/testing';
 
test('a token test', async () => {
  const token = "XXX";
 
  await expect(
    actions.withAuthToken(token).createProduct({ title: "cool product" })
  ).not.toHaveAuthorizationError();
});

CLI

The test command can be run like so:

$ keel test

Flags

keel test has the following CLI arguments:

FlagDescription
--pattern, -pRuns only test cases matching the pattern. Accepts a simple string or regular expression. Uses Vitest's --testNamePattern internally.
--dir, -dThe path to the directory containing your schema and tests. Defaults to the current directory.

FAQs

Does this communicate with my Keel backend in the cloud?

No, the test runner runs tests against a local instance of Postgres that is spun up for the tests and swiftly discarded.

Help! It's not working!

Please ensure you have followed the steps outlined in the Local Environment setup guide.

Can I run my tests as part of deployment?

We will be supporting this in the very near future.