Documentation

Documentation versions (currently viewing)

Testing

How to test views in Hilla applications.

This guide explains the basics of testing views in Hilla. It uses the Vitest library for testing and setting up a configuration that runs tests in a browser to produce accurate results. It also uses the React Testing Library to render and interact with React components during tests.

Setting Up a Test Environment

Before starting to test, make sure your Hilla project is initialized, and the default Vite configuration exists. You can do this by starting the application once by running the default Maven goal:

mvn

Now, install the necessary testing libraries like so:

npm install -D vitest @vitest/browser webdriverio pretty-format @testing-library/react @testing-library/user-event

Next, create a test configuration file named, vitest.config.ts in the project root with the following content:

import type { UserConfigFn } from 'vite';
import { overrideVaadinConfig } from './vite.generated';

const customConfig: UserConfigFn = (env) => ({
  plugins: [],
  test: {
    include: ['./tests/**/*.{test,spec}.ts?(x)'],
    globals: true,
    browser: {
      enabled: true,
      name: 'chrome',
    }
  },
});

export default overrideVaadinConfig(customConfig);

Finally, add an npm script to package.json to run the tests and open the Vitest UI:

"scripts": {
  "test": "vitest"
}

You can now run the tests by executing the following from the command-line:

npm test

At this point, this command fails because there aren’t any tests, yet. That’s covered in the next section.

Writing Tests

As an example, to see how to test views in Hilla, use a simple Todo view. To start, create a basic view in frontend/views/TodoView.tsx like this:

export function TodoView() {
  return (
    <div>
      <h2>My Todos</h2>
    </div>
  );
}

Now, create a basic test for this view in a file named, frontend/tests/TodoView.test.tsx and copy the following into it:

import { describe, it, expect } from "vitest";
import { TodoView } from "Frontend/views/TodoView";
import { render, screen } from "@testing-library/react";

describe("TodoView", () => {
  it("should render", () => {
    render(<TodoView />);
    expect(screen.getByText("My Todos")).to.exist;
  });
});

This test file contains a single test that renders the view and verifies that the My Todos text is present in the DOM.

Start the test by executing the following again from the command-line:

npm test

This opens a browser window and displays the Vitest UI, which can be used for running and monitoring tests. It should run the newly added test and report that it passed.

Testing User Interaction

Now you can explore how to add a feature to the view and test it with user interactions. To do this, add an input field and a button to enter new tasks to do, along with a list to render them. Here’s the updated TodoView component:

import { Button } from "@vaadin/react-components/Button.js";
import {
  TextField,
  TextFieldValueChangedEvent,
} from "@vaadin/react-components/TextField.js";
import { useSignal } from "@vaadin/hilla-react-signals";

export function TodoView() {
  const newTodo = useSignal("");
  const todos = useSignal<Array<string>>([]);

  return (
    <div className="p-m">
      <h2>My Todos</h2>
      <div className="flex gap-s items-baseline">
        <TextField
          label="New todo"
          value={newTodo.value}
          onValueChanged={(e: TextFieldValueChangedEvent) =>
            newTodo.value = e.detail.value
          }
        />
        <Button
          onClick={() => {
            todos.value = [...todos, newTodo];
            newTodo = "";
          }}
        >
          Add todo
        </Button>
      </div>
      <ul>
        {todos.value.map((todo, idx) => (
          <li key={idx}>{todo}</li>
        ))}
      </ul>
    </div>
  );
}

Next, add a test that interacts with the view. For testing interactions, import userEvent from @testing-library/user-event like so:

import { userEvent } from "@testing-library/user-event";

describe("TodoView", () => {
  ...

  it("should add a todo", async () => {
    render(<TodoView />);

    // Change the value of the text field
    const textField = screen.getByLabelText("New todo");
    await userEvent.click(textField);
    await userEvent.type(textField, "Read testing guide");

    // Click the add todo button
    const button = screen.getByText("Add todo");
    await userEvent.click(button);

    // Rerender and check that the todo is shown
    expect(screen.getByText("Read testing guide")).to.exist;
  });
});

This new test first finds the text field and changes its value using userEvent.type. Next, it finds the button and clicks it using userEvent.click. Finally, it verifies that the new todo is rendered in the list.

Save the test so Vitest can run it in the browser, verifying that the interaction works as expected.

Testing Backend Calls

Views often need to interact with backend services, which should be tested, as well. In this section, you’ll create a backend service to store todos and then verify that it’s called correctly from the view. First, create a dummy service called, TodoService.java next to Application.java:

package com.example.application;

import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.hilla.BrowserCallable;

@BrowserCallable
@AnonymousAllowed
public class TodoService {
    public void addTodo(String todo) {
        System.out.println("addTodo: " + todo);
    }
}

The service has a single method, addTodo that only prints the given todo to the console.

From the CLI, run the following command to generate the TypeScript client for the service:

mvn clean compile hilla:generate

Now, update the click handler of the button in TodoView to call the service:

/* Add new import for generated service client */
import { TodoService } from "Frontend/generated/endpoints";

...

<Button
  onClick={() => {
    TodoService.addTodo(newTodo);
    setTodos([...todos, newTodo]);
    setNewTodo("");
  }}
>
  Add todo
</Button>

Next, add a test to verify that the service is called correctly. Set up a test environment that stubs the service to prevent actual backend calls and allows you to monitor calls:

/* Update imports from vitest */
import { afterEach, beforeEach, describe, expect, it, type SpyInstance, vi } from "vitest";

/* Add new import for generated service client */
import { TodoService } from "Frontend/generated/endpoints";

describe("TodoView", () => {
  /* Add test setup and teardown */
  let addTodoSpy: SpyInstance;

  beforeEach(() => {
    addTodoSpy = vi.spyOn(TodoService, "addTodo");
    addTodoSpy.mockReturnValue(Promise.resolve());
  });

  afterEach(() => {
    addTodoSpy.mockRestore();
  });

  ...

  it("should call service when adding todo", async () => {
    render(<TodoView />);

    const textField = screen.getByLabelText("New todo");
    await userEvent.click(textField);
    await userEvent.type(textField, "Read testing guide");

    const button = screen.getByText("Add todo");
    await userEvent.click(button);

    expect(addTodoSpy).toHaveBeenCalledWith("Read testing guide");
  });
});

In the beforeEach hook, this test first creates a spy for the addTodo method of the service. Then, this stubs the spy to return a resolved promise to prevent the actual backend call. Finally, it restores the original method in the afterEach hook to prevent the stub from leaking to other tests.

The actual test does the same user interaction as before, but this time it’s verifying that the service is called with the correct argument.

Save the test so Vitest can run it in the browser, verifying that the service is called correctly.

Further Information

For more information about Vitest and React Testing Library, refer to the official documentation: