Files
everything-is-suitable/docs/plans/2026-03-20-test-coverage-improvement-plan.md
T
张翔 08ea5fbe98 feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
2026-03-28 14:37:29 +08:00

35 KiB

测试覆盖率提升实施计划

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: 修复现有测试失败并提升测试覆盖率至生产级别标准

Architecture: 采用渐进式修复策略,优先修复高优先级问题,逐步提升测试覆盖率。使用TDD方法论,确保每个修复都有测试验证。

Tech Stack:

  • 后端: Spring Boot 3.4.1 + JUnit 5 + Mockito + JaCoCo
  • 前端: Vue 3 + Vitest + Playwright
  • 测试工具: pytest (API测试)

执行策略

优先级排序

P0 - 立即执行 (1-3天):

  1. 修复Admin端E2E测试URL配置错误
  2. 修复Admin端E2E测试元素选择器
  3. 修复前端单元测试Mock配置
  4. 实现日期工具缺失函数

P1 - 短期执行 (3-7天): 5. 补充后端Service层单元测试 6. 修复Python E2E测试框架

P2 - 中期执行 (1-2周): 7. 建立UAT测试体系 8. 建立性能测试体系


阶段一:修复Admin端E2E测试 (P0)

Task 1: 修复Dashboard URL配置错误

问题: 导航到无效URL http://localhost:5174undefined

Files:

  • Modify: everything-is-suitable-admin/e2e/pages/dashboard-page.ts
  • Test: everything-is-suitable-admin/e2e/dashboard.spec.ts

Step 1: 检查Dashboard页面URL配置

检查文件 everything-is-suitable-admin/e2e/pages/dashboard-page.ts:

// 当前可能有问题的代码
export class DashboardPage extends BasePage {
  constructor(page: Page) {
    super(page);
    this.url = '/dashboard'; // 检查这里是否正确
  }
}

Step 2: 修复URL配置

// 修复后的代码
export class DashboardPage extends BasePage {
  constructor(page: Page) {
    super(page);
    this.url = '/dashboard'; // 确保不以 / 开头时添加 baseURL
  }

  async navigate() {
    await this.page.goto(this.url); // 使用相对路径
    await this.waitForLoad();
  }
}

Step 3: 验证修复

运行测试:

cd everything-is-suitable-admin
npx playwright test e2e/dashboard.spec.ts -v

预期: Dashboard页面加载测试通过

Step 4: 提交修复

git add e2e/pages/dashboard-page.ts
git commit -m "fix(e2e): 修复Dashboard页面URL配置错误"

Task 2: 修复用户管理页面元素选择器

问题: 用户列表表格元素选择器问题

Files:

  • Modify: everything-is-suitable-admin/e2e/pages/user-management-page.ts
  • Test: everything-is-suitable-admin/e2e/user.spec.ts

Step 1: 检查用户管理页面实际DOM结构

在浏览器中打开用户管理页面,使用开发者工具检查元素:

<!-- 实际DOM结构示例 -->
<a-table 
  class="ant-table"
  data-testid="user-table"
>
  <tbody class="ant-table-tbody">
    <tr class="ant-table-row">
      <td>username</td>
    </tr>
  </tbody>
</a-table>

Step 2: 更新元素选择器

修改 everything-is-suitable-admin/e2e/pages/user-management-page.ts:

export class UserManagementPage extends BasePage {
  // 使用更稳定的选择器
  private userTable = this.page.locator('[data-testid="user-table"]');
  private createUserButton = this.page.locator('button:has-text("创建用户")');
  private searchInput = this.page.locator('input[placeholder*="搜索"]');
  
  async waitForTableLoad() {
    await this.userTable.waitFor({ state: 'visible' });
    await this.page.waitForLoadState('networkidle');
  }

  async getUserCount(): Promise<number> {
    await this.waitForTableLoad();
    return await this.userTable.locator('tbody tr').count();
  }
}

Step 3: 为元素添加data-testid属性

修改 everything-is-suitable-admin/src/views/UserManagement.vue:

<template>
  <div class="user-management">
    <a-table
      :data-source="users"
      data-testid="user-table"
      :columns="columns"
    >
      <!-- 表格内容 -->
    </a-table>
    
    <a-button
      type="primary"
      data-testid="create-user-button"
      @click="showCreateModal"
    >
      创建用户
    </a-button>
    
    <a-input
      v-model:value="searchText"
      data-testid="search-input"
      placeholder="搜索用户名或邮箱"
    />
  </div>
