乐闻世界logo
搜索文章和话题

如何测试 MobX 应用?

2月21日 15:45

MobX 的测试策略和工具对于构建可靠的应用至关重要。以下是 MobX 测试的完整指南:

1. 测试 Store

基本测试

javascript
import { UserStore } from './UserStore'; describe('UserStore', () => { let store; beforeEach(() => { store = new UserStore(); }); it('should initialize with default values', () => { expect(store.user).toBeNull(); expect(store.isAuthenticated).toBe(false); }); it('should login user', async () => { await store.login({ username: 'test', password: 'test' }); expect(store.user).not.toBeNull(); expect(store.isAuthenticated).toBe(true); }); it('should logout user', () => { store.user = { id: 1, name: 'Test' }; store.isAuthenticated = true; store.logout(); expect(store.user).toBeNull(); expect(store.isAuthenticated).toBe(false); }); });

测试 computed 属性

javascript
describe('ProductStore', () => { let store; beforeEach(() => { store = new ProductStore(); }); it('should compute featured products', () => { store.products = [ { id: 1, name: 'Product 1', featured: true }, { id: 2, name: 'Product 2', featured: false }, { id: 3, name: 'Product 3', featured: true } ]; expect(store.featuredProducts).toHaveLength(2); expect(store.featuredProducts[0].name).toBe('Product 1'); expect(store.featuredProducts[1].name).toBe('Product 3'); }); it('should update when products change', () => { store.products = [{ id: 1, name: 'Product 1', featured: true }]; expect(store.featuredProducts).toHaveLength(1); store.products.push({ id: 2, name: 'Product 2', featured: true }); expect(store.featuredProducts).toHaveLength(2); }); });

测试异步 action

javascript
describe('AsyncStore', () => { let store; let mockApi; beforeEach(() => { mockApi = { fetchData: jest.fn().mockResolvedValue({ data: 'test' }) }; store = new AsyncStore(mockApi); }); it('should fetch data successfully', async () => { await store.fetchData(); expect(store.data).toEqual({ data: 'test' }); expect(store.loading).toBe(false); expect(mockApi.fetchData).toHaveBeenCalled(); }); it('should handle errors', async () => { mockApi.fetchData.mockRejectedValue(new Error('Network error')); await expect(store.fetchData()).rejects.toThrow('Network error'); expect(store.error).toBe('Network error'); expect(store.loading).toBe(false); }); });

2. 测试 React 组件

测试 observer 组件

javascript
import { render, screen, fireEvent } from '@testing-library/react'; import { observer } from 'mobx-react-lite'; import { UserStore } from './UserStore'; const TestComponent = observer(({ store }) => ( <div> {store.isAuthenticated ? ( <div>Welcome, {store.user?.name}</div> ) : ( <div>Please login</div> )} <button onClick={store.login}>Login</button> </div> )); describe('TestComponent', () => { it('should show login message when not authenticated', () => { const store = new UserStore(); render(<TestComponent store={store} />); expect(screen.getByText('Please login')).toBeInTheDocument(); }); it('should show welcome message when authenticated', () => { const store = new UserStore(); store.user = { id: 1, name: 'Test' }; store.isAuthenticated = true; render(<TestComponent store={store} />); expect(screen.getByText('Welcome, Test')).toBeInTheDocument(); }); it('should update when state changes', () => { const store = new UserStore(); render(<TestComponent store={store} />); expect(screen.getByText('Please login')).toBeInTheDocument(); store.user = { id: 1, name: 'Test' }; store.isAuthenticated = true; expect(screen.getByText('Welcome, Test')).toBeInTheDocument(); }); });

测试表单组件

javascript
describe('FormComponent', () => { it('should update form data', () => { const store = new FormStore(); render(<FormComponent store={store} />); const input = screen.getByLabelText('Name'); fireEvent.change(input, { target: { value: 'John' } }); expect(store.formData.name).toBe('John'); }); it('should submit form', async () => { const store = new FormStore(); store.submit = jest.fn(); render(<FormComponent store={store} />); const button = screen.getByText('Submit'); fireEvent.click(button); expect(store.submit).toHaveBeenCalled(); }); });

3. 使用 MobX 测试工具

使用 spy

javascript
import { spy } from 'mobx'; describe('Spy Usage', () => { it('should spy on observable changes', () => { const store = observable({ count: 0 }); const countSpy = jest.fn(); spy(store, 'count', (change) => { countSpy(change); }); store.count = 1; store.count = 2; expect(countSpy).toHaveBeenCalledTimes(2); }); });

使用 trace

