搜索

PythonReactSEOPDF
GitHub Skill

testing-library

Enforces best practices for unit testing with Jest, @testing-library/react-native, and jest-expo in Expo projects. This skill should be used when writing, reviewing, or debugging unit tests to ensure tests are accessible, maintainable, and follow Testing Library guiding principles. Use this skill for test file creation, query selection, async handling, mocking patterns, and Expo Router testing.

CodySwannGT☆ 1 仓库 Stars1 仓库 Forks

安装与使用

请根据来源仓库和你的 AI 工具说明安装此技能。先阅读技能文件和依赖要求,再添加到工作流。

查看来源 ↗使用指南

技能说明

Testing Library Best Practices

Overview

This skill enforces best practices for unit testing in Expo applications using Jest, @testing-library/react-native, and jest-expo. Tests should be user-centric, accessible, and behavior-focused rather than implementation-focused.

Core Principles

1. Test User Behavior, Not Implementation

Focus on what the component does from a user's perspective, not how it achieves it internally.

// Correct - tests visible behavior
expect(screen.getByRole("button", { name: "Submit" })).toBeEnabled();

// Incorrect - tests implementation details
expect(component.state.isSubmitting).toBe(false);
2. Use Accessible Queries

Queries should reflect how users and assistive technologies interact with the UI.

// Correct - uses accessible role and name
screen.getByRole("button", { name: /save changes/i });

// Incorrect - relies on implementation detail
screen.getByTestId("save-btn");
3. One Assertion Per Behavior

Each test should verify one behavior. Multiple assertions are acceptable when verifying different aspects of the same behavior.

// Correct - focused test
test("displays error message when submission fails", async () => {
  render(<Form />);
  await userEvent.press(screen.getByRole("button", { name: "Submit" }));
  expect(await screen.findByRole("alert")).toHaveTextContent("Failed");
});

// Incorrect - testing multiple behaviors
test("form works correctly", async () => {
  // Tests validation, submission, success, and error handling...
});
4. Prefer userEvent Over fireEvent

userEvent simulates realistic user interactions including the full event sequence.

// Correct - realistic interaction
const user = userEvent.setup();
await user.press(screen.getByRole("button", { name: "Submit" }));

// Less ideal - simplified event
fireEvent.press(screen.getByRole("button", { name: "Submit" }));

Query Priority

Choose queries based on accessibility, following this priority order:

Priority Query When to Use
1 getByRole Interactive elements, headings, buttons
2 getByLabelText Form fields with labels
3 getByText Non-interactive content, static text
4 getByTestId Last resort when semantic queries fail

For detailed query patterns, see references/query-priority.md.

Async Testing Patterns

Use findBy for Async Assertions
// Correct - waits for element to appear
expect(await screen.findByRole("alert")).toBeOnTheScreen();

// Incorrect - may fail if element appears async
expect(screen.getByRole("alert")).toBeOnTheScreen();
Use waitFor for Side Effects
// Correct - assertion inside waitFor
await waitFor(() => {
  expect(mockCallback).toHaveBeenCalledWith("success");
});

// Incorrect - side effect inside waitFor
await waitFor(() => {
  fireEvent.press(button); // Never do this
});

For comprehensive async patterns, see references/async-patterns.md.

Mocking Patterns

Required Global Mocks

These must be configured in jest/setup-jest.ts:

// AsyncStorage
jest.mock("@react-native-async-storage/async-storage", () =>
  require("@react-native-async-storage/async-storage/jest/async-storage-mock")
);

// Expo Fonts (to avoid async icon assertions)
jest.mock("expo-font", () => ({
  ...jest.requireActual("expo-font"),
  isLoaded: jest.fn(() => true),
}));

For complete mocking patterns, see references/mocking-patterns.md.

Expo Router Testing

Use renderRouter from expo-router/testing-library instead of render when testing components that use Expo Router.

import { renderRouter, screen } from "expo-router/testing-library";

test("navigates to player detail", async () => {
  renderRouter({
    index: () => <PlayerList />,
    "players/[id]": () => <PlayerDetail />,
  });

  await userEvent.press(screen.getByRole("button", { name: "View Player" }));
  expect(screen).toHavePathname("/players/123");
});