</template>

Step 4: 运行测试验证

cd everything-is-suitable-admin
npx playwright test e2e/user.spec.ts -v

预期: 用户管理相关测试通过

Step 5: 提交修复

git add e2e/pages/user-management-page.ts src/views/UserManagement.vue
git commit -m "fix(e2e): 修复用户管理页面元素选择器并添加data-testid"

Task 3: 修复认证模块元素选择器

问题: 登录/登出相关元素选择器错误

Files:

  • Modify: everything-is-suitable-admin/e2e/pages/login-page.ts
  • Test: everything-is-suitable-admin/e2e/auth.spec.ts

Step 1: 检查登录页面DOM结构

<!-- Login.vue 实际结构 -->
<a-form class="login-form">
  <a-form-item>
    <a-input v-model:value="form.username" data-testid="username-input" />
  </a-form-item>
  <a-form-item>
    <a-input-password v-model:value="form.password" data-testid="password-input" />
  </a-form-item>
  <a-button type="primary" html-type="submit" data-testid="login-button">
    登录
  </a-button>
</a-form>

Step 2: 更新登录页面选择器

修改 everything-is-suitable-admin/e2e/pages/login-page.ts:

export class LoginPage extends BasePage {
  private usernameInput = this.page.locator('[data-testid="username-input"]');
  private passwordInput = this.page.locator('[data-testid="password-input"]');
  private loginButton = this.page.locator('[data-testid="login-button"]');
  private errorMessage = this.page.locator('.ant-message-error');

  async login(username: string, password: string) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }

  async expectLoginSuccess() {
    await this.page.waitForURL('**/dashboard');
  }

  async expectLoginError(message: string) {
    await this.errorMessage.waitFor({ state: 'visible' });
    await expect(this.errorMessage).toContainText(message);
  }
}

Step 3: 为登录页面添加data-testid

修改 everything-is-suitable-admin/src/views/Login.vue:

<template>
  <div class="login-container">
    <a-form class="login-form" @finish="handleLogin">
      <a-form-item name="username">
        <a-input
          v-model:value="form.username"
          data-testid="username-input"
          placeholder="用户名"
        />
      </a-form-item>
      
      <a-form-item name="password">
        <a-input-password
          v-model:value="form.password"
          data-testid="password-input"
          placeholder="密码"
        />
      </a-form-item>
      
      <a-button
        type="primary"
        html-type="submit"
        data-testid="login-button"
        :loading="loading"
      >
        登录
      </a-button>
    </a-form>
  </div>
</template>

Step 4: 运行测试验证

cd everything-is-suitable-admin
npx playwright test e2e/auth.spec.ts -v

预期: 认证相关测试通过

Step 5: 提交修复

git add e2e/pages/login-page.ts src/views/Login.vue
git commit -m "fix(e2e): 修复认证模块元素选择器并添加data-testid"

Task 4: 修复前端单元测试Mock配置

问题: API和Service测试Mock配置不正确

Files:

  • Modify: everything-is-suitable-admin/src/test/setup.ts
  • Modify: everything-is-suitable-admin/vitest.config.ts
  • Test: everything-is-suitable-admin/src/test/auth.service.test.ts

Step 1: 检查当前Mock配置

查看 everything-is-suitable-admin/src/test/setup.ts:

// 当前可能的问题
import { vi } from 'vitest';

// Mock可能不完整

Step 2: 创建完整的Mock配置

修改 everything-is-suitable-admin/src/test/setup.ts:

import { vi } from 'vitest';
import { config } from '@vue/test-utils';

// Mock localStorage
const localStorageMock = {
  getItem: vi.fn(),
  setItem: vi.fn(),
  removeItem: vi.fn(),
  clear: vi.fn(),
};
global.localStorage = localStorageMock as any;

// Mock sessionStorage
global.sessionStorage = localStorageMock as any;

// Mock axios
vi.mock('axios', () => ({
  default: {
    create: vi.fn(() => ({
      get: vi.fn(),
      post: vi.fn(),
      put: vi.fn(),
      delete: vi.fn(),
      interceptors: {
        request: { use: vi.fn() },
        response: { use: vi.fn() },
      },
    })),
  },
}));

