feat(miniprogram): service 层测试框架搭建
- 新增 __tests__/helpers/: mock-taro (Taro API mock) + mock-api (request mock) - 示例测试: patient.test.ts (3 用例) + appointment.test.ts (9 用例) - 覆盖 list/create/update/cancel/calendar 等核心场景 - 全部 42 测试通过(含 4 个已有 BLE 测试)
This commit is contained in:
2
apps/miniprogram/__tests__/helpers/index.ts
Normal file
2
apps/miniprogram/__tests__/helpers/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { mockTaro, createTaroMock } from './mock-taro';
|
||||
export { mockApi, apiOk } from './mock-api';
|
||||
19
apps/miniprogram/__tests__/helpers/mock-api.ts
Normal file
19
apps/miniprogram/__tests__/helpers/mock-api.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// 顶层 mock — vitest 自动提升,无警告
|
||||
vi.mock('@/services/request', () => ({
|
||||
api: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
clearRequestCache: vi.fn(),
|
||||
markLoggingOut: vi.fn(),
|
||||
clearLoggingOut: vi.fn(),
|
||||
}));
|
||||
|
||||
/** 创建一个成功的 API 响应 */
|
||||
export function apiOk<T>(data: T) {
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
50
apps/miniprogram/__tests__/helpers/mock-taro.ts
Normal file
50
apps/miniprogram/__tests__/helpers/mock-taro.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
/**
|
||||
* Taro API mock — 所有小程序测试共享的基础 mock。
|
||||
* storage 使用内存 Map,可在 beforeEach 中 clear。
|
||||
*/
|
||||
export function createTaroMock() {
|
||||
const storage = new Map<string, any>();
|
||||
|
||||
return {
|
||||
storage,
|
||||
mock: {
|
||||
default: {
|
||||
getStorageSync: vi.fn((key: string) => storage.get(key) ?? ''),
|
||||
setStorageSync: vi.fn((key: string, val: any) => storage.set(key, val)),
|
||||
removeStorageSync: vi.fn((key: string) => storage.delete(key)),
|
||||
showToast: vi.fn(),
|
||||
hideToast: vi.fn(),
|
||||
showLoading: vi.fn(),
|
||||
hideLoading: vi.fn(),
|
||||
request: vi.fn(),
|
||||
reLaunch: vi.fn(),
|
||||
navigateTo: vi.fn(),
|
||||
redirectTo: vi.fn(),
|
||||
switchTab: vi.fn(),
|
||||
getCurrentPages: vi.fn(() => []),
|
||||
getAccountInfoSync: vi.fn(() => ({ miniProgram: { envVersion: 'develop' } })),
|
||||
getSystemInfoSync: vi.fn(() => ({
|
||||
windowHeight: 800,
|
||||
windowWidth: 375,
|
||||
pixelRatio: 2,
|
||||
})),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 一键 mock @tarojs/taro — 在 vi.mock 回调中使用 */
|
||||
export function mockTaro() {
|
||||
const { storage, mock } = createTaroMock();
|
||||
|
||||
vi.mock('@tarojs/taro', () => mock);
|
||||
vi.mock('@/utils/secure-storage', () => ({
|
||||
secureGet: vi.fn((key: string) => storage.get(key) ?? ''),
|
||||
secureSet: vi.fn((key: string, val: string) => storage.set(key, val)),
|
||||
secureRemove: vi.fn((key: string) => storage.delete(key)),
|
||||
}));
|
||||
|
||||
return { storage };
|
||||
}
|
||||
149
apps/miniprogram/__tests__/services/appointment.test.ts
Normal file
149
apps/miniprogram/__tests__/services/appointment.test.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import '../helpers/mock-api';
|
||||
|
||||
import { api } from '@/services/request';
|
||||
import {
|
||||
listAppointments,
|
||||
getAppointment,
|
||||
createAppointment,
|
||||
cancelAppointment,
|
||||
getDoctorSchedules,
|
||||
listDoctors,
|
||||
calendarView,
|
||||
} from '@/services/appointment';
|
||||
|
||||
describe('appointment service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('listAppointments', () => {
|
||||
it('无 patientId 时传递分页参数', async () => {
|
||||
const mock = { data: [], total: 0 };
|
||||
vi.mocked(api.get).mockResolvedValueOnce(mock);
|
||||
|
||||
await listAppointments();
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith('/health/appointments', {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
});
|
||||
});
|
||||
|
||||
it('有 patientId 时附加 patient_id', async () => {
|
||||
vi.mocked(api.get).mockResolvedValueOnce({ data: [], total: 0 });
|
||||
|
||||
await listAppointments('p-123');
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith('/health/appointments', {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
patient_id: 'p-123',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAppointment', () => {
|
||||
it('调用 api.get 拼接 ID', async () => {
|
||||
const mock = { id: 'a-1', status: 'confirmed', version: 1 } as any;
|
||||
vi.mocked(api.get).mockResolvedValueOnce(mock);
|
||||
|
||||
const result = await getAppointment('a-1');
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith('/health/appointments/a-1');
|
||||
expect(result).toEqual(mock);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAppointment', () => {
|
||||
it('调用 api.post 传递预约数据', async () => {
|
||||
const input = {
|
||||
patient_id: 'p-1',
|
||||
doctor_id: 'd-1',
|
||||
appointment_date: '2026-06-01',
|
||||
start_time: '09:00',
|
||||
end_time: '09:30',
|
||||
};
|
||||
vi.mocked(api.post).mockResolvedValueOnce({ id: 'ap-1', ...input });
|
||||
|
||||
await createAppointment(input);
|
||||
|
||||
expect(api.post).toHaveBeenCalledWith('/health/appointments', input);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelAppointment', () => {
|
||||
it('调用 api.put 传递 cancelled 状态和 version', async () => {
|
||||
vi.mocked(api.put).mockResolvedValueOnce({});
|
||||
|
||||
await cancelAppointment('ap-1', 2);
|
||||
|
||||
expect(api.put).toHaveBeenCalledWith('/health/appointments/ap-1/status', {
|
||||
status: 'cancelled',
|
||||
version: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDoctorSchedules', () => {
|
||||
it('返回排班并计算 available_count', async () => {
|
||||
vi.mocked(api.get).mockResolvedValueOnce({
|
||||
data: [
|
||||
{ id: 's-1', doctor_id: 'd-1', date: '2026-06-01', start_time: '09:00', end_time: '12:00', max_appointments: 10, current_appointments: 3 },
|
||||
],
|
||||
total: 1,
|
||||
});
|
||||
|
||||
const result = await getDoctorSchedules('d-1', '2026-06-01', '2026-06-07');
|
||||
|
||||
expect(result.data[0].available_count).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listDoctors', () => {
|
||||
it('无 department 时获取全部', async () => {
|
||||
vi.mocked(api.get).mockResolvedValueOnce({ data: [], total: 0 });
|
||||
|
||||
await listDoctors();
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith('/health/doctors', { page_size: 100 });
|
||||
});
|
||||
|
||||
it('有 department 时过滤', async () => {
|
||||
vi.mocked(api.get).mockResolvedValueOnce({ data: [], total: 0 });
|
||||
|
||||
await listDoctors('内科');
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith('/health/doctors', {
|
||||
page_size: 100,
|
||||
department: '内科',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('calendarView', () => {
|
||||
it('展平嵌套结构并计算 available_count', async () => {
|
||||
vi.mocked(api.get).mockResolvedValueOnce([
|
||||
{
|
||||
date: '2026-06-01',
|
||||
schedules: [
|
||||
{ id: 's-1', doctor_id: 'd-1', start_time: '09:00', end_time: '12:00', max_appointments: 5, current_appointments: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2026-06-02',
|
||||
schedules: [
|
||||
{ id: 's-2', doctor_id: 'd-1', start_time: '14:00', end_time: '17:00', max_appointments: 8, current_appointments: 1 },
|
||||
],
|
||||
},
|
||||
] as any);
|
||||
|
||||
const result = await calendarView('2026-06-01', '2026-06-07');
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].available_count).toBe(3);
|
||||
expect(result[1].available_count).toBe(7);
|
||||
expect(result[0].date).toBe('2026-06-01');
|
||||
});
|
||||
});
|
||||
});
|
||||
56
apps/miniprogram/__tests__/services/patient.test.ts
Normal file
56
apps/miniprogram/__tests__/services/patient.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
// mock-api.ts 的顶层 vi.mock 自动生效,无需显式调用
|
||||
import '../helpers/mock-api';
|
||||
|
||||
import { api } from '@/services/request';
|
||||
import { listPatients, createPatient, updatePatient } from '@/services/patient';
|
||||
|
||||
describe('patient service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('listPatients', () => {
|
||||
it('调用 api.get 并传递分页参数', async () => {
|
||||
const mockData = { data: [{ id: '1', name: '张三', version: 1 }], total: 1 };
|
||||
vi.mocked(api.get).mockResolvedValueOnce(mockData);
|
||||
|
||||
const result = await listPatients();
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith('/health/patients', {
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
});
|
||||
expect(result).toEqual(mockData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPatient', () => {
|
||||
it('调用 api.post 传递患者数据', async () => {
|
||||
const input = { name: '李四', gender: 'male', birth_date: '1990-01-01' };
|
||||
const mockPatient = { id: '2', name: '李四', version: 1 };
|
||||
vi.mocked(api.post).mockResolvedValueOnce(mockPatient);
|
||||
|
||||
const result = await createPatient(input);
|
||||
|
||||
expect(api.post).toHaveBeenCalledWith('/health/patients', input);
|
||||
expect(result).toEqual(mockPatient);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePatient', () => {
|
||||
it('调用 api.put 传递更新数据和 version', async () => {
|
||||
const input = { name: '王五' };
|
||||
const mockPatient = { id: '1', name: '王五', version: 2 };
|
||||
vi.mocked(api.put).mockResolvedValueOnce(mockPatient);
|
||||
|
||||
const result = await updatePatient('1', input, 1);
|
||||
|
||||
expect(api.put).toHaveBeenCalledWith('/health/patients/1', {
|
||||
...input,
|
||||
version: 1,
|
||||
});
|
||||
expect(result).toEqual(mockPatient);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user