# Appsmith Testing Guide
This guide outlines best practices for writing tests for the Appsmith codebase.
## Frontend Testing
### Unit Tests with Jest
Appsmith uses Jest for frontend unit tests. Unit tests should be written for individual components, utility functions, and Redux slices.
#### Test File Structure
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
```
#### Writing React Component Tests
```typescript
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();
expect(screen.getByText("Click me")).toBeInTheDocument();
});
it("calls onClick handler when clicked", () => {
const handleClick = jest.fn();
render();
fireEvent.click(screen.getByText("Click me"));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
```
#### Redux Testing
```typescript
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: "test@example.com" };
expect(
reducer(
{ userInfo: null, isLoading: false, error: null },
setUserInfo(userInfo)
)
).toEqual({
userInfo,
isLoading: false,
error: null
});
});
});
```
### Testing Redux/React Safety Patterns
Safety when accessing deeply nested properties in Redux state is critical for application reliability. Here are patterns for testing these safety mechanisms:
#### Testing Redux Selectors with Incomplete State
```typescript
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 }) => (
{children}
);
// 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 }) => (
{children}
);
const { result } = renderHook(() => useSelector(state =>
selectNestedData(state, '123')
), { wrapper });
// Verify the selector returns the actual data
expect(result.current).toEqual(expectedData);
});
});
```
#### Testing Components with Error Boundaries
```typescript
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 = () =>
Error occurred
;
render(
);
// 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 = () =>
Error occurred
;
render(
);
// Verify the component renders normally
expect(screen.getByText('Test User')).toBeInTheDocument();
});
});
```
#### Testing Safe Property Access Utilities
```typescript
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');
});
});
```
### Integration Tests with Cypress
Cypress is used for integration and end-to-end testing. These tests should verify the functionality of the application from a user's perspective.
#### Test File Structure
```
cypress/
integration/
Editor/
Canvas.spec.ts
PropertyPane.spec.ts
Workspace/
Applications.spec.ts
```
#### Writing Cypress Tests
```typescript
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 Testing
### Unit Tests with JUnit
Backend unit tests should validate individual components and services.
#### Test File Structure
```
src/test/java/com/appsmith/server/
services/
ApplicationServiceTest.java
UserServiceTest.java
controllers/
ApplicationControllerTest.java
```
#### Writing Java Unit Tests
```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 workspaceMono = Mono.just(workspace);
when(workspaceService.findById(any())).thenReturn(workspaceMono);
// Act
Mono 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();
}
}
```
### Integration Tests
Backend integration tests should verify interactions between different components of the system.
```java
@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
}
}
```
## Best Practices
### General Test Guidelines
1. **Test Isolation**: Each test should be independent of others.
2. **Test Coverage**: Aim for 80%+ coverage for critical code paths.
3. **Avoid Implementation Details**: Test behavior, not implementation.
4. **Concise Tests**: Keep tests focused on one behavior or functionality.
5. **Descriptive Names**: Use clear test names that describe what is being tested.
### Redux/React Safety Best Practices
1. **Always Check Property Existence**: Test edge cases where properties might not exist.
2. **Use Defensive Programming**: Design components and selectors to handle incomplete data gracefully.
3. **Test Error Boundaries**: Verify that error boundaries correctly catch and handle errors from property access.
4. **Test Default Values**: Ensure selectors return appropriate defaults when data is missing.
5. **Test Different State Permutations**: Create tests with various combinations of missing or incomplete state to ensure robustness.
### Performance Considerations
1. **Mock Heavy Dependencies**: Use mocks for API calls, databases, etc.
2. **Optimize Test Speed**: Keep tests fast to encourage frequent testing.
3. **Use Focused Tests**: Test only what needs to be tested.
## Troubleshooting Tests
### Common Issues
1. **Flaky Tests**: Tests that sometimes pass and sometimes fail.
- Solution: Make tests more deterministic, avoid race conditions.
2. **Memory Leaks**: Tests that consume increasing memory.
- Solution: Clean up resources, avoid global state.
3. **Slow Tests**: Tests that take too long to run.
- Solution: Mock heavy dependencies, parallelize when possible.
### React-Specific Issues
1. **Component State Issues**: Components not updating as expected.
- Solution: Use `act()` for state updates, wait for async operations.
2. **Redux State Access Errors**: Errors when accessing nested properties.
- Solution: Use optional chaining, lodash/get, or default values in selectors.
3. **Rendering Errors**: Components not rendering as expected.
- Solution: Verify props, check for conditionals that might prevent rendering.
## Advanced Testing Techniques
### Property-Based Testing
Test with a wide range of automatically generated inputs to find edge cases.
### Snapshot Testing
Useful for detecting unintended changes in UI components.
### Visual Regression Testing
Compare screenshots of components to detect visual changes.
### Load and Performance Testing
Test system behavior under high load or stress conditions.
### A/B Testing
Compare different implementations to determine which performs better.
## Test Data Best Practices
### Creating Test Fixtures
- Create reusable fixtures for common test data
- Use descriptive names for test fixtures
- Keep test data minimal but sufficient
### Mocking External Services
- Mock external API calls and dependencies
- Use realistic mock responses
- Consider edge cases and error conditions
## Testing Standards
### Frontend Testing Standards
1. Aim for 80%+ test coverage for utility functions
2. Test all Redux slices thoroughly
3. Focus on critical user journeys in integration tests
4. Test responsive behavior for key components
5. Include accessibility tests for UI components
### Backend Testing Standards
1. Test all public service methods
2. Test both successful and error cases
3. Test database interactions with real repositories
4. Test API endpoints with WebTestClient
5. Mock external services to isolate tests
## Running Tests
### Frontend Tests
```bash
# 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
```
### Backend Tests
```bash
# Run all backend tests
cd app/server
./mvnw test
# Run a specific test class
./mvnw test -Dtest=ApplicationServiceTest
```
## Best Practices for Test-Driven Development
1. Write failing tests first
2. Start with simple test cases
3. Refactor after tests pass
4. Use descriptive test names
5. Keep tests independent
6. Avoid test interdependence
7. Test edge cases and error conditions
8. Keep tests fast
9. Avoid testing implementation details
10. Review and update tests when requirements change