// Mock router
vi.mock('vue-router', () => ({
  createRouter: vi.fn(),
  createWebHistory: vi.fn(),
  useRoute: vi.fn(() => ({
    params: {},
    query: {},
    path: '/',
  })),
  useRouter: vi.fn(() => ({
    push: vi.fn(),
    replace: vi.fn(),
    go: vi.fn(),
  })),
}));

// 全局测试配置
config.global.stubs = {
  RouterLink: true,
  RouterView: true,
};

Step 3: 更新vitest配置

修改 everything-is-suitable-admin/vitest.config.ts:

import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  },
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    exclude: [
      'node_modules/',
      'dist/',
      'e2e/',
      '**/*.config.ts',
      '**/*.config.js'
    ],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'src/test/',
        '**/*.d.ts',
        '**/*.config.*',
        '**/mockData',
        'e2e/'
      ]
    },
    // 添加Mock相关配置
    deps: {
      inline: ['ant-design-vue'],
    },
  }
});

Step 4: 修复auth.service.test.ts

修改 everything-is-suitable-admin/src/test/auth.service.test.ts:

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { authService } from '@/services/auth.service';
import request from '@/utils/request';

// Mock request
vi.mock('@/utils/request', () => ({
  default: {
    post: vi.fn(),
    get: vi.fn(),
  },
}));

describe('AuthService', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should login successfully', async () => {
    const mockResponse = {
      token: 'test-token',
      user: {
        id: 1,
        username: 'admin',
        email: 'admin@example.com',
      },
    };

    vi.mocked(request.post).mockResolvedValue(mockResponse);

    const result = await authService.login({
      username: 'admin',
      password: 'admin123',
    });

    expect(request.post).toHaveBeenCalledWith('/sys/auth/login', {
      username: 'admin',
      password: 'admin123',
    });
    expect(result).toEqual(mockResponse);
  });

  it('should logout successfully', async () => {
    vi.mocked(request.post).mockResolvedValue({});

    await authService.logout();

    expect(request.post).toHaveBeenCalledWith('/sys/auth/logout');
  });

  it('should refresh token successfully', async () => {
    const mockResponse = {
      token: 'new-token',
      user: {
        id: 1,
        username: 'admin',
      },
    };

    vi.mocked(request.post).mockResolvedValue(mockResponse);

    const result = await authService.refreshToken('old-token');

    expect(request.post).toHaveBeenCalledWith('/sys/auth/refresh/old-token');
    expect(result).toEqual(mockResponse);
  });
});

Step 5: 运行测试验证

cd everything-is-suitable-admin
npm run test src/test/auth.service.test.ts

预期: auth.service测试全部通过

Step 6: 提交修复

git add src/test/setup.ts vitest.config.ts src/test/auth.service.test.ts
git commit -m "fix(test): 修复前端单元测试Mock配置"

Task 5: 实现日期工具缺失函数

问题: 日期工具测试失败,缺少函数实现

Files:

  • Modify: everything-is-suitable-admin/src/utils/date.ts
  • Test: everything-is-suitable-admin/src/test/date.test.ts

Step 1: 检查测试用例

查看 everything-is-suitable-admin/src/test/date.test.ts:

// 缺失的函数测试
it('should check if year is leap year', () => {
  expect(isLeapYear(2024)).toBe(true);
  expect(isLeapYear(2023)).toBe(false);
});

it('should get days in month', () => {
  expect(getDaysInMonth(2024, 2)).toBe(29);
  expect(getDaysInMonth(2023, 2)).toBe(28);
});

it('should get week number', () => {
  expect(getWeekNumber(new Date('2024-01-01'))).toBe(1);
});

it('should calculate age', () => {
  expect(getAge(new Date('1990-01-01'))).toBe(34);
});

it('should format duration', () => {
  expect(formatDuration(3661)).toBe('1小时1分钟1秒');
});

it('should parse duration', () => {
  expect(parseDuration('1小时30分钟')).toBe(5400);
});

Step 2: 实现缺失的函数

修改 everything-is-suitable-admin/src/utils/date.ts:

import dayjs from 'dayjs';

/**
 * 检查是否为闰年
 */
export function isLeapYear(year: number): boolean {
  return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}

/**
 * 获取指定月份的天数
 */
export function getDaysInMonth(year: number, month: number): number {
  return new Date(year, month, 0).getDate();
}