javascript
import { trace } from 'mobx'; describe('Trace Usage', () => { it('should trace computed dependencies', () => { const store = observable({ firstName: 'John', lastName: 'Doe' }); const fullName = computed(() => `${store.firstName} ${store.lastName}`); // 追踪依赖 trace(fullName); expect(fullName.get()).toBe('John Doe'); }); });

使用 isObservable

javascript
import { isObservable } from 'mobx'; describe('IsObservable Usage', () => { it('should check if object is observable', () => { const observableObj = observable({ count: 0 }); const plainObj = { count: 0 }; expect(isObservable(observableObj)).toBe(true); expect(isObservable(plainObj)).toBe(false); }); });

4. Mock API 调用

使用 Jest mock

javascript
import { UserStore } from './UserStore'; describe('UserStore with API', () => { let store; let mockApi; beforeEach(() => { mockApi = { login: jest.fn(), logout: jest.fn(), fetchUser: jest.fn() }; store = new UserStore(mockApi); }); it('should call API on login', async () => { mockApi.login.mockResolvedValue({ id: 1, name: 'Test' }); await store.login({ username: 'test', password: 'test' }); expect(mockApi.login).toHaveBeenCalledWith({ username: 'test', password: 'test' }); }); it('should handle API errors', async () => { mockApi.login.mockRejectedValue(new Error('Invalid credentials')); await expect(store.login({ username: 'test', password: 'test' })) .rejects.toThrow('Invalid credentials'); expect(store.error).toBe('Invalid credentials'); }); });

使用 MSW (Mock Service Worker)

javascript
import { setupServer, rest } from 'msw'; import { UserStore } from './UserStore'; const server = setupServer( rest.post('/api/login', (req, res, ctx) => { return res( ctx.status(200), ctx.json({ id: 1, name: 'Test' }) ); }) ); describe('UserStore with MSW', () => { let store; beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); beforeEach(() => { store = new UserStore(); }); it('should login successfully', async () => { await store.login({ username: 'test', password: 'test' }); expect(store.user).toEqual({ id: 1, name: 'Test' }); expect(store.isAuthenticated).toBe(true); }); });

5. 测试 reaction

javascript
describe('Reaction Testing', () => { it('should trigger reaction when observable changes', () => { const store = observable({ count: 0 }); const reactionSpy = jest.fn(); reaction( () => store.count, (count) => { reactionSpy(count); } ); store.count = 1; expect(reactionSpy).toHaveBeenCalledWith(1); store.count = 2; expect(reactionSpy).toHaveBeenCalledWith(2); }); it('should not trigger when value is same', () => { const store = observable({ count: 0 }); const reactionSpy = jest.fn(); reaction( () => store.count, (count) => { reactionSpy(count); } ); store.count = 0; expect(reactionSpy).not.toHaveBeenCalled(); }); });

6. 测试中间件

javascript
describe('Middleware Testing', () => { it('should call middleware before action', () => { const middlewareSpy = jest.fn(); const actionSpy = jest.fn(); const store = { @observable count: 0, @action increment() { this.count++; } }; const originalIncrement = store.increment; store.increment = function(...args) { middlewareSpy(...args); return originalIncrement.apply(this, args); }; store.increment(); expect(middlewareSpy).toHaveBeenCalled(); expect(actionSpy).toHaveBeenCalled(); }); });

7. 集成测试

javascript
describe('Integration Tests', () => { it('should handle complete user flow', async () => { const store = new RootStore(); // 登录 await store.userStore.login({ username: 'test', password: 'test' }); expect(store.userStore.isAuthenticated).toBe(true); // 加载数据 await store.dataStore.loadData(); expect(store.dataStore.data).not.toBeNull(); // 添加到购物车 store.cartStore.addItem(store.dataStore.data[0]); expect(store.cartStore.items).toHaveLength(1); // 结账 await store.cartStore.checkout(); expect(store.cartStore.items).toHaveLength(0); }); });

8. 测试最佳实践

1. 隔离测试

javascript
// 每个测试都应该独立 beforeEach(() => { store = new Store(); }); // 清理副作用 afterEach(() => { if (store.dispose) { store.dispose(); } });

2. 使用快照测试

javascript
it('should match snapshot', () => { const store = new Store(); store.data = { id: 1, name: 'Test' }; expect(toJS(store.data)).toMatchSnapshot(); });

3. 测试边界情况

javascript
it('should handle empty array', () => { const store = new Store(); store.items = []; expect(store.itemCount).toBe(0); }); it('should handle null values', () => { const store = new Store(); store.user = null; expect(store.userName).toBe('Guest'); });

4. 测试错误处理

javascript
it('should handle network errors gracefully', async () => { const store = new Store(); mockApi.fetchData.mockRejectedValue(new Error('Network error')); await expect(store.fetchData()).rejects.toThrow('Network error'); expect(store.error).toBe('Network error'); expect(store.loading).toBe(false); });

总结

MobX 测试的关键点:

  1. 测试 Store:验证 observable、computed 和 action 的行为
  2. 测试组件:验证 observer 组件的响应性
  3. 使用测试工具:spy、trace、isObservable
  4. Mock API:使用 Jest mock 或 MSW
  5. 测试 reaction:验证副作用是否正确触发
  6. 测试中间件:验证中间件是否正确执行
  7. 集成测试:验证多个 store 之间的交互
  8. 最佳实践:隔离测试、快照测试、边界情况、错误处理

遵循这些测试策略,可以构建可靠、可维护的 MobX 应用。

标签:Mobx