Skip to content

Commit

Permalink
Add unit testing guide and example tests
Browse files Browse the repository at this point in the history
  • Loading branch information
OlimpiaZurek committed Nov 22, 2024
1 parent 2166dff commit 0399881
Show file tree
Hide file tree
Showing 4 changed files with 299 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ Often times in order to write a unit test, you may need to mock data, a componen
to help run our Unit tests.

* To run the **Jest unit tests**: `npm run test`
* UI tests guidelines can be found [here](tests/README.md)

## Performance tests
We use Reassure for monitoring performance regression. More detailed information can be found [here](tests/perf-test/README.md):
Expand Down
134 changes: 134 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,137 @@
# Getting Started

## What are UI Tests?

UI (User Interface) tests validate the visible and interactive parts of an application. They ensure that components render correctly, handle user interactions as expected, and provide a reliable user experience.

### Why are UI tests important?

- Catch regressions early: Ensure that existing functionality remains intact after changes.
- Validate that the application works as intended: Confirm that the app behaves as expected from the user’s perspective.
- Increase confidence when refactoring code: When you need to change or improve the codebase, tests help verify that changes don’t break existing features.
- Improve user experience: UI tests ensure that components interact properly, making your app more robust and user-friendly.

### Prerequisites

- Familiarity with the React Native Testing Library [RNTL](https://callstack.github.io/react-native-testing-library/).
- Basic understanding of [Jest](https://jestjs.io/).

## Best practices

### When to Add UI Tests:

1. **User Interactions**:
- Why: When the component responds to user actions, it's essential to verify that the interactions are correctly handled.
- Example: Testing if a button calls its `onPress` handler correctly.

``` javascript
test('calls onPress when button is clicked', () => {
render(<Button onPress={mockHandler} />);
fireEvent.press(screen.getByText('Click Me'));
expect(mockHandler).toHaveBeenCalled();
});
```
2. **Dynamic Behavior**:
- Components that change their state or appearance based on props or state require tests to ensure they adapt correctly.
- Example: A dropdown that expands when clicked.

``` javascript
test('expands dropdown when clicked', () => {
render(<Dropdown label="Options" options={['Option 1', 'Option 2']} />);
expect(screen.queryByText('Option 1')).not.toBeInTheDocument();

fireEvent.press(screen.getByText('Options'));
expect(screen.getByText('Option 1')).toBeInTheDocument();
expect(screen.getByText('Option 2')).toBeInTheDocument();
});
```

3. **Edge Cases**:
- It's crucial to test how your component behaves with invalid or unusual inputs to ensure stability and handle user errors gracefully
- Example: Testing an input field's behavior with empty or invalid values.

``` javascript
test('shows error message for invalid input', () => {
render(<TextInputWithValidation />);

const input = screen.getByPlaceholderText('Enter your email');
fireEvent.changeText(input, 'invalid-email');

expect(screen.getByText('Please enter a valid email')).toBeInTheDocument();
});
```

4. **Reusable UI Patterns**:
- Why: Components that are reused across the app need consistent behavior across different contexts.
- Example: Custom Checkbox or Dropdown components.


``` javascript
test('toggles state when clicked', () => {
const mockOnChange = jest.fn();
render(<Checkbox label="Accept Terms" onChange={mockOnChange} />);

const checkbox = screen.getByText('Accept Terms');
fireEvent.press(checkbox);
expect(mockOnChange).toHaveBeenCalledWith(true);

fireEvent.press(checkbox);
expect(mockOnChange).toHaveBeenCalledWith(false);
});
```
### When to Skip UI Tests for Components:

**Purely Presentational Components**:

- Why: These components don’t contain logic or interactivity, so testing them is often unnecessary. Focus on visual output and accessibility instead.
- Example: Avatar component that only displays an image, we generally don’t need tests unless there’s a specific behavior to verify.

### Do’s

- Write tests that reflect actual user behavior (e.g., clicking buttons, filling forms).
- Mock external dependencies like network calls or Onyx data.
- Use `findBy` and `waitFor` to handle asynchronous updates in the UI.
- Follow naming conventions for test IDs (`testID="component-name-action"`).
- Test for accessibility attributes like `accessibilityLabel`, `accessibilityHint`, etc., to improve test coverage for screen readers.
- Reuse test utilities: Write helper functions to handle repetitive test setup, reducing code duplication and improving maintainability.
- **Rather than targeting 100% coverage, prioritize meaningful tests for critical workflows and components**.

### Don’ts

- Don’t test implementation details (e.g., state variables or internal method calls).
- Don’t ignore warnings: Fix `act()` or other common warnings to ensure reliability.
- Avoid over-mocking: Mock dependencies sparingly to maintain realistic tests.
- *Bad*: Mocking a child component unnecessarily.
- *Good*: Mocking a network request while testing component interactions.
- Don’t hardcode timeouts: Use dynamic queries like `waitFor` instead.

### When to Skip Tests

Contributors may skip tests in the following cases:
- Non-Functional Changes: E.g.,styling updates or refactors without behavioral changes.
- Temporary Fixes: If a test will be added in a follow-up task, document the reason in the PR.
- Legacy Code: For highly complex legacy components, prioritize fixing the issue over full test coverage.

**Note**: Always document skipped tests clearly in the PR description.


### Common Pitfalls
- Not awaiting async actions: Forgetting to await async actions can result in tests failing because components haven’t updated yet.

```javascript
// Correct usage
await waitFor(() => expect(screen.getByText('Success')).toBeInTheDocument());
```
- Testing too much or too little: Striking a balance is key. Too many trivial tests lead to bloated test suites, while too few leave untested areas that could break easily.

- Not cleaning up between tests: React components often leave behind side effects. Make sure to clean up between tests to ensure they are isolated and avoid conflicts.

``` javascript
afterEach(() => {
jest.clearAllMocks(); // Clears mocks after each test
});
```

# Tips on Writing Automated Tests in Jest

[Jest](https://jestjs.io/) is a testing framework we use to ensure our most mission critical libraries are as stable as possible. Here are a few things to consider with regards to our app's architecture when testing in Jest.
Expand Down
86 changes: 86 additions & 0 deletions tests/ui/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {fireEvent, render, screen} from '@testing-library/react-native';
import React from 'react';
import colors from '@styles/theme/colors';
import Button from '@src/components/Button';
import type {ButtonProps} from '@src/components/Button';

const buttonText = 'Click me';
const accessibilityLabel = 'button-label';

describe('Button Component', () => {
const renderButton = (props: ButtonProps = {}) =>
render(
<Button
text={buttonText}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>,
);
const onPress = jest.fn();
const getButton = () => screen.getByText(buttonText);

afterEach(() => {
jest.clearAllMocks();
});

it('renders correctly with default text', () => {
// Given the component is rendered
renderButton();

// Then the default text is displayed
expect(screen.getByText(buttonText)).toBeOnTheScreen();
});

it('renders without text gracefully', () => {
// Given the component is rendered without text
renderButton({text: undefined});

// Then the button is not displayed
expect(screen.queryByText(buttonText)).not.toBeOnTheScreen();
});

it('handles press event correctly', () => {
// Given the component is rendered with an onPress function
renderButton({onPress});

// When the button is pressed
fireEvent.press(getButton());

// Then the onPress function should be called
expect(onPress).toHaveBeenCalledTimes(1);
});

it('renders loading state', () => {
// Given the component is rendered with isLoading
renderButton({isLoading: true});

// Then the loading state is displayed
expect(getButton()).toBeDisabled();
});

it('disables button when isDisabled is true', () => {
// Given the component is rendered with isDisabled true
renderButton({isDisabled: true});

// Then the button is disabled
expect(getButton()).toBeDisabled();
});

it('sets accessibility label correctly', () => {
// Given the component is rendered with an accessibility label
renderButton({accessibilityLabel});

// Then the button should be accessible using the provided label
expect(screen.getByLabelText(accessibilityLabel)).toBeOnTheScreen();
});

it('applies custom styles correctly', () => {
// Given the component is rendered with custom styles
renderButton({accessibilityLabel, innerStyles: {width: '100%'}});

// Then the button should have the custom styles
const buttonContainer = screen.getByLabelText(accessibilityLabel);
expect(buttonContainer).toHaveStyle({backgroundColor: colors.productDark400});
expect(buttonContainer).toHaveStyle({width: '100%'});
});
});
78 changes: 78 additions & 0 deletions tests/ui/components/CheckboxWithLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {fireEvent, render, screen} from '@testing-library/react-native';
import React from 'react';
import CheckboxWithLabel from '@src/components/CheckboxWithLabel';
import type {CheckboxWithLabelProps} from '@src/components/CheckboxWithLabel';
import Text from '@src/components/Text';

const LABEL = 'Agree to Terms';
describe('CheckboxWithLabel Component', () => {
const mockOnInputChange = jest.fn();
const renderCheckboxWithLabel = (props: Partial<CheckboxWithLabelProps> = {}) =>
render(
<CheckboxWithLabel
label={LABEL}
onInputChange={mockOnInputChange}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>,
);

afterEach(() => {
jest.clearAllMocks();
});

it('renders the checkbox with label', () => {
// Given the component is rendered
renderCheckboxWithLabel();
// Then the label is displayed
expect(screen.getByText(LABEL)).toBeOnTheScreen();
});

it('calls onInputChange when the checkbox is pressed', () => {
// Given the component is rendered
renderCheckboxWithLabel();

// When the checkbox is pressed
const checkbox = screen.getByText(LABEL);
fireEvent.press(checkbox);

// Then the onInputChange function should be called with 'true' (checked)
expect(mockOnInputChange).toHaveBeenCalledWith(true);

// And when the checkbox is pressed again
fireEvent.press(checkbox);

// Then the onInputChange function should be called with 'false' (unchecked)
expect(mockOnInputChange).toHaveBeenCalledWith(false);
});

it('displays error message when errorText is provided', () => {
// Given the component is rendered with an error message
const errorText = 'This field is required';
renderCheckboxWithLabel({errorText});

// Then the error message is displayed
expect(screen.getByText(errorText)).toBeOnTheScreen();
});

it('renders custom LabelComponent if provided', () => {
// Given the component is rendered with a custom LabelComponent
function MockLabelComponent() {
return <Text>Mock Label Component</Text>;
}
renderCheckboxWithLabel({LabelComponent: MockLabelComponent});

// Then the custom LabelComponent is displayed
expect(screen.getByText('Mock Label Component')).toBeOnTheScreen();
});

it('is accessible and has the correct accessibility label', () => {
// Given the component is rendered with an accessibility label
const accessibilityLabel = 'checkbox-agree-to-terms';
renderCheckboxWithLabel({accessibilityLabel});

// Then the checkbox should be accessible using the provided label
const checkbox = screen.getByLabelText(accessibilityLabel);
expect(checkbox).toBeOnTheScreen();
});
});

0 comments on commit 0399881

Please sign in to comment.