/**
 * 获取日期所在的周数
 */
export function getWeekNumber(date: Date): number {
  const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  const dayNum = d.getUTCDay() || 7;
  d.setUTCDate(d.getUTCDate() + 4 - dayNum);
  const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
  return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
}

/**
 * 计算年龄
 */
export function getAge(birthDate: Date): number {
  const today = new Date();
  let age = today.getFullYear() - birthDate.getFullYear();
  const monthDiff = today.getMonth() - birthDate.getMonth();
  
  if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
    age--;
  }
  
  return age;
}

/**
 * 格式化时长(秒转换为可读格式)
 */
export function formatDuration(seconds: number): string {
  const hours = Math.floor(seconds / 3600);
  const minutes = Math.floor((seconds % 3600) / 60);
  const secs = seconds % 60;
  
  const parts: string[] = [];
  if (hours > 0) parts.push(`${hours}小时`);
  if (minutes > 0) parts.push(`${minutes}分钟`);
  if (secs > 0 || parts.length === 0) parts.push(`${secs}秒`);
  
  return parts.join('');
}

/**
 * 解析时长字符串为秒数
 */
export function parseDuration(durationStr: string): number {
  let totalSeconds = 0;
  
  const hourMatch = durationStr.match(/(\d+)\s*小时/);
  const minuteMatch = durationStr.match(/(\d+)\s*分钟/);
  const secondMatch = durationStr.match(/(\d+)\s*秒/);
  
  if (hourMatch) totalSeconds += parseInt(hourMatch[1]) * 3600;
  if (minuteMatch) totalSeconds += parseInt(minuteMatch[1]) * 60;
  if (secondMatch) totalSeconds += parseInt(secondMatch[1]);
  
  return totalSeconds;
}

Step 3: 运行测试验证

cd everything-is-suitable-admin
npm run test src/test/date.test.ts

预期: 日期工具测试全部通过

Step 4: 提交实现

git add src/utils/date.ts
git commit -m "feat(utils): 实现日期工具缺失函数"

阶段二:补充后端Service层单元测试 (P1)

Task 6: 创建AlmanacService单元测试

问题: Service层测试覆盖率0%

Files:

  • Create: everything-is-suitable-api/everything-is-suitable-biz/src/test/java/io/destiny/biz/service/impl/AlmanacServiceImplTest.java
  • Test: 运行JUnit测试

Step 1: 创建测试类

创建文件 everything-is-suitable-api/everything-is-suitable-biz/src/test/java/io/destiny/biz/service/impl/AlmanacServiceImplTest.java:

package io.destiny.biz.service.impl;

import io.destiny.biz.dto.AlmanacDTO;
import io.destiny.biz.exception.AlmanacException;
import io.destiny.biz.repository.AlmanacRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import java.time.LocalDate;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class AlmanacServiceImplTest {

    @Mock
    private AlmanacRepository almanacRepository;

    @InjectMocks
    private AlmanacServiceImpl almanacService;

    private AlmanacDTO testAlmanac;

    @BeforeEach
    void setUp() {
        testAlmanac = AlmanacDTO.builder()
            .date(LocalDate.now())
            .lunarDate("正月初一")
            .suit("祭祀 祈福")
            .avoid("开仓 动土")
            .build();
    }

    @Test
    void getAlmanacByDate_shouldReturnAlmanac() {
        // Given
        LocalDate date = LocalDate.now();
        when(almanacRepository.findByDate(date))
            .thenReturn(Mono.just(testAlmanac));

        // When
        Mono<AlmanacDTO> result = almanacService.getAlmanacByDate(date);

        // Then
        StepVerifier.create(result)
            .expectNext(testAlmanac)
            .verifyComplete();

        verify(almanacRepository).findByDate(date);
    }

    @Test
    void getAlmanacByDate_shouldThrowExceptionWhenNotFound() {
        // Given
        LocalDate date = LocalDate.now();
        when(almanacRepository.findByDate(date))
            .thenReturn(Mono.empty());

        // When & Then
        StepVerifier.create(almanacService.getAlmanacByDate(date))
            .expectError(AlmanacException.class)
            .verify();

        verify(almanacRepository).findByDate(date);
    }

    @Test
    void getAlmanacByDateRange_shouldReturnAlmanacList() {
        // Given
        LocalDate startDate = LocalDate.now();
        LocalDate endDate = startDate.plusDays(7);
        
        when(almanacRepository.findByDateBetween(startDate, endDate))
            .thenReturn(Mono.just(testAlmanac));

        // When
        Mono<AlmanacDTO> result = almanacService.getAlmanacByDateRange(startDate, endDate);

        // Then
        StepVerifier.create(result)
            .expectNext(testAlmanac)
            .verifyComplete();

        verify(almanacRepository).findByDateBetween(startDate, endDate);
    }
}

