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:
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:
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:
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:
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 Code | Description |
---|---|
ERR_INVALID_INPUT | The input data to the action was incorrect. The message field in the response of the action will provide the exact reason. |
ERR_PERMISSION_DENIED | The given user was not authorized to execute the action. Examine any @permission attributes that apply to the action. |
ERR_RECORD_NOT_FOUND | Could not find any existing record in the database for the unique constraints provided to the action to find the record (usually id ) |
ERR_INTERNAL | Internal 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:
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:
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:
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:
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:
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:
Flag | Description |
---|---|
--pattern , -p | Runs only test cases matching the pattern. Accepts a simple string or regular expression. Uses Vitest's --testNamePattern internally. |
--dir , -d | The 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.