.cursor/docs/guides/testing.md
This guide outlines best practices for writing tests for the Appsmith codebase.
Appsmith uses Jest for frontend unit tests. Unit tests should be written for individual components, utility functions, and Redux slices.
Create test files with the .test.ts or .test.tsx extension in the same directory as the source file:
src/
components/
Button/
Button.tsx
Button.test.tsx
utils/
helpers.ts
helpers.test.ts
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import Button from "./Button";
describe("Button component", () => {
it("renders correctly with default props", () => {
render(<Button>Click me</Button>);
expect(screen.getByText("Click me")).toBeInTheDocument();
});
it("calls onClick handler when clicked", () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText("Click me"));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
import { configureStore } from "@reduxjs/toolkit";
import reducer, {
setUserInfo,
fetchUserInfo
} from "./userSlice";
describe("User reducer", () => {
it("should handle initial state", () => {
expect(reducer(undefined, { type: "unknown" })).toEqual({
userInfo: null,
isLoading: false,
error: null
});
});
it("should handle setUserInfo", () => {
const userInfo = { name: "Test User", email: "[email protected]" };
expect(
reducer(
{ userInfo: null, isLoading: false, error: null },
setUserInfo(userInfo)
)
).toEqual({
userInfo,
isLoading: false,
error: null
});
});
});
Safety when accessing deeply nested properties in Redux state is critical for application reliability. Here are patterns for testing these safety mechanisms:
import { configureStore } from '@reduxjs/toolkit';
import reducer, { selectNestedData } from './dataSlice';
import { renderHook } from '@testing-library/react-hooks';
import { Provider } from 'react-redux';
import { useSelector } from 'react-redux';
describe("selectNestedData", () => {
it("returns default value when state is incomplete", () => {
// Set up store with incomplete state
const store = configureStore({
reducer: {
data: reducer,
},
preloadedState: {
data: {
// Missing expected nested properties
},
},
});
// Wrap the hook with the Redux provider
const wrapper = ({ children }) => (
<Provider store={store}>{children}</Provider>
);
// Render the hook with the selector
const { result } = renderHook(() => useSelector(selectNestedData), { wrapper });
// Verify the selector returns the fallback/default value
expect(result.current).toEqual(/* expected default value */);
});
it("returns actual data when state is complete", () => {
// Set up store with complete state
const expectedData = { value: "test" };
const store = configureStore({
reducer: {
data: reducer,
},
preloadedState: {
data: {
entities: {
items: {
123: {
details: expectedData,
},
},
},
},
},
});
const wrapper = ({ children }) => (
<Provider store={store}>{children}</Provider>
);
const { result } = renderHook(() => useSelector(state =>
selectNestedData(state, '123')
), { wrapper });
// Verify the selector returns the actual data
expect(result.current).toEqual(expectedData);
});
});
import React from 'react';
import { render, screen } from '@testing-library/react';
import { ErrorBoundary } from 'react-error-boundary';
import ComponentWithDeepAccess from './ComponentWithDeepAccess';
describe('ComponentWithDeepAccess with error boundary', () => {
it('renders fallback UI when data is invalid', () => {
// Define invalid data that would cause property access errors
const invalidData = {
// Missing required nested structure
};
const FallbackComponent = () => <div>Error occurred</div>;
render(
<ErrorBoundary FallbackComponent={FallbackComponent}>
<ComponentWithDeepAccess data={invalidData} />
</ErrorBoundary>
);
// Verify the fallback component is rendered
expect(screen.getByText('Error occurred')).toBeInTheDocument();
});
it('renders normally with valid data', () => {
// Define valid data with complete structure
const validData = {
user: {
profile: {
name: 'Test User'
}
}
};
const FallbackComponent = () => <div>Error occurred</div>;
render(
<ErrorBoundary FallbackComponent={FallbackComponent}>
<ComponentWithDeepAccess data={validData} />
</ErrorBoundary>
);
// Verify the component renders normally
expect(screen.getByText('Test User')).toBeInTheDocument();
});
});
import { safeGet } from './propertyAccessUtils';
describe('safeGet utility', () => {
it('returns the value when the path exists', () => {
const obj = {
a: {
b: {
c: 'value'
}
}
};
expect(safeGet(obj, 'a.b.c')).toBe('value');
});
it('returns default value when path does not exist', () => {
const obj = {
a: {}
};
expect(safeGet(obj, 'a.b.c', 'default')).toBe('default');
});
it('handles array indices in path', () => {
const obj = {
users: [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' }
]
};
expect(safeGet(obj, 'users.1.name')).toBe('User 2');
});
it('handles null and undefined input', () => {
expect(safeGet(null, 'a.b.c', 'default')).toBe('default');
expect(safeGet(undefined, 'a.b.c', 'default')).toBe('default');
});
});
Cypress is used for integration and end-to-end testing. These tests should verify the functionality of the application from a user's perspective.
cypress/
integration/
Editor/
Canvas.spec.ts
PropertyPane.spec.ts
Workspace/
Applications.spec.ts
describe("Application Canvas", () => {
before(() => {
cy.visit("/applications/my-app/pages/page-1/edit");
});
it("should allow adding a widget to the canvas", () => {
cy.get("[data-cy=entity-explorer]").should("be.visible");
cy.get("[data-cy=widget-button]").drag("[data-cy=canvas-drop-zone]");
cy.get("[data-cy=widget-card-button]").should("exist");
});
it("should open property pane when widget is selected", () => {
cy.get("[data-cy=widget-card-button]").click();
cy.get("[data-cy=property-pane]").should("be.visible");
cy.get("[data-cy=property-pane-title]").should("contain", "Button");
});
});
Backend unit tests should validate individual components and services.
src/test/java/com/appsmith/server/
services/
ApplicationServiceTest.java
UserServiceTest.java
controllers/
ApplicationControllerTest.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationServiceTest {
@Autowired
private ApplicationService applicationService;
@MockBean
private WorkspaceService workspaceService;
@Test
public void testCreateApplication() {
// Arrange
Application application = new Application();
application.setName("Test Application");
Workspace workspace = new Workspace();
workspace.setId("workspace-id");
Mono<Workspace> workspaceMono = Mono.just(workspace);
when(workspaceService.findById(any())).thenReturn(workspaceMono);
// Act
Mono<Application> result = applicationService.createApplication(application, "workspace-id");
// Assert
StepVerifier.create(result)
.assertNext(app -> {
assertThat(app.getId()).isNotNull();
assertThat(app.getName()).isEqualTo("Test Application");
assertThat(app.getWorkspaceId()).isEqualTo("workspace-id");
})
.verifyComplete();
}
}
Backend integration tests should verify interactions between different components of the system.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ApplicationControllerIntegrationTest {
@Autowired
private WebTestClient webTestClient;
@Autowired
private ApplicationRepository applicationRepository;
@Before
public void setUp() {
applicationRepository.deleteAll().block();
}
@Test
public void testGetAllApplications() {
// Test implementation
}
}
Flaky Tests: Tests that sometimes pass and sometimes fail.
Memory Leaks: Tests that consume increasing memory.
Slow Tests: Tests that take too long to run.
Component State Issues: Components not updating as expected.
act() for state updates, wait for async operations.Redux State Access Errors: Errors when accessing nested properties.
Rendering Errors: Components not rendering as expected.
Test with a wide range of automatically generated inputs to find edge cases.
Useful for detecting unintended changes in UI components.
Compare screenshots of components to detect visual changes.
Test system behavior under high load or stress conditions.
Compare different implementations to determine which performs better.
# Run all Jest tests
cd app/client
yarn run test:unit
# Run a specific test file
yarn jest src/path/to/test.ts
# Run Cypress tests
npx cypress run
# Run all backend tests
cd app/server
./mvnw test
# Run a specific test class
./mvnw test -Dtest=ApplicationServiceTest