Skip to main content

Write document model tests

Tutorial Repository

📦 Reference Code: step-4-implement-tests-for-todos-operations

This step covers writing tests for the reducers you implemented in the previous step.

📖 How to use this tutorial

Compare your tests​

After writing tests:

# Compare your tests with the reference
git diff tutorial/step-4-implement-tests-for-todos-operations -- document-models/todo-list/src/tests/

# View the reference test implementation
git show tutorial/step-4-implement-tests-for-todos-operations:document-models/todo-list/src/tests/todos.test.ts

Visual comparison with GitHub Desktop​

After committing your work, compare visually:

  1. Branch menu → "Compare to Branch..."
  2. Select tutorial/step-4-implement-tests-for-todos-operations
  3. Review differences in the visual interface

To verify the operation reducers work as expected, write tests for them. Generating your document model code created boilerplate tests; now extend them to check the reducer logic.

Understanding the generated test file​

Navigate to /document-models/todo-list/src/tests/todos.test.ts. You will see that we have some basic "sanity check" style tests already. These make sure that your operations at least result in a valid document model state.

import { describe, it, expect } from "vitest";
import { generateMock } from "@powerhousedao/codegen";
import {
reducer,
utils,
isTodoListDocument,
addTodoItem,
AddTodoItemInputSchema,
updateTodoItem,
UpdateTodoItemInputSchema,
deleteTodoItem,
DeleteTodoItemInputSchema,
} from "todo-tutorial/document-models/todo-list";

describe("Todos Operations", () => {
it("should handle addTodoItem operation", () => {
// The `createDocument` utility function from your document model creates
// a new empty document, i.e. one with your default initial state
const document = utils.createDocument();

// The generateMock function takes one of your generated input schemas
// and creates an object populated with random values for each field
const input = generateMock(AddTodoItemInputSchema());

// We call your document model's reducer with the new document we just created
// and the action we want to test, `addTodoItem` in this case.
// The reducer returns a new object, which is the document with the action applied.
// If successful, there will be an operation which corresponds to this action
// in the updated document's operations list.
const updatedDocument = reducer(document, addTodoItem(input));

// When you generate a document model, we give you validation utilities like
// `isTodoListDocument` which confirms the document is of the correct form
// in a way that TypeScript recognizes
expect(isTodoListDocument(updatedDocument)).toBe(true);

// At the start a document will have 0 operations, so after applying this action
// there should now be one operation
expect(updatedDocument.operations.global).toHaveLength(1);

// The operation added to the list should correspond to the correct action type
expect(updatedDocument.operations.global[0].action.type).toBe(
"ADD_TODO_ITEM",
);
});

it("should handle updateTodoItem operation", () => {
// ... boilerplate test
});

it("should handle deleteTodoItem operation", () => {
// ... boilerplate test
});
});

Enhance the tests​

The boilerplate tests check that operations are applied, but they don't verify the actual results. Extend them to assert on the resulting state.

Test 1: Update the addTodoItem test​

The add test is already fairly complete. We just need to add type annotations:

it("should handle addTodoItem operation", () => {
const document = utils.createDocument();
const input: AddTodoItemInput = generateMock(AddTodoItemInputSchema());

const updatedDocument = reducer(document, addTodoItem(input));
expect(isTodoListDocument(updatedDocument)).toBe(true);

expect(updatedDocument.operations.global).toHaveLength(1);
expect(updatedDocument.operations.global[0].action.type).toBe(
"ADD_TODO_ITEM",
);
expect(updatedDocument.operations.global[0].action.input).toStrictEqual(
input,
);
expect(updatedDocument.operations.global[0].index).toEqual(0);
});

Test 2: Replace the updateTodoItem test​

Delete the existing boilerplate and add two separate tests - one for updating text, one for updating the checked state:

it("should handle updateTodoItem operation", () => {
const document = utils.createDocument();
const input = generateMock(UpdateTodoItemInputSchema());
const updatedDocument = reducer(document, updateTodoItem(input));
expect(isTodoListDocument(updatedDocument)).toBe(true);
// ...
});

Add the new test for updating text:

