测试是保证软件质量的重要手段,Tagtag Starter 项目采用多层次的测试策略,包括单元测试、集成测试和 API 测试。本文档将详细介绍 Tagtag Starter 项目的测试方法、工具和最佳实践。
Tagtag Starter 项目的测试分为以下几个层次:
| 测试类型 | 测试对象 | 测试范围 | 执行速度 | 依赖关系 |
|---|---|---|---|---|
| 单元测试 | 单个类/函数 | 最小单位 | 快 | 低 |
| 集成测试 | 多个模块 | 模块间交互 | 中 | 中 |
| API 测试 | REST API | 接口级 | 中 | 高 |
| E2E 测试 | 整个应用 | 端到端流程 | 慢 | 高 |
Tagtag Starter 后端使用 Java 语言开发,采用 JUnit 5 和 Mockito 进行测试。
单元测试是对单个类或函数的测试,验证其在隔离环境下的行为。
单元测试文件通常与被测试类放在同一包下,位于 src/test/java 目录中。
src
└── main
└── java
└── dev
└── tagtag
└── module
└── system
└── service
└── impl
└── PostServiceImpl.java
└── test
└── java
└── dev
└── tagtag
└── module
└── system
└── service
└── impl
└── PostServiceImplTest.java
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
class PostServiceImplTest {
@Mock
private PostMapper postMapper;
@InjectMocks
private PostServiceImpl postService;
@Test
void testPage() {
// 准备测试数据
PostQuery query = new PostQuery();
query.setPage(1);
query.setSize(10);
query.setTitle("测试");
// 模拟分页结果
Page<PostEntity> page = new Page<>(1, 10);
List<PostEntity> records = new ArrayList<>();
records.add(new PostEntity(1L, "测试文章", "内容", 1));
page.setRecords(records);
page.setTotal(1);
// 设置 Mock 行为
when(postMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(page);
// 执行测试
PageResult<PostEntity> result = postService.page(query);
// 验证结果
assertThat(result).isNotNull();
assertThat(result.getTotal()).isEqualTo(1);
assertThat(result.getRecords()).hasSize(1);
assertThat(result.getRecords().get(0).getTitle()).isEqualTo("测试文章");
}
}
集成测试是对多个模块的测试,验证模块间的交互是否正确。
集成测试文件通常与单元测试放在同一目录中,但需要使用 @SpringBootTest 注解。
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Transactional
class PostServiceIntegrationTest {
@Autowired
private PostService postService;
@Autowired
private PostMapper postMapper;
@Test
void testSaveAndGetPost() {
// 准备测试数据
PostDTO postDTO = new PostDTO();
postDTO.setTitle("集成测试文章");
postDTO.setContent("集成测试内容");
postDTO.setStatus(1);
// 保存文章
postService.save(postDTO);
// 验证文章已保存
List<PostEntity> posts = postMapper.selectList(null);
assertThat(posts).hasSize(1);
assertThat(posts.get(0).getTitle()).isEqualTo("集成测试文章");
// 分页查询文章
PostQuery query = new PostQuery();
query.setPage(1);
query.setSize(10);
query.setTitle("集成测试");
PageResult<PostEntity> result = postService.page(query);
assertThat(result).isNotNull();
assertThat(result.getTotal()).isEqualTo(1);
assertThat(result.getRecords()).hasSize(1);
assertThat(result.getRecords().get(0).getTitle()).isEqualTo("集成测试文章");
}
}
@Transactional 注解,确保测试数据不会污染数据库API 测试是对 REST API 的测试,验证接口的正确性和可靠性。
TestRestTemplate 和 WebTestClient 用于 API 测试@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class PostControllerApiTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void testPagePosts() {
// 准备测试数据
PostDTO postDTO = new PostDTO();
postDTO.setTitle("API 测试文章");
postDTO.setContent("API 测试内容");
postDTO.setStatus(1);
// 保存文章
restTemplate.postForEntity("/posts", postDTO, Void.class);
// 测试分页查询
ResponseEntity<Result<PageResult<PostDTO>>> response = restTemplate.getForEntity(
"/posts?page=1&size=10&title=API测试",
new ParameterizedTypeReference<Result<PageResult<PostDTO>>>() {}
);
// 验证结果
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
Result<PageResult<PostDTO>> result = response.getBody();
assertThat(result).isNotNull();
assertThat(result.isSuccess()).isTrue();
assertThat(result.getData().getTotal()).isEqualTo(1);
assertThat(result.getData().getRecords()).hasSize(1);
assertThat(result.getData().getRecords().get(0).getTitle()).isEqualTo("API 测试文章");
}
}
Tagtag Starter 前端使用 TypeScript 语言开发,采用 Vitest 和 Cypress 进行测试。
前端单元测试是对单个组件或函数的测试,验证其在隔离环境下的行为。
前端单元测试文件通常与被测试组件放在同一目录中,使用 .test.ts 或 .spec.ts 后缀。
src
└── views
└── modules
└── system
└── post
├── index.vue
├── FormModal.vue
└── __tests__
├── index.test.ts
└── FormModal.test.ts
import { describe, it, expect, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import PostIndex from '../index.vue';
import { useTable } from '@vben/plugins/vxe-table';
// 模拟依赖
vi.mock('@vben/plugins/vxe-table', () => ({
useTable: vi.fn(() => [
// 模拟 Grid 组件
{ template: '<div></div>' },
// 模拟 gridApi
{
refresh: vi.fn(),
getSelectedRows: vi.fn(() => [])
}
])
}));
describe('PostIndex Component', () => {
it('should render correctly', () => {
const wrapper = mount(PostIndex);
expect(wrapper.exists()).toBe(true);
});
it('should open modal when add button is clicked', async () => {
const wrapper = mount(PostIndex, {
global: {
stubs: {
Grid: { template: '<div @toolbar-click="$emit(\'toolbar-click\', $event)"></div>' },
FormModal: { template: '<div></div>' }
}
}
});
// 触发工具栏点击事件
await wrapper.findComponent({ name: 'Grid' }).vm.$emit('toolbar-click', { code: 'add' });
// 验证模态框已打开
expect(wrapper.vm.modalVisible).toBe(true);
expect(wrapper.vm.modalTitle).toBe('新增文章');
});
});
前端集成测试是对多个组件的测试,验证组件间的交互是否正确。
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount } from '@vue/test-utils';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import PostIndex from '../index.vue';
import { useTable } from '@vben/plugins/vxe-table';
// 模拟依赖
vi.mock('@vben/plugins/vxe-table', () => ({
useTable: vi.fn(() => [
{ template: '<div @toolbar-click="$emit(\'toolbar-click\', $event)"></div>' },
{
refresh: vi.fn(),
getSelectedRows: vi.fn(() => [])
}
])
}));
// 设置 MSW 服务器
const server = setupServer(
rest.get('/api/posts', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
success: true,
data: {
records: [
{ id: 1, title: '测试文章', content: '测试内容', status: 1, createTime: new Date().toISOString() }
],
total: 1
}
})
);
}),
rest.post('/api/posts', (req, res, ctx) => {
return res(ctx.status(200), ctx.json({ success: true }));
})
);
// 启动服务器
beforeAll(() => server.listen());
// 关闭服务器
afterAll(() => server.close());
// 重置请求处理器
afterEach(() => server.resetHandlers());
describe('PostIndex Integration Test', () => {
it('should load posts correctly', async () => {
const wrapper = mount(PostIndex, {
global: {
stubs: {
Grid: { template: '<div></div>' },
FormModal: { template: '<div></div>' }
}
}
});
// 模拟 gridApi.refresh() 触发数据加载
const [_, gridApi] = useTable();
await gridApi.refresh();
// 验证数据已加载
// 这里可以根据实际实现添加更详细的验证
expect(gridApi.refresh).toHaveBeenCalled();
});
});
E2E (End-to-End) 测试是对整个应用的测试,验证端到端的用户流程。
E2E 测试文件通常放在 cypress/e2e 目录中,使用 .cy.ts 后缀。
describe('Post Management', () => {
beforeEach(() => {
// 登录系统
cy.visit('/');
cy.get('input[name="username"]').type('admin');
cy.get('input[name="password"]').type('admin123');
cy.get('button[type="submit"]').click();
// 导航到文章管理页面
cy.visit('/system/post');
});
it('should add a new post', () => {
// 点击新增按钮
cy.get('button:contains("新增")').click();
// 填写表单
cy.get('input[placeholder="请输入文章标题"]').type('E2E 测试文章');
cy.get('textarea[placeholder="请输入文章内容"]').type('E2E 测试内容');
cy.get('button:contains("保存")').click();
// 验证文章已添加
cy.get('.ant-table-row').should('contain', 'E2E 测试文章');
});
it('should edit a post', () => {
// 点击编辑按钮
cy.get('.ant-table-row').first().find('a:contains("编辑")').click();
// 修改表单
cy.get('input[placeholder="请输入文章标题"]').clear().type('修改后的 E2E 测试文章');
cy.get('button:contains("保存")').click();
// 验证文章已修改
cy.get('.ant-table-row').should('contain', '修改后的 E2E 测试文章');
});
it('should delete a post', () => {
// 点击删除按钮
cy.get('.ant-table-row').first().find('a:contains("删除")').click();
// 确认删除
cy.get('.ant-modal-confirm-btns').find('button:contains("确定")').click();
// 验证文章已删除
cy.get('.ant-table-row').should('not.contain', '修改后的 E2E 测试文章');
});
});
安全测试是前端测试的重要组成部分,主要关注 XSS、CSRF 等安全漏洞。
XSS(跨站脚本攻击)是前端最常见的安全漏洞之一。以下是 XSS 防护的测试示例:
// 必须修复:XSS 漏洞
// 不推荐 - 直接渲染用户输入的内容
function renderUserContent(content: string) {
return `<div>${content}</div>`;
}
// 推荐 - 使用 DOMPurify 进行 HTML 净化
import DOMPurify from 'dompurify';
function renderUserContent(content: string) {
const cleanContent = DOMPurify.sanitize(content);
return `<div>${cleanContent}</div>`;
}
// 推荐 - 使用 Vue 的 v-text 指令
<template>
<div v-text="userContent"></div>
</template>
// 推荐 - 使用 Vue 的插值表达式(自动转义)
<template>
<div>{{ userContent }}</div>
</template>
// 不推荐 - 使用 v-html 渲染用户输入
<template>
<div v-html="userContent"></div>
</template>
// XSS 测试用例示例
import { describe, it, expect } from 'vitest';
import { renderUserContent } from './utils';
describe('XSS Protection', () => {
it('should sanitize malicious scripts', () => {
const maliciousContent = '<script>alert("XSS")</script>';
const rendered = renderUserContent(maliciousContent);
// 验证 script 标签被移除
expect(rendered).not.toContain('<script>');
expect(rendered).not.toContain('alert("XSS")');
});
it('should sanitize img onerror', () => {
const maliciousContent = '<img src="x" onerror="alert(1)">';
const rendered = renderUserContent(maliciousContent);
// 验证 onerror 事件被移除
expect(rendered).not.toContain('onerror');
});
it('should sanitize javascript: protocol', () => {
const maliciousContent = '<a href="javascript:alert(1)">Click me</a>';
const rendered = renderUserContent(maliciousContent);
// 验证 javascript: 协议被移除或转义
expect(rendered).not.toContain('javascript:');
});
it('should allow safe HTML', () => {
const safeContent = '<p>Safe <strong>content</strong></p>';
const rendered = renderUserContent(safeContent);
// 验证安全的 HTML 标签被保留
expect(rendered).toContain('<p>');
expect(rendered).toContain('<strong>');
});
});
CSRF(跨站请求伪造)是另一个常见的安全漏洞。以下是 CSRF 防护的测试示例:
// 必须修复:CSRF 漏洞
// 不推荐 - 没有 CSRF Token 保护
async function updateProfile(data: any) {
return axios.post('/api/profile', data);
}
// 推荐 - 使用 CSRF Token
async function updateProfile(data: any) {
const csrfToken = getCsrfToken();
return axios.post('/api/profile', data, {
headers: {
'X-CSRF-TOKEN': csrfToken
}
});
}
// 推荐 - 使用 axios 拦截器自动添加 CSRF Token
axios.interceptors.request.use(config => {
const csrfToken = getCsrfToken();
if (csrfToken) {
config.headers['X-CSRF-TOKEN'] = csrfToken;
}
return config;
});
// CSRF 测试用例示例
import { describe, it, expect, vi } from 'vitest';
import { updateProfile } from './api';
describe('CSRF Protection', () => {
it('should include CSRF token in request headers', async () => {
const mockAxios = vi.spyOn(axios, 'post').mockResolvedValue({ data: {} });
await updateProfile({ name: 'Test' });
// 验证请求头包含 CSRF Token
expect(mockAxios).toHaveBeenCalledWith(
'/api/profile',
{ name: 'Test' },
expect.objectContaining({
headers: expect.objectContaining({
'X-CSRF-TOKEN': expect.any(String)
})
})
);
});
});
敏感信息泄露是前端安全的另一个重要方面。以下是敏感信息防护的测试示例:
// 必须修复:敏感信息泄露
// 不推荐 - 在 localStorage 中存储敏感信息
function saveToken(token: string) {
localStorage.setItem('authToken', token);
}
// 推荐 - 使用 sessionStorage 或内存存储
function saveToken(token: string) {
sessionStorage.setItem('authToken', token);
}
// 推荐 - 使用 httpOnly Cookie
// 后端设置 Cookie: Set-Cookie: authToken=xxx; HttpOnly; Secure; SameSite=Strict
// 不推荐 - 在 URL 中传递敏感信息
function redirectToProfile(userId: string) {
window.location.href = `/profile?userId=${userId}`;
}
// 推荐 - 使用路由参数或状态管理
function redirectToProfile(userId: string) {
router.push({ name: 'Profile', params: { userId } });
}
// 敏感信息泄露测试用例示例
import { describe, it, expect, vi } from 'vitest';
import { saveToken } from './auth';
describe('Sensitive Information Protection', () => {
it('should not store token in localStorage', () => {
const mockLocalStorage = vi.spyOn(Storage.prototype, 'setItem');
saveToken('test-token');
// 验证没有使用 localStorage
expect(mockLocalStorage).not.toHaveBeenCalled();
});
it('should store token in sessionStorage', () => {
const mockSessionStorage = vi.spyOn(sessionStorage, 'setItem');
saveToken('test-token');
// 验证使用了 sessionStorage
expect(mockSessionStorage).toHaveBeenCalledWith('authToken', 'test-token');
});
});
性能测试是确保应用响应速度和用户体验的重要手段。
// 必须修复:性能问题 - 不必要的重新渲染
// 不推荐
<template>
<div v-for="item in items" :key="item.id">
<ExpensiveComponent :data="item" />
</div>
</template>
// 推荐 - 使用虚拟滚动
<template>
<VirtualList :items="items" :item-size="100">
<template #default="{ item }">
<ExpensiveComponent :data="item" />
</template>
</VirtualList>
</template>
// 推荐 - 使用 v-once 静态内容
<template>
<div v-once>{{ staticContent }}</div>
<div>{{ dynamicContent }}</div>
</template>
// 渲染性能测试用例示例
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import PostList from './PostList.vue';
describe('Rendering Performance', () => {
it('should render large list efficiently', async () => {
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i,
title: `Post ${i}`
}));
const wrapper = mount(PostList, {
props: { items }
});
const startTime = performance.now();
await wrapper.vm.$nextTick();
const endTime = performance.now();
// 验证渲染时间在合理范围内(例如 100ms)
expect(endTime - startTime).toBeLessThan(100);
});
});
// 必须修复:内存泄漏
// 不推荐 - 未清理的事件监听器
export default {
mounted() {
window.addEventListener('resize', this.handleResize);
}
}
// 推荐 - 在组件销毁时清理事件监听器
export default {
mounted() {
window.addEventListener('resize', this.handleResize);
},
beforeUnmount() {
window.removeEventListener('resize', this.handleResize);
}
}
// 推荐 - 使用 onUnmounted 清理
import { onMounted, onUnmounted } from 'vue';
export default {
setup() {
const handleResize = () => {
// 处理窗口大小变化
};
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
}
}
// 内存泄漏测试用例示例
import { describe, it, expect, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import MyComponent from './MyComponent.vue';
describe('Memory Leak Prevention', () => {
it('should remove event listeners on unmount', () => {
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
const wrapper = mount(MyComponent);
// 验证事件监听器已添加
expect(addEventListenerSpy).toHaveBeenCalled();
wrapper.unmount();
// 验证事件监听器已移除
expect(removeEventListenerSpy).toHaveBeenCalled();
expect(removeEventListenerSpy.mock.calls.length).toBe(addEventListenerSpy.mock.calls.length);
});
});
Tagtag Starter 项目使用 GitHub Actions 进行持续集成,自动执行测试。
name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn -B package --file pom.xml
- name: Run tests
run: mvn test
- name: Upload test results
uses: actions/upload-artifact@v3
with:
name: test-results
path: tagtag-*/target/surefire-reports/
测试是保证软件质量的重要手段,Tagtag Starter 项目采用多层次的测试策略,包括单元测试、集成测试和 API 测试。通过遵循测试最佳实践,使用合适的测试工具,我们可以提高代码质量,减少回归问题,支持持续集成,确保项目的长期稳定发展。
希望本文档能够帮助开发者了解 Tagtag Starter 项目的测试方法和最佳实践,编写高质量的测试用例,共同维护一个高质量的代码库。