For Expo Router testing details, see references/expo-router-testing.md.

Test Structure

File Organization
  • Place test files in __tests__/ directories, not alongside source files
  • Never place tests inside the app/ directory (Expo Router constraint)
  • Use .test.ts or .test.tsx extensions
AAA Pattern

Structure every test with Arrange-Act-Assert:

test("increments counter when button pressed", async () => {
  // Arrange
  const user = userEvent.setup();
  render(<Counter initialCount={0} />);

  // Act
  await user.press(screen.getByRole("button", { name: "Increment" }));

  // Assert
  expect(screen.getByRole("text", { name: "Count: 1" })).toBeOnTheScreen();
});
Descriptive Test Names

Use descriptive names that explain the expected behavior:

// Correct - describes behavior
test("displays validation error when email format is invalid", () => {});
test("disables submit button while form is submitting", () => {});

// Incorrect - vague or implementation-focused
test("email validation works", () => {});
test("sets isSubmitting to true", () => {});

Jest Configuration

Use jest-expo Universal Preset

Configure Jest to test across all Expo platforms:

{
  "jest": {
    "preset": "jest-expo/universal"
  }
}
Use Fake Timers with userEvent
jest.useFakeTimers();

test("handles debounced input", async () => {
  const user = userEvent.setup();
  render(<SearchInput />);

  await user.type(screen.getByRole("textbox"), "query");
  jest.runAllTimers();

  expect(await screen.findByText("Results")).toBeOnTheScreen();
});

Anti-Patterns

Never Test Implementation Details
// Wrong - testing internal state
expect(wrapper.state().isLoading).toBe(true);

// Wrong - testing component methods
expect(wrapper.instance().handleSubmit).toHaveBeenCalled();

// Correct - testing visible behavior
expect(screen.getByRole("progressbar")).toBeOnTheScreen();
Never Use getByTestId as Default
// Wrong - using testID when semantic query exists
screen.getByTestId("submit-button");

// Correct - using accessible query
screen.getByRole("button", { name: "Submit" });
Never Wrap render or fireEvent in act()
// Wrong - unnecessary act wrapper
await act(async () => {
  render(<Component />);
});

// Correct - render already wraps in act
render(<Component />);
Never Put Side Effects in waitFor
// Wrong - side effect in waitFor
await waitFor(() => {
  fireEvent.press(button);
  expect(result).toBeOnTheScreen();
});

// Correct - side effect before waitFor
fireEvent.press(button);
await waitFor(() => {
  expect(result).toBeOnTheScreen();
});
Never Use Multiple Assertions in waitFor
// Wrong - multiple assertions
await waitFor(() => {
  expect(title).toBeOnTheScreen();
  expect(subtitle).toBeOnTheScreen();
  expect(button).toBeEnabled();
});

// Correct - single assertion, chain with findBy
expect(await screen.findByRole("heading")).toBeOnTheScreen();
expect(screen.getByText("Subtitle")).toBeOnTheScreen();
expect(screen.getByRole("button")).toBeEnabled();

Quick Reference

Common Matchers
Matcher Purpose
toBeOnTheScreen() Element is currently rendered
toBeEnabled() Interactive element is enabled
toBeDisabled() Interactive element is disabled
toHaveTextContent() Element contains text
toBeVisible() Element is visible to user
toBeChecked() Checkbox/radio is checked
Query Variants
Prefix Returns Throws on 0 Throws on >1 Async
getBy Element Yes Yes No
queryBy Element | null No Yes No
findBy Promise<Element> Yes Yes Yes
getAllBy Element[] Yes No No
queryAllBy Element[] No No No
findAllBy Promise<Element[]> Yes No Yes

References

使用示例

查看来源仓库中的使用示例与配置说明。

查看来源 ↗

来源历史

收录时间
2026-01-20
索引更新时间
2026-01-26
Branch
main

收录与同步日期属于本站记录。源码修改历史请查看来源仓库。

GitHub ↗

chat Comments (0)

chat_bubble_outline

No comments yet. Be the first to share your thoughts!