react-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact Testing
React 测试
For general UI testing patterns (queries, events, async, accessibility), load the skill. For TDD workflow, load the skill.
front-end-testingtdd如需通用UI测试模式(查询、事件、异步、无障碍),请加载技能。如需TDD工作流,请加载技能。
front-end-testingtddVitest Browser Mode with React (Preferred)
搭配React的Vitest浏览器模式(推荐)
Always prefer over . Tests run in a real browser, giving production-accurate rendering, events, and CSS.
vitest-browser-react@testing-library/react优先使用而非。 测试在真实浏览器中运行,可提供与生产环境一致的渲染、事件和CSS效果。
vitest-browser-react@testing-library/reactSetup
配置步骤
bash
npm install -D vitest @vitest/browser-playwright vitest-browser-react @vitejs/plugin-reacttypescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
browser: {
enabled: true,
provider: playwright(),
headless: true,
instances: [{ browser: 'chromium' }],
},
},
})bash
npm install -D vitest @vitest/browser-playwright vitest-browser-react @vitejs/plugin-reacttypescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
browser: {
enabled: true,
provider: playwright(),
headless: true,
instances: [{ browser: 'chromium' }],
},
},
})Component Testing
组件测试
tsx
import { render } from 'vitest-browser-react'
import { expect, test } from 'vitest'
test('should display user name when provided', async () => {
const screen = await render(<UserProfile name="Alice" email="alice@example.com" />)
await expect.element(screen.getByText(/alice/i)).toBeVisible()
await expect.element(screen.getByText(/alice@example.com/i)).toBeVisible()
})Key differences from :
@testing-library/react- is async — use
render()await - Returns a scoped to the rendered component
screen - Use for auto-retrying assertions
expect.element() - No wrapper needed — CDP events + retry handle timing
act() - Auto-cleanup happens before each test (not after), so components stay visible for debugging
tsx
import { render } from 'vitest-browser-react'
import { expect, test } from 'vitest'
test('should display user name when provided', async () => {
const screen = await render(<UserProfile name="Alice" email="alice@example.com" />)
await expect.element(screen.getByText(/alice/i)).toBeVisible()
await expect.element(screen.getByText(/alice@example.com/i)).toBeVisible()
})与的主要区别:
@testing-library/react- 为异步方法 —— 需要使用
render()await - 返回作用于渲染组件的对象
screen - 使用实现自动重试断言
expect.element() - 无需包装 —— CDP事件和重试机制处理时序问题
act() - 自动清理在每个测试前执行(而非测试后),便于调试时查看组件状态
Testing Props and Callbacks
测试Props与回调函数
tsx
test('should call onSubmit when form submitted', async () => {
const handleSubmit = vi.fn()
const screen = await render(<LoginForm onSubmit={handleSubmit} />)
await screen.getByLabelText(/email/i).fill('test@example.com')
await screen.getByRole('button', { name: /submit/i }).click()
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
})
})tsx
test('should call onSubmit when form submitted', async () => {
const handleSubmit = vi.fn()
const screen = await render(<LoginForm onSubmit={handleSubmit} />)
await screen.getByLabelText(/email/i).fill('test@example.com')
await screen.getByRole('button', { name: /submit/i }).click()
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
})
})Testing Conditional Rendering
测试条件渲染
tsx
test('should show error message when login fails', async () => {
server.use(
http.post('/api/login', () => {
return HttpResponse.json({ error: 'Invalid credentials' }, { status: 401 })
})
)
const screen = await render(<LoginForm />)
await screen.getByLabelText(/email/i).fill('wrong@example.com')
await screen.getByRole('button', { name: /submit/i }).click()
await expect.element(screen.getByText(/invalid credentials/i)).toBeVisible()
})tsx
test('should show error message when login fails', async () => {
server.use(
http.post('/api/login', () => {
return HttpResponse.json({ error: 'Invalid credentials' }, { status: 401 })
})
)
const screen = await render(<LoginForm />)
await screen.getByLabelText(/email/i).fill('wrong@example.com')
await screen.getByRole('button', { name: /submit/i }).click()
await expect.element(screen.getByText(/invalid credentials/i)).toBeVisible()
})Testing Hooks with renderHook
使用renderHook测试Hooks
tsx
import { renderHook } from 'vitest-browser-react'
test('should toggle value', async () => {
const { result } = await renderHook(() => useToggle(false))
expect(result.current.value).toBe(false)
await act(() => {
result.current.toggle()
})
expect(result.current.value).toBe(true)
})tsx
import { renderHook } from 'vitest-browser-react'
test('should toggle value', async () => {
const { result } = await renderHook(() => useToggle(false))
expect(result.current.value).toBe(false)
await act(() => {
result.current.toggle()
})
expect(result.current.value).toBe(true)
})Testing Context Providers
测试上下文提供者(Context Providers)
tsx
test('should show user menu when authenticated', async () => {
const screen = await render(
<AuthProvider initialUser={{ name: 'Alice', role: 'admin' }}>
<Dashboard />
</AuthProvider>
)
await expect.element(screen.getByRole('button', { name: /user menu/i })).toBeVisible()
})For hooks that need context:
tsx
const { result } = await renderHook(() => useAuth(), {
wrapper: ({ children }) => (
<AuthProvider>{children}</AuthProvider>
),
})tsx
test('should show user menu when authenticated', async () => {
const screen = await render(
<AuthProvider initialUser={{ name: 'Alice', role: 'admin' }}>
<Dashboard />
</AuthProvider>
)
await expect.element(screen.getByRole('button', { name: /user menu/i })).toBeVisible()
})对于需要上下文的hooks:
tsx
const { result } = await renderHook(() => useAuth(), {
wrapper: ({ children }) => (
<AuthProvider>{children}</AuthProvider>
),
})Legacy: @testing-library/react Patterns
旧版方案:@testing-library/react 测试模式
The patterns below apply when using with jsdom. Prefer for new projects.
@testing-library/reactvitest-browser-react以下模式适用于搭配jsdom使用的场景。新项目优先选择。
@testing-library/reactvitest-browser-reactTesting React Components
React组件测试
React components are just functions that return JSX. Test them like functions: inputs (props) → output (rendered DOM).
React组件本质是返回JSX的函数。 像测试函数一样测试它们:输入(props)→ 输出(渲染后的DOM)。
Basic Component Testing
基础组件测试
tsx
// ✅ CORRECT - Test component behavior
it('should display user name when provided', () => {
render(<UserProfile name="Alice" email="alice@example.com" />);
expect(screen.getByText(/alice/i)).toBeInTheDocument();
expect(screen.getByText(/alice@example.com/i)).toBeInTheDocument();
});tsx
// ❌ WRONG - Testing implementation
it('should set name state', () => {
const wrapper = mount(<UserProfile name="Alice" />);
expect(wrapper.state('name')).toBe('Alice'); // Internal state!
});tsx
// ✅ 正确方式 - 测试组件行为
it('should display user name when provided', () => {
render(<UserProfile name="Alice" email="alice@example.com" />);
expect(screen.getByText(/alice/i)).toBeInTheDocument();
expect(screen.getByText(/alice@example.com/i)).toBeInTheDocument();
});tsx
// ❌ 错误方式 - 测试内部实现
it('should set name state', () => {
const wrapper = mount(<UserProfile name="Alice" />);
expect(wrapper.state('name')).toBe('Alice'); // 内部状态!
});Testing Props
测试Props
tsx
// ✅ CORRECT - Test how props affect rendered output
it('should call onSubmit when form submitted', async () => {
const handleSubmit = vi.fn();
const user = userEvent.setup();
render(<LoginForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
});
});tsx
// ✅ 正确方式 - 测试props对渲染结果的影响
it('should call onSubmit when form submitted', async () => {
const handleSubmit = vi.fn();
const user = userEvent.setup();
render(<LoginForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
});
});Testing Conditional Rendering
测试条件渲染
tsx
// ✅ CORRECT - Test what user sees in different states
it('should show error message when login fails', async () => {
server.use(
http.post('/api/login', () => {
return HttpResponse.json({ error: 'Invalid credentials' }, { status: 401 });
})
);
const user = userEvent.setup();
render(<LoginForm />);
await user.type(screen.getByLabelText(/email/i), 'wrong@example.com');
await user.click(screen.getByRole('button', { name: /submit/i }));
await screen.findByText(/invalid credentials/i);
});tsx
// ✅ 正确方式 - 测试用户在不同状态下看到的内容
it('should show error message when login fails', async () => {
server.use(
http.post('/api/login', () => {
return HttpResponse.json({ error: 'Invalid credentials' }, { status: 401 });
})
);
const user = userEvent.setup();
render(<LoginForm />);
await user.type(screen.getByLabelText(/email/i), 'wrong@example.com');
await user.click(screen.getByRole('button', { name: /submit/i }));
await screen.findByText(/invalid credentials/i);
});Testing React Hooks
React Hooks测试
Custom Hooks with renderHook
使用renderHook测试自定义Hooks
Built into (import directly, no separate package needed):
@testing-library/reacttsx
import { renderHook } from '@testing-library/react';
it('should toggle value', () => {
const { result } = renderHook(() => useToggle(false));
expect(result.current.value).toBe(false);
act(() => {
result.current.toggle();
});
expect(result.current.value).toBe(true);
});Pattern:
- - Current return value of hook
result.current - - Wrap state updates
act() - - Re-run hook with new props
rerender()
@testing-library/reacttsx
import { renderHook } from '@testing-library/react';
it('should toggle value', () => {
const { result } = renderHook(() => useToggle(false));
expect(result.current.value).toBe(false);
act(() => {
result.current.toggle();
});
expect(result.current.value).toBe(true);
});模式说明:
- - Hook当前的返回值
result.current - - 包裹状态更新操作
act() - - 使用新props重新运行Hook
rerender()
Hooks with Props
带Props的Hooks测试
tsx
it('should accept initial value', () => {
const { result, rerender } = renderHook(
({ initialValue }) => useCounter(initialValue),
{ initialProps: { initialValue: 10 } }
);
expect(result.current.count).toBe(10);
// Test with different initial value
rerender({ initialValue: 20 });
expect(result.current.count).toBe(20);
});tsx
it('should accept initial value', () => {
const { result, rerender } = renderHook(
({ initialValue }) => useCounter(initialValue),
{ initialProps: { initialValue: 10 } }
);
expect(result.current.count).toBe(10);
// 使用不同初始值测试
rerender({ initialValue: 20 });
expect(result.current.count).toBe(20);
});Testing Context
上下文(Context)测试
wrapper Option
wrapper选项
For hooks that need context providers:
tsx
const { result } = renderHook(() => useAuth(), {
wrapper: ({ children }) => (
<AuthProvider>
{children}
</AuthProvider>
),
});
expect(result.current.user).toBeNull();
act(() => {
result.current.login({ email: 'test@example.com' });
});
expect(result.current.user).toEqual({ email: 'test@example.com' });对于需要上下文提供者的hooks:
tsx
const { result } = renderHook(() => useAuth(), {
wrapper: ({ children }) => (
<AuthProvider>
{children}
</AuthProvider>
),
});
expect(result.current.user).toBeNull();
act(() => {
result.current.login({ email: 'test@example.com' });
});
expect(result.current.user).toEqual({ email: 'test@example.com' });Multiple Providers
多提供者嵌套
tsx
const AllProviders = ({ children }) => (
<AuthProvider>
<ThemeProvider>
<RouterProvider>
{children}
</RouterProvider>
</ThemeProvider>
</AuthProvider>
);
const { result } = renderHook(() => useMyHook(), {
wrapper: AllProviders,
});tsx
const AllProviders = ({ children }) => (
<AuthProvider>
<ThemeProvider>
<RouterProvider>
{children}
</RouterProvider>
</ThemeProvider>
</AuthProvider>
);
const { result } = renderHook(() => useMyHook(), {
wrapper: AllProviders,
});Testing Components with Context
测试带上下文的组件
tsx
// ✅ CORRECT - Wrap component in provider
const renderWithAuth = (ui, { user = null, ...options } = {}) => {
return render(
<AuthProvider initialUser={user}>
{ui}
</AuthProvider>,
options
);
};
it('should show user menu when authenticated', () => {
renderWithAuth(<Dashboard />, {
user: { name: 'Alice', role: 'admin' },
});
expect(screen.getByRole('button', { name: /user menu/i })).toBeInTheDocument();
});tsx
// ✅ 正确方式 - 将组件包裹在提供者中
const renderWithAuth = (ui, { user = null, ...options } = {}) => {
return render(
<AuthProvider initialUser={user}>
{ui}
</AuthProvider>,
options
);
};
it('should show user menu when authenticated', () => {
renderWithAuth(<Dashboard />, {
user: { name: 'Alice', role: 'admin' },
});
expect(screen.getByRole('button', { name: /user menu/i })).toBeInTheDocument();
});Testing Forms
表单测试
Controlled Inputs
受控输入框
tsx
it('should update input value as user types', async () => {
const user = userEvent.setup();
render(<SearchInput />);
const input = screen.getByLabelText(/search/i);
await user.type(input, 'react');
expect(input).toHaveValue('react');
});tsx
it('should update input value as user types', async () => {
const user = userEvent.setup();
render(<SearchInput />);
const input = screen.getByLabelText(/search/i);
await user.type(input, 'react');
expect(input).toHaveValue('react');
});Form Submissions
表单提交
tsx
it('should submit form with user input', async () => {
const handleSubmit = vi.fn();
const user = userEvent.setup();
render(<RegistrationForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/name/i), 'Alice');
await user.type(screen.getByLabelText(/email/i), 'alice@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /sign up/i }));
expect(handleSubmit).toHaveBeenCalledWith({
name: 'Alice',
email: 'alice@example.com',
password: 'password123',
});
});tsx
it('should submit form with user input', async () => {
const handleSubmit = vi.fn();
const user = userEvent.setup();
render(<RegistrationForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/name/i), 'Alice');
await user.type(screen.getByLabelText(/email/i), 'alice@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /sign up/i }));
expect(handleSubmit).toHaveBeenCalledWith({
name: 'Alice',
email: 'alice@example.com',
password: 'password123',
});
});Form Validation
表单验证
tsx
it('should show validation errors for invalid input', async () => {
const user = userEvent.setup();
render(<RegistrationForm />);
// Submit empty form
await user.click(screen.getByRole('button', { name: /sign up/i }));
// Validation errors appear
expect(screen.getByText(/name is required/i)).toBeInTheDocument();
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
});tsx
it('should show validation errors for invalid input', async () => {
const user = userEvent.setup();
render(<RegistrationForm />);
// 提交空表单
await user.click(screen.getByRole('button', { name: /sign up/i }));
// 显示验证错误
expect(screen.getByText(/name is required/i)).toBeInTheDocument();
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
});React-Specific Anti-Patterns
React专属反模式
1. Unnecessary act() wrapping
1. 不必要的act()包裹
❌ WRONG - Manual act() everywhere
tsx
act(() => {
render(<MyComponent />);
});
await act(async () => {
await user.click(button);
});✅ CORRECT - RTL handles it
tsx
render(<MyComponent />);
await user.click(button);Modern RTL auto-wraps:
render()- methods
userEvent fireEvent- ,
waitForfindBy
When you DO need manual :
act()- Custom hook state updates ()
renderHook - Direct state mutations (rare, usually bad practice)
❌ 错误方式 - 手动到处使用act()
tsx
act(() => {
render(<MyComponent />);
});
await act(async () => {
await user.click(button);
});✅ 正确方式 - RTL自动处理
tsx
render(<MyComponent />);
await user.click(button);现代RTL自动包裹以下操作:
render()- 方法
userEvent fireEvent- ,
waitFor系列方法findBy
需要手动使用的场景:
act()- 自定义Hook的状态更新(中)
renderHook - 直接修改状态(罕见,通常是不良实践)
2. Manual cleanup() calls
2. 手动调用cleanup()
❌ WRONG - Manual cleanup
tsx
afterEach(() => {
cleanup(); // Automatic since RTL 9!
});✅ CORRECT - No cleanup needed
tsx
// Cleanup happens automatically after each test❌ 错误方式 - 手动清理
tsx
afterEach(() => {
cleanup(); // RTL 9版本后已自动执行!
});✅ 正确方式 - 无需手动清理
tsx
// 每个测试后自动执行清理3. beforeEach render pattern
3. beforeEach中渲染组件的模式
❌ WRONG - Shared render in beforeEach
tsx
let button;
beforeEach(() => {
render(<MyComponent />);
button = screen.getByRole('button'); // Shared state across tests
});
it('test 1', () => {
// Uses shared button from beforeEach
});✅ CORRECT - Factory function per test
tsx
const renderComponent = () => {
render(<MyComponent />);
return {
button: screen.getByRole('button'),
};
};
it('test 1', () => {
const { button } = renderComponent(); // Fresh state
});For factory patterns, see skill.
testing❌ 错误方式 - 在beforeEach中共享渲染
tsx
let button;
beforeEach(() => {
render(<MyComponent />);
button = screen.getByRole('button'); // 测试间共享状态
});
it('test 1', () => {
// 使用beforeEach中共享的button
});✅ 正确方式 - 每个测试使用工厂函数
tsx
const renderComponent = () => {
render(<MyComponent />);
return {
button: screen.getByRole('button'),
};
};
it('test 1', () => {
const { button } = renderComponent(); // 全新状态
});关于工厂模式,请查看技能。
testing4. Testing component internals
4. 测试组件内部实现
❌ WRONG - Accessing component internals
tsx
const wrapper = shallow(<MyComponent />);
expect(wrapper.state('isOpen')).toBe(true); // Internal state
expect(wrapper.instance().handleClick).toBeDefined(); // Internal method✅ CORRECT - Test rendered output
tsx
render(<MyComponent />);
expect(screen.getByRole('dialog')).toBeInTheDocument(); // What user sees❌ 错误方式 - 访问组件内部
tsx
const wrapper = shallow(<MyComponent />);
expect(wrapper.state('isOpen')).toBe(true); // 内部状态
expect(wrapper.instance().handleClick).toBeDefined(); // 内部方法✅ 正确方式 - 测试渲染输出
tsx
render(<MyComponent />);
expect(screen.getByRole('dialog')).toBeInTheDocument(); // 用户可见内容5. Shallow rendering
5. 浅渲染(Shallow rendering)
❌ WRONG - Shallow rendering
tsx
const wrapper = shallow(<MyComponent />);
// Child components not rendered - incomplete test✅ CORRECT - Full rendering
tsx
render(<MyComponent />);
// Full component tree rendered - realistic testWhy: Shallow rendering hides integration bugs between parent/child components.
❌ 错误方式 - 浅渲染
tsx
const wrapper = shallow(<MyComponent />);
// 子组件未渲染 - 测试不完整✅ 正确方式 - 完整渲染
tsx
render(<MyComponent />);
// 完整组件树渲染 - 更贴近真实场景的测试原因: 浅渲染会隐藏父子组件间的集成bug。
Testing Loading States
加载状态测试
tsx
it('should show loading then data', async () => {
render(<UserList />);
// Initially loading
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Wait for data
await screen.findByText(/alice/i);
// Loading gone
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});tsx
it('should show loading then data', async () => {
render(<UserList />);
// 初始状态为加载中
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// 等待数据加载完成
await screen.findByText(/alice/i);
// 加载状态消失
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});Testing Error Boundaries
错误边界(Error Boundaries)测试
tsx
it('should catch errors with error boundary', () => {
// Suppress console.error for this test
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
render(
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<ThrowsError />
</ErrorBoundary>
);
expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
spy.mockRestore();
});tsx
it('should catch errors with error boundary', () => {
// 抑制本次测试的console.error输出
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
render(
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<ThrowsError />
</ErrorBoundary>
);
expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
spy.mockRestore();
});Testing Portals
门户(Portals)测试
tsx
it('should render modal in portal', () => {
render(<Modal isOpen={true}>Modal content</Modal>);
// Portal renders outside root, but Testing Library finds it
expect(screen.getByText(/modal content/i)).toBeInTheDocument();
});Testing Library queries the entire document, so portals work automatically.
tsx
it('should render modal in portal', () => {
render(<Modal isOpen={true}>Modal content</Modal>);
// 门户渲染在根节点外,但Testing Library仍能找到
expect(screen.getByText(/modal content/i)).toBeInTheDocument();
});Testing Library会查询整个文档, 因此门户可自动被检测到。
Testing Suspense
Suspense测试
tsx
it('should show fallback then content', async () => {
render(
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
// Initially fallback
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Wait for component
await screen.findByText(/lazy content/i);
});tsx
it('should show fallback then content', async () => {
render(
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
// 初始显示fallback
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// 等待组件加载完成
await screen.findByText(/lazy content/i);
});Summary Checklist
检查清单总结
React-specific checks:
- Preferred: Using with Vitest Browser Mode (real browser)
vitest-browser-react - Fallback: Using if Browser Mode not yet configured
@testing-library/react - All Playwright/Browser Mode tests are idempotent (no shared state between tests)
- Using for custom hooks
renderHook() - Using option for context providers
wrapper - No manual calls (handled automatically)
act() - No manual calls (automatic)
cleanup() - Testing component output, not internal state
- Using factory functions, not render
beforeEach - Using for auto-retrying assertions (Browser Mode)
expect.element() - Following TDD workflow (see skill)
tdd - Using general UI testing patterns (see skill)
front-end-testing - Using test factories for data (see skill)
testing
React专属检查项:
- 推荐方案:使用搭配Vitest浏览器模式的(真实浏览器环境)
vitest-browser-react - 备选方案:若未配置浏览器模式,使用
@testing-library/react - 所有Playwright/浏览器模式测试均为幂等(测试间无共享状态)
- 使用测试自定义Hooks
renderHook() - 使用选项配置上下文提供者
wrapper - 无手动调用(由框架自动处理)
act() - 无手动调用(自动执行)
cleanup() - 测试组件输出,而非内部状态
- 使用工厂函数,而非中渲染组件
beforeEach - 使用实现自动重试断言(浏览器模式)
expect.element() - 遵循TDD工作流(查看技能)
tdd - 使用通用UI测试模式(查看技能)
front-end-testing - 使用测试工厂生成数据(查看技能)
testing