Step 2: 运行测试验证

cd everything-is-suitable-api/everything-is-suitable-biz
mvn test -Dtest=AlmanacServiceImplTest

预期: 测试通过

Step 3: 提交测试

git add src/test/java/io/destiny/biz/service/impl/AlmanacServiceImplTest.java
git commit -m "test(service): 添加AlmanacService单元测试"

Task 7: 创建FortuneAnalysisService单元测试

Files:

  • Create: everything-is-suitable-api/everything-is-suitable-biz/src/test/java/io/destiny/biz/service/impl/FortuneAnalysisServiceImplTest.java

Step 1: 创建测试类

package io.destiny.biz.service.impl;

import io.destiny.biz.dto.FortuneDTO;
import io.destiny.biz.enums.FortuneType;
import io.destiny.biz.repository.FortuneRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import java.time.LocalDate;

import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class FortuneAnalysisServiceImplTest {

    @Mock
    private FortuneRepository fortuneRepository;

    @InjectMocks
    private FortuneAnalysisServiceImpl fortuneService;

    private FortuneDTO testFortune;

    @BeforeEach
    void setUp() {
        testFortune = FortuneDTO.builder()
            .date(LocalDate.now())
            .fortuneType(FortuneType.DAILY)
            .overallScore(85)
            .careerScore(90)
            .wealthScore(80)
            .loveScore(85)
            .healthScore(75)
            .build();
    }

    @Test
    void getDailyFortune_shouldReturnFortune() {
        // Given
        LocalDate date = LocalDate.now();
        when(fortuneRepository.findByDateAndType(date, FortuneType.DAILY))
            .thenReturn(Mono.just(testFortune));

        // When
        Mono<FortuneDTO> result = fortuneService.getDailyFortune(date);

        // Then
        StepVerifier.create(result)
            .expectNext(testFortune)
            .verifyComplete();

        verify(fortuneRepository).findByDateAndType(date, FortuneType.DAILY);
    }

    @Test
    void getMonthlyFortune_shouldReturnFortune() {
        // Given
        int year = 2024;
        int month = 3;
        when(fortuneRepository.findByYearAndMonthAndType(year, month, FortuneType.MONTHLY))
            .thenReturn(Mono.just(testFortune));

        // When
        Mono<FortuneDTO> result = fortuneService.getMonthlyFortune(year, month);

        // Then
        StepVerifier.create(result)
            .expectNext(testFortune)
            .verifyComplete();

        verify(fortuneRepository).findByYearAndMonthAndType(year, month, FortuneType.MONTHLY);
    }

    @Test
    void getYearlyFortune_shouldReturnFortune() {
        // Given
        int year = 2024;
        when(fortuneRepository.findByYearAndType(year, FortuneType.YEARLY))
            .thenReturn(Mono.just(testFortune));

        // When
        Mono<FortuneDTO> result = fortuneService.getYearlyFortune(year);

        // Then
        StepVerifier.create(result)
            .expectNext(testFortune)
            .verifyComplete();

        verify(fortuneRepository).findByYearAndType(year, FortuneType.YEARLY);
    }
}

Step 2: 运行测试验证

cd everything-is-suitable-api/everything-is-suitable-biz
mvn test -Dtest=FortuneAnalysisServiceImplTest

预期: 测试通过

Step 3: 提交测试

git add src/test/java/io/destiny/biz/service/impl/FortuneAnalysisServiceImplTest.java
git commit -m "test(service): 添加FortuneAnalysisService单元测试"

Task 8: 创建ZiweiChartService单元测试

Files:

  • Create: everything-is-suitable-api/everything-is-suitable-biz/src/test/java/io/destiny/biz/service/impl/ZiweiChartServiceImplTest.java

Step 1: 创建测试类

package io.destiny.biz.service.impl;