it("should handle updateTodoItem operation to update text", () => {
// We need there to already be a todo item in the document,
// since we want to test updating an existing item
const mockItem = generateMock(TodoItemSchema());

// We also need to generate a mock input for the update operation we are testing
const input: UpdateTodoItemInput = generateMock(UpdateTodoItemInputSchema());

// Since the mocks are generated with random values, we need to set the `id`
// on our mock input to match the `id` of the existing mock item
input.id = mockItem.id;

// We want to easily check if the item's text was updated to our new value,
// so we assign a variable and use that for the mock input's text field
const newText = "new text";
input.text = newText;

// We are only testing updating the text here, so we want the checked field
// on the input to be undefined, i.e. it should not change anything on the existing item
input.checked = undefined;

// We can pass a different initial state to the `createDocument` utility,
// so in this case we pass in an `items` array with our existing item already in it
const document = utils.createDocument({
global: {
items: [mockItem],
},
});

// Create an updated document by applying the reducer with the action and input
const updatedDocument = reducer(document, updateTodoItem(input));

// Use our validator to check that the document conforms to the document model schema
expect(isTodoListDocument(updatedDocument)).toBe(true);

// There should now be one operation in the operations list
expect(updatedDocument.operations.global).toHaveLength(1);
expect(updatedDocument.operations.global[0].action.type).toBe(
"UPDATE_TODO_ITEM",
);

// Find the updated item in the items list by its `id`
const updatedItem = updatedDocument.state.global.items.find(
(item) => item.id === input.id,
);

// The item's text should now be updated to be our new text
expect(updatedItem?.text).toBe(newText);

// The item's `checked` field should be unchanged
expect(updatedItem?.checked).toBe(mockItem.checked);
});

Add the new test for updating checked state:

it("should handle updateTodoItem operation to update checked", () => {
// Generate a mock existing item
const mockItem = generateMock(TodoItemSchema());

// Generate a mock input
const input: UpdateTodoItemInput = generateMock(UpdateTodoItemInputSchema());

// Set the mock input's `id` to the mock item's `id`
input.id = mockItem.id;

// We want the new `checked` field value to be the opposite of the
// randomly generated value from the mock
const newChecked = !mockItem.checked;
input.checked = newChecked;

// Leave the `text` field unchanged
input.text = undefined;

// Create a document with the existing item in it
const document = utils.createDocument({
global: {
items: [mockItem],
},
});

// Apply the reducer with the action and the mock input
const updatedDocument = reducer(document, updateTodoItem(input));

// Validate your document
expect(isTodoListDocument(updatedDocument)).toBe(true);

// Get the updated item by its `id`
const updatedItem = updatedDocument.state.global.items.find(
(item) => item.id === input.id,
);

// The item's `text` field should remain unchanged
expect(updatedItem?.text).toBe(mockItem.text);

// The item's `checked` field should be updated to our new checked value
expect(updatedItem?.checked).toBe(newChecked);
});

Test 3: Update the deleteTodoItem test​

The boilerplate delete test passes even without an existing item to delete. Let's fix that:

it("should handle deleteTodoItem operation", () => {
const document = utils.createDocument();
const input = generateMock(DeleteTodoItemInputSchema());
// Create an existing item to delete
const mockItem = generateMock(TodoItemSchema());
const document = utils.createDocument({
global: {
items: [mockItem],
},
});
const input: DeleteTodoItemInput = generateMock(DeleteTodoItemInputSchema());
input.id = mockItem.id;

const updatedDocument = reducer(document, deleteTodoItem(input));
expect(isTodoListDocument(updatedDocument)).toBe(true);

expect(updatedDocument.operations.global).toHaveLength(1);
expect(updatedDocument.operations.global[0].action.type).toBe(
"DELETE_TODO_ITEM",
);
expect(updatedDocument.operations.global[0].action.input).toStrictEqual(
input,
);
expect(updatedDocument.operations.global[0].index).toEqual(0);

// Verify the item was actually removed
const updatedItems = updatedDocument.state.global.items;
expect(updatedItems).toHaveLength(0);
});

Complete test file​

Here's the complete test file with all updates. Don't forget to add the missing imports:

Complete todos.test.ts
import { describe, it, expect } from "vitest";
import { generateMock } from "@powerhousedao/codegen";
import type {
AddTodoItemInput,
DeleteTodoItemInput,
UpdateTodoItemInput,
} from "todo-tutorial/document-models/todo-list";
import {
reducer,
utils,
isTodoListDocument,
addTodoItem,
AddTodoItemInputSchema,
updateTodoItem,
UpdateTodoItemInputSchema,
deleteTodoItem,
DeleteTodoItemInputSchema,
TodoItemSchema,
} from "todo-tutorial/document-models/todo-list";

describe("Todos Operations", () => {
it("should handle addTodoItem operation", () => {
const document = utils.createDocument();
const input: AddTodoItemInput = generateMock(AddTodoItemInputSchema());

const updatedDocument = reducer(document, addTodoItem(input));
expect(isTodoListDocument(updatedDocument)).toBe(true);

expect(updatedDocument.operations.global).toHaveLength(1);
expect(updatedDocument.operations.global[0].action.type).toBe(
"ADD_TODO_ITEM",
);
expect(updatedDocument.operations.global[0].action.input).toStrictEqual(
input,
);
expect(updatedDocument.operations.global[0].index).toEqual(0);
});

it("should handle updateTodoItem operation to update text", () => {
const mockItem = generateMock(TodoItemSchema());
const input: UpdateTodoItemInput = generateMock(
UpdateTodoItemInputSchema(),
);
input.id = mockItem.id;
const newText = "new text";
input.text = newText;
input.checked = undefined;

const document = utils.createDocument({
global: {
items: [mockItem],
},
});

const updatedDocument = reducer(document, updateTodoItem(input));
expect(isTodoListDocument(updatedDocument)).toBe(true);

expect(updatedDocument.operations.global).toHaveLength(1);
expect(updatedDocument.operations.global[0].action.type).toBe(
"UPDATE_TODO_ITEM",
);

const updatedItem = updatedDocument.state.global.items.find(
(item) => item.id === input.id,
);
expect(updatedItem?.text).toBe(newText);
expect(updatedItem?.checked).toBe(mockItem.checked);
});

it("should handle updateTodoItem operation to update checked", () => {
const mockItem = generateMock(TodoItemSchema());
const input: UpdateTodoItemInput = generateMock(
UpdateTodoItemInputSchema(),
);
input.id = mockItem.id;
const newChecked = !mockItem.checked;
input.checked = newChecked;
input.text = undefined;

const document = utils.createDocument({
global: {
items: [mockItem],
},
});

const updatedDocument = reducer(document, updateTodoItem(input));
expect(isTodoListDocument(updatedDocument)).toBe(true);

const updatedItem = updatedDocument.state.global.items.find(
(item) => item.id === input.id,
);
expect(updatedItem?.text).toBe(mockItem.text);
expect(updatedItem?.checked).toBe(newChecked);
});

it("should handle deleteTodoItem operation", () => {
const mockItem = generateMock(TodoItemSchema());
const document = utils.createDocument({
global: {
items: [mockItem],
},
});

const input: DeleteTodoItemInput = generateMock(
DeleteTodoItemInputSchema(),
);
input.id = mockItem.id;

const updatedDocument = reducer(document, deleteTodoItem(input));
expect(isTodoListDocument(updatedDocument)).toBe(true);

expect(updatedDocument.operations.global).toHaveLength(1);
expect(updatedDocument.operations.global[0].action.type).toBe(
"DELETE_TODO_ITEM",
);

const updatedItems = updatedDocument.state.global.items;
expect(updatedItems).toHaveLength(0);
});
});
Check your work

To make sure everything works as expected:

# Check types compile correctly
pnpm tsc

# Check linting passes
pnpm lint

# Run tests
pnpm test

# Compare with reference implementation
git diff tutorial/step-4-implement-tests-for-todos-operations -- document-models/todo-list/src/tests/

Expected test output:

✓ document-models/todo-list/src/tests/document-model.test.ts (3 tests) 1ms
✓ document-models/todo-list/src/tests/todos.test.ts (4 tests) 8ms

Test Files 2 passed (2)
Tests 7 passed (7)

Up next: Building the editor​

In the next chapter, you'll learn how to implement a user interface (editor) for your document model so you can interact with it visually.