Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update testing example to use node:test #8829

Merged
merged 4 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 17 additions & 13 deletions examples/testing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@ You can also access a GraphQL Playground at [localhost:3000/api/graphql](http://

## Features

Keystone provides a testing library in [`@keystone-6/core/testing`](https://keystonejs.com/guides/testing) which helps you write tests using [Jest](https://jestjs.io/).
This example project uses this library to add tests to the [`withAuth()`](../with-auth) example project. The tests can be found in [example.test.ts](./example.test.ts)
This example project uses this library to add tests to the [`withAuth()`](../with-auth) example project. The tests can be found in [example-test.ts](./example-test.ts). This example uses the built in `getContext` API to run tests against a Keystone Context

## Recommendations when Testing Keystone

When testing Keystone it is good to focus on Access Control, Hooks, Virtual Fields, Custom GraphQL extensions. Ideally these can be broken out into unit tests that test the individual function and its output - most of the time without even needing to use the Keystone Context. For higher level, end-to-end/integration tests, where you want to test a particular use case or user flow, you can then use `getContext`. It is highly recommended that you do not switch database providers for your tests as each provider can have slightly different functionality.

### Running tests

The project's `package.json` includes a script:

```
"test": "jest"
"test": "node --loader tsx example-test.ts"
```

We can run the tests by running the command
Expand All @@ -38,16 +41,17 @@ pnpm test
which should give output ending with:

```
PASS ./example.test.ts
✓ Create a Person using the Query API (183 ms)
✓ Check that trying to create user with no name (required field) fails (116 ms)
✓ Check access control by running updateTask as a specific user via context.withSession() (198 ms)

Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 2.316 s, estimated 5 s
Ran all test suites.
✔ Create a User using the Query API (139.404167ms)
✔ Check that trying to create user with no name (required field) fails (96.580875ms)
✔ Check access control by running updateTask as a specific user via context.withSession() (193.86275ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 0.072292
```

## Try it out in CodeSandbox 🧪
Expand Down
26 changes: 0 additions & 26 deletions examples/testing/babel.config.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import path from 'path';
import path from 'node:path';
import { test, beforeEach } from 'node:test';
import assert from 'node:assert/strict';

import { resetDatabase } from '@keystone-6/core/testing';
import { getContext } from '@keystone-6/core/context';
import baseConfig from './keystone';
Expand All @@ -14,52 +17,45 @@ beforeEach(async () => {
await resetDatabase(dbUrl, prismaSchemaPath);
});

test('Create a User using the Query API', async () => {
// We can use the context argument provided by the test runner to access
// the full context API.
test('Create a User using context.query', async () => {
const person = await context.query.User.createOne({
data: { name: 'Alice', password: 'dont-use-me' },
query: 'id name password { isSet }',
});
expect(person.name).toEqual('Alice');
expect(person.password.isSet).toEqual(true);
assert.equal(person.name, 'Alice');
assert.equal(person.password.isSet, true);
});

test('Check that trying to create user with no name (required field) fails', async () => {
// The context.graphql.raw API is useful when we expect to recieve an
// error from an operation.
// the context.graphql.raw API can be useful when you expect errors
const { data, errors } = (await context.graphql.raw({
query: `mutation {
createUser(data: { password: "dont-use-me" }) {
id name password { isSet }
}
}`,
})) as any;
expect(data!.createUser).toBe(null);
expect(errors).toHaveLength(1);
expect(errors![0].path).toEqual(['createUser']);
expect(errors![0].message).toEqual(
assert.equal(data!.createUser, null);
assert.equal(errors![0].path[0], 'createUser');
assert.equal(
errors![0].message,
'You provided invalid data for this operation.\n - User.name: Name must not be empty'
);
});

test('Check access control by running updateTask as a specific user via context.withSession()', async () => {
// We can modify the value of context.session via context.withSession() to masquerade
// as different logged in users. This allows us to test that our access control rules
// are behaving as expected.

// Create some users
// seed a few users to test with
const [alice, bob] = await context.query.User.createMany({
data: [
{ name: 'Alice', password: 'dont-use-me' },
{ name: 'Bob', password: 'dont-use-me' },
],
query: 'id name',
});
expect(alice.name).toEqual('Alice');
expect(bob.name).toEqual('Bob');
assert.equal(alice.name, 'Alice');
assert.equal(bob.name, 'Bob');

// Create a task assigned to Alice
// assign a task to Alice
const task = await context.query.Task.createOne({
data: {
label: 'Experiment with Keystone',
Expand All @@ -69,12 +65,12 @@ test('Check access control by running updateTask as a specific user via context.
},
query: 'id label priority isComplete assignedTo { name }',
});
expect(task.label).toEqual('Experiment with Keystone');
expect(task.priority).toEqual('high');
expect(task.isComplete).toEqual(false);
expect(task.assignedTo.name).toEqual('Alice');
assert.equal(task.label, 'Experiment with Keystone');
assert.equal(task.priority, 'high');
assert.equal(task.isComplete, false);
assert.equal(task.assignedTo.name, 'Alice');

// Check that we can't update the task (not logged in)
// test that we can't update the task (without a session)
{
const { data, errors } = (await context.graphql.raw({
query: `mutation update($id: ID!) {
Expand All @@ -84,16 +80,17 @@ test('Check access control by running updateTask as a specific user via context.
}`,
variables: { id: task.id },
})) as any;
expect(data!.updateTask).toBe(null);
expect(errors).toHaveLength(1);
expect(errors![0].path).toEqual(['updateTask']);
expect(errors![0].message).toEqual(
`Access denied: You cannot update that Task - it may not exist`
assert.equal(data!.updateTask, null);
assert.equal(errors.length, 1);
assert.equal(errors![0].path[0], 'updateTask');
assert.equal(
errors![0].message,
'Access denied: You cannot update that Task - it may not exist'
);
}

// test that we can update the task (with a session)
{
// Check that we can update the task when logged in as Alice
const { data, errors } = (await context
.withSession({ listKey: 'User', itemId: alice.id, data: {} })
.graphql.raw({
Expand All @@ -104,11 +101,11 @@ test('Check access control by running updateTask as a specific user via context.
}`,
variables: { id: task.id },
})) as any;
expect(data!.updateTask.id).toEqual(task.id);
expect(errors).toBe(undefined);
assert.equal(data!.updateTask.id, task.id);
assert.equal(errors, undefined);
}

// Check that we can't update the task when logged in as Bob
// test that we can't update the task (with an invalid session (Bob))
{
const { data, errors } = (await context
.withSession({ listKey: 'User', itemId: bob.id, data: {} })
Expand All @@ -120,10 +117,11 @@ test('Check access control by running updateTask as a specific user via context.
}`,
variables: { id: task.id },
})) as any;
expect(data!.updateTask).toBe(null);
expect(errors).toHaveLength(1);
expect(errors![0].path).toEqual(['updateTask']);
expect(errors![0].message).toEqual(
assert.equal(data!.updateTask, null);
assert.equal(errors!.length, 1);
assert.equal(errors![0].path[0], 'updateTask');
assert.equal(
errors![0].message,
`Access denied: You cannot update that Task - it may not exist`
);
}
Expand Down
8 changes: 2 additions & 6 deletions examples/testing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,16 @@
"start": "keystone start",
"build": "keystone build",
"postinstall": "keystone postinstall",
"test": "jest --runInBand --testTimeout=60000"
"test": "node --loader tsx example-test.ts"
},
"dependencies": {
"@babel/core": "^7.21.0",
"@babel/preset-env": "^7.20.0",
"@babel/preset-typescript": "^7.21.0",
"@keystone-6/auth": "^7.0.0",
"@keystone-6/core": "^5.0.0",
"@prisma/client": "^4.16.2"
},
"devDependencies": {
"@types/node": "^18.11.14",
"babel-jest": "^29.0.1",
"jest": "^29.0.0",
"tsx": "^3.9.0",
"prisma": "^4.16.2",
"typescript": "~5.0.0"
}
Expand Down
Loading