import io.destiny.biz.dto.ZiweiChartDTO;
import io.destiny.biz.util.ZiweiAlgorithmUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.LocalDateTime;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class ZiweiChartServiceImplTest {

    @InjectMocks
    private ZiweiChartServiceImpl ziweiService;

    private ZiweiChartDTO testChart;

    @BeforeEach
    void setUp() {
        testChart = ZiweiChartDTO.builder()
            .birthTime(LocalDateTime.of(1990, 1, 1, 12, 0))
            .mingGong("命宫")
            .ziweiStar("紫微星")
            .build();
    }

    @Test
    void calculateChart_shouldReturnCorrectChart() {
        // Given
        LocalDateTime birthTime = LocalDateTime.of(1990, 1, 1, 12, 0);
        String gender = "男";

        try (MockedStatic<ZiweiAlgorithmUtil> mockedUtil = mockStatic(ZiweiAlgorithmUtil.class)) {
            mockedUtil.when(() -> ZiweiAlgorithmUtil.calculateMingGong(any()))
                .thenReturn("命宫");
            mockedUtil.when(() -> ZiweiAlgorithmUtil.calculateZiweiStar(any()))
                .thenReturn("紫微星");

            // When
            ZiweiChartDTO result = ziweiService.calculateChart(birthTime, gender);

            // Then
            assertNotNull(result);
            assertEquals("命宫", result.getMingGong());
            assertEquals("紫微星", result.getZiweiStar());
        }
    }

    @Test
    void calculateChart_shouldThrowExceptionForInvalidInput() {
        // Given
        LocalDateTime birthTime = null;
        String gender = "男";

        // When & Then
        assertThrows(IllegalArgumentException.class, () -> {
            ziweiService.calculateChart(birthTime, gender);
        });
    }
}

Step 2: 运行测试验证

cd everything-is-suitable-api/everything-is-suitable-biz
mvn test -Dtest=ZiweiChartServiceImplTest

预期: 测试通过

Step 3: 提交测试

git add src/test/java/io/destiny/biz/service/impl/ZiweiChartServiceImplTest.java
git commit -m "test(service): 添加ZiweiChartService单元测试"

阶段三:建立UAT测试体系 (P2)

Task 9: 创建UAT测试框架

Files:

  • Create: everything-is-suitable-test/uat/README.md
  • Create: everything-is-suitable-test/uat/test-scenarios.md
  • Create: everything-is-suitable-test/uat/uat-checklist.md

Step 1: 创建UAT测试场景文档

创建文件 everything-is-suitable-test/uat/test-scenarios.md:

# UAT测试场景

## 场景1: 用户注册-登录-查看个人信息

**前置条件**:
- 系统已启动
- 数据库已初始化

**测试步骤**:
1. 打开系统登录页面
2. 点击"注册"按钮
3. 填写注册表单:
   - 用户名: testuser001
   - 密码: Test@123
   - 邮箱: test@example.com
   - 手机: 13800138000
4. 提交注册
5. 验证注册成功提示
6. 使用新账号登录
7. 进入个人信息页面
8. 验证个人信息显示正确

**预期结果**:
- ✅ 注册成功
- ✅ 登录成功
- ✅ 个人信息显示正确

---

## 场景2: 管理员创建用户-分配角色-验证权限

**前置条件**:
- 管理员账号已登录

**测试步骤**:
1. 进入用户管理页面
2. 点击"创建用户"按钮
3. 填写用户信息:
   - 用户名: newuser001
   - 密码: User@123
   - 邮箱: newuser@example.com
4. 提交创建
5. 进入角色管理页面
6. 创建新角色"测试角色"
7. 为角色分配权限: 用户查看、角色查看
8. 将角色分配给新用户
9. 使用新用户登录
10. 验证权限:
    - ✅ 可访问用户管理页面
    - ✅ 可查看用户列表
    - ❌ 不可创建用户
    - ❌ 不可删除用户

**预期结果**:
- ✅ 用户创建成功
- ✅ 角色创建成功
- ✅ 权限分配正确
- ✅ 权限验证通过

---

## 场景3: 黄历查询-运势分析-结果展示

**前置条件**:
- 用户已登录

**测试步骤**:
1. 进入黄历查询页面
2. 选择日期: 2024年3月20日
3. 点击查询
4. 验证黄历信息显示:
   - ✅ 农历日期
   - ✅ 宜忌事项
   - ✅ 节气信息
5. 进入运势分析页面
6. 选择日期类型: 日运势
7. 选择日期: 2024年3月20日
8. 点击查询
9. 验证运势信息显示:
   - ✅ 综合运势评分
   - ✅ 事业运势
   - ✅ 财运
   - ✅ 爱情运势
   - ✅ 健康运势

**预期结果**:
- ✅ 黄历查询成功
- ✅ 运势分析成功
- ✅ 结果展示完整

Step 2: 创建UAT测试检查清单

创建文件 everything-is-suitable-test/uat/uat-checklist.md:

# UAT测试检查清单

## 测试环境

- [ ] 测试环境已部署
- [ ] 数据库已初始化
- [ ] 测试账号已创建
- [ ] 测试数据已准备

## 功能测试

### 认证功能
- [ ] 用户注册
- [ ] 用户登录
- [ ] 用户登出
- [ ] Token刷新
- [ ] 权限验证

### 用户管理
- [ ] 用户列表查询
- [ ] 用户创建
- [ ] 用户编辑
- [ ] 用户删除
- [ ] 用户状态切换

### 角色管理
- [ ] 角色列表查询
- [ ] 角色创建
- [ ] 角色编辑
- [ ] 角色删除
- [ ] 角色权限分配

### 菜单管理
- [ ] 菜单树查询
- [ ] 菜单创建
- [ ] 菜单编辑
- [ ] 菜单删除
- [ ] 菜单排序

### 黄历服务
- [ ] 黄历查询
- [ ] 黄历搜索
- [ ] 节气信息

### 运势服务
- [ ] 日运势查询
- [ ] 月运势查询
- [ ] 年运势查询

## 性能测试

- [ ] 页面加载时间 < 2秒
- [ ] API响应时间 < 500ms
- [ ] 并发用户数 >= 100

## 安全测试

- [ ] SQL注入测试
- [ ] XSS攻击测试
- [ ] CSRF攻击测试
- [ ] 权限绕过测试

## 兼容性测试

- [ ] Chrome浏览器
- [ ] Firefox浏览器
- [ ] Safari浏览器
- [ ] Edge浏览器

## 测试结果

- 通过: ___
- 失败: ___
- 阻塞: ___
- 通过率: ___%

Step 3: 提交UAT测试框架

git add everything-is-suitable-test/uat/
git commit -m "docs(uat): 建立UAT测试体系"

阶段四:建立性能测试体系 (P2)

Task 10: 创建性能测试脚本

Files:

  • Create: everything-is-suitable-test/performance/README.md
  • Create: everything-is-suitable-test/performance/jmeter/login-test.jmx
  • Create: everything-is-suitable-test/performance/k6/api-load-test.js

Step 1: 创建JMeter登录性能测试脚本

创建文件 everything-is-suitable-test/performance/jmeter/login-test.jmx:

<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="登录性能测试">
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments">
        <collectionProp name="Arguments.arguments">
          <elementProp name="BASE_URL" elementType="Argument">
            <stringProp name="Argument.name">BASE_URL</stringProp>
            <stringProp name="Argument.value">http://localhost:8080</stringProp>
          </elementProp>
        </collectionProp>
      </elementProp>
    </TestPlan>
    <hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="用户登录">
        <stringProp name="ThreadGroup.num_threads">100</stringProp>
        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
        <boolProp name="ThreadGroup.scheduler">true</boolProp>
        <stringProp name="ThreadGroup.duration">60</stringProp>
      </ThreadGroup>
      <hashTree>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="登录请求">
          <stringProp name="HTTPSampler.domain">${BASE_URL}</stringProp>
          <stringProp name="HTTPSampler.path">/sys/auth/login</stringProp>
          <stringProp name="HTTPSampler.method">POST</stringProp>
          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
            <collectionProp name="Arguments.arguments">
              <elementProp name="" elementType="HTTPArgument">
                <stringProp name="Argument.value">{&quot;username&quot;:&quot;admin&quot;,&quot;password&quot;:&quot;admin123&quot;}</stringProp>
              </elementProp>
            </collectionProp>
          </elementProp>
        </HTTPSamplerProxy>
        <hashTree>
          <JSONPathAssertion guiclass="JSONPathAssertionGui" testclass="JSONPathAssertion" testname="验证Token">
            <stringProp name="JSON_PATH">$.token</stringProp>
            <boolProp name="JSONVALIDATION">true</boolProp>
          </JSONPathAssertion>
          <hashTree/>
        </hashTree>
      </hashTree>
      <ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="聚合报告">
        <boolProp name="ResultCollector.error_logging">true</boolProp>
      </ResultCollector>
      <hashTree/>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

Step 2: 创建k6 API负载测试脚本

创建文件 everything-is-suitable-test/performance/k6/api-load-test.js:

import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';

const errorRate = new Rate('errors');

export const options = {
  stages: [
    { duration: '30s', target: 20 },  // 逐渐增加到20个用户
    { duration: '1m', target: 20 },   // 保持20个用户1分钟
    { duration: '30s', target: 50 },  // 增加到50个用户
    { duration: '1m', target: 50 },   // 保持50个用户1分钟
    { duration: '30s', target: 0 },   // 逐渐减少到0
  ],
  thresholds: {
    http_req_duration: ['p(99)<500'], // 99%的请求必须在500ms内完成
    errors: ['rate<0.1'],             // 错误率必须小于10%
  },
};

const BASE_URL = 'http://localhost:8080';

export default function () {
  // 登录获取Token
  const loginRes = http.post(`${BASE_URL}/sys/auth/login`, JSON.stringify({
    username: 'admin',
    password: 'admin123',
  }), {
    headers: { 'Content-Type': 'application/json' },
  });

  check(loginRes, {
    '登录成功': (r) => r.status === 200,
    '返回Token': (r) => r.json('token') !== undefined,
  });

  errorRate.add(loginRes.status !== 200);

  if (loginRes.status === 200) {
    const token = loginRes.json('token');
    
    // 查询用户列表
    const usersRes = http.get(`${BASE_URL}/sys/user`, {
      headers: { 
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
    });

    check(usersRes, {
      '用户列表查询成功': (r) => r.status === 200,
      '返回用户数据': (r) => r.json('records') !== undefined,
    });

    errorRate.add(usersRes.status !== 200);
  }

  sleep(1);
}

Step 3: 创建性能测试README

创建文件 everything-is-suitable-test/performance/README.md:

# 性能测试

## 测试工具

- **JMeter**: 用于模拟大量用户并发访问
- **k6**: 用于API负载测试

## 测试场景

### 1. 登录性能测试

**目标**: 验证登录接口在高并发下的性能

**工具**: JMeter

**执行命令**:
```bash
jmeter -n -t jmeter/login-test.jmx -l results/login-test.jtl

性能指标:

  • 并发用户数: 100
  • 平均响应时间 < 500ms
  • 错误率 < 1%

2. API负载测试

目标: 验证API在高负载下的性能

工具: k6

执行命令:

k6 run k6/api-load-test.js

性能指标:

  • 最大并发用户数: 50
  • P99响应时间 < 500ms
  • 错误率 < 10%

性能基准

指标 目标值 说明
页面加载时间 < 2秒 首屏加载时间
API响应时间 < 500ms P99响应时间
并发用户数 >= 100 系统支持的最大并发数
吞吐量 >= 1000 TPS 系统每秒处理事务数
错误率 < 1% 请求失败率

测试报告

测试完成后,查看以下报告:

  • JMeter: results/login-test.jtl
  • k6: 控制台输出

**Step 4: 提交性能测试框架**

```bash
git add everything-is-suitable-test/performance/
git commit -m "feat(performance): 建立性能测试体系"

执行总结

完成所有任务后,执行以下验证:

验证1: 运行所有E2E测试

cd everything-is-suitable-admin
npx playwright test

预期通过率: 85%+

验证2: 运行所有前端单元测试

cd everything-is-suitable-admin
npm run test

预期通过率: 90%+

验证3: 运行所有后端单元测试

cd everything-is-suitable-api
mvn test

预期覆盖率: 75%+

验证4: 生成覆盖率报告

cd everything-is-suitable-api
mvn jacoco:report

查看报告: everything-is-suitable-biz/target/site/jacoco/index.html


预期成果

完成本计划后,预期达到以下目标:

指标 当前值 目标值 提升
后端指令覆盖率 58% 75% +17%
后端Service层覆盖率 0% 80% +80%
前端单元测试通过率 55.5% 90% +34.5%
E2E测试通过率 45.1% 85% +39.9%
UAT测试体系 建立
性能测试体系 建立