Test Guidelines for Emmie
Introduction
This document provides guidelines for writing tests in the frontend/tests directory. It outlines the principles and patterns used in existing tests to ensure consistency and reliability across the test suite.
Testing Framework
- Vitest: Use Vitest as the testing framework. It provides a robust environment for testing Vue.js applications.
- Vue Test Utils: Utilize Vue Test Utils for testing Vue components. It offers utilities for mounting and interacting with Vue components in tests.
Test Structure
- Describe Blocks: Group related tests using
describeblocks. Usedescribe.sequentialto ensure tests run in sequence when necessary. - It Blocks: Define individual test cases within
itblocks. Each test should focus on a single aspect of the functionality being tested.
Mocking
- Mock Components and Services: Use
vi.mockto mock components and services. This isolates the unit of work being tested and prevents dependencies on external components or services. - Mock Implementation: Provide mock implementations where necessary to simulate specific behaviors or responses. We use the
__mocks__directory for manual mock implementations. - Mock every import: We mock every import because we want to test the component itself, not imported components and services.
Manual Mocking
Manual mocks are defined by writing a module in a __mocks__/ subdirectory immediately adjacent to the module. For example, to mock a module called session in the auth directory, create a file called session.ts and put it in the auth/__mocks__ directory.
Here an example of a manual mock for the auth/session module:
import {vi} from 'vitest';
import {computed} from 'vue';
export const INACTIVITY_LOGOUT_KEY = 'INACTIVITY_LOGOUT_KEY';
// Here we mock the reset session function by using the mock function from vitest.
// If we want to test against a specific return value for the reset session function, we can use vi.mocked(resetSession).mockResolvedValue(value);
export const resetSession = vi.fn();
// Here we mock the computed property by creating a computed property that always returns 0.
// This way we have a standard value to test against.
// And if we want to test the computed property, we can change the value of the computed property in the test itself.
// To do this we can use vi.spyOn(secondsRemaining, 'value', 'get').mockReturnValue(0);
export const secondsRemaining = computed(() => 0);Asynchronous Testing
- Async/Await: Use asynchronous functions and
awaitto handle asynchronous operations. Ensure promises are resolved before making assertions. - Flush Promises: Use
flushPromisesto wait for all pending promises to resolve, ensuring the test environment is stable before assertions.
Assertions
- Expect: Use
expectto make assertions about the behavior of the code. Check that functions are called with the correct parameters and that promises resolve to expected values.
Component Mounting
- shallowMount vs. mount: Prefer using
shallowMountovermountwhen testing components.shallowMountcreates a component instance without rendering its child components, which helps isolate the component under test and improves test performance by reducing the complexity of the rendered output.
Example Test Case
The AAA (Arrange-Act-Assert) pattern is a common structure for writing test cases. It helps organize tests in a clear and consistent manner, making them easier to read and maintain.
- Arrange: Set up the test environment, including any necessary data, mocks, or configurations. This step prepares everything needed for the test.
- Act: Execute the function or component being tested. This is where the action takes place.
- Assert: Verify that the outcomes are as expected. This step checks that the code behaves correctly.
describe('Component Name', () => {
it('should perform a specific action', async ({expect}) => {
// Arrange
// Setup the test environment and mock dependencies
// Act
// Execute the function or component being tested
// Assert
// Verify the expected outcomes
});
});Best Practices
- Isolation: Ensure each test is independent and does not rely on the state of other tests.
- Clarity: Write clear and descriptive test names that convey the purpose of the test.
- Maintainability: Keep tests concise and focused. Avoid testing multiple behaviors in a single test case.
Compare with constants instead of calculations.
By performing calculations in the test and comparing the result of a function with the result of the calculations, you run the risk of repeating the same error from the function.
// bad
it('should reverse the string', () => {
const result = stringReverser('testString');
expect(result).toBe('testString'.split('').reverse().join(''));
});
// good
it('should reverse the string', () => {
const result = stringReverser('testString');
expect(result).toBe('gnirtStset');
});Use realistic data
Try to use realistic data as much as possible. For example, instead of using a person with the name 'a' as a test object, use a real name.
Using Vitest Workspaces
Vitest workspaces allow you to organize and manage multiple test projects within a single repository.
- Run tests for a specific workspace: Use the
--projectflag to run tests for a specific project.
npx vitest --project shared- Run tests for all projects: Simply run
vitestwithout any flags to execute tests for all projects.
npx vitestBenefits of Workspaces
- Modularity: Separate configurations and dependencies for different projects.
- Scalability: Easily add new projects to the workspace.
- Isolation: Run tests for individual projects without affecting others.
Specifying Elements
To select an (HTML) element from a file (being tested), a "data-test" tag can be added to the element. Example:
<div data-test="example"></div>const test = wrapper.getByDataTest('example');