Skip to content

JavaScript单元测试2024:前端开发者掌握测试驱动开发完整指南

📊 SEO元描述:2024年最新JavaScript单元测试教程,详解测试重要性、Jest测试框架、测试覆盖率分析。包含完整实战案例,适合前端开发者掌握测试驱动开发。

核心关键词:JavaScript单元测试2024、Jest测试框架、测试覆盖率、测试驱动开发、前端测试技术

长尾关键词:JavaScript单元测试怎么写、Jest怎么使用、测试覆盖率怎么提高、前端测试最佳实践、JavaScript测试工具推荐


📚 JavaScript单元测试学习目标与核心收获

通过本节JavaScript单元测试基础详解,你将系统性掌握:

  • 测试重要性理解:深入理解单元测试在软件开发中的核心价值
  • Jest测试框架精通:掌握最流行的JavaScript测试框架使用
  • 测试覆盖率分析:学会评估和提升代码测试覆盖率
  • 测试驱动开发:掌握TDD开发模式和最佳实践
  • 测试策略制定:建立完整的前端测试策略和工作流
  • 测试自动化集成:将测试集成到CI/CD开发流程

🎯 适合人群

  • JavaScript开发工程师的测试技能提升需求
  • 前端团队负责人的代码质量保证需求
  • 全栈开发者的测试驱动开发技能
  • 技术团队成员的软件质量保障能力

🌟 单元测试是什么?为什么对现代开发如此重要?

单元测试是什么?这是现代软件开发中不可或缺的质量保证手段。单元测试是对软件中最小可测试单元进行检查和验证,也是软件质量保证体系的重要组成部分。

单元测试的核心价值

  • 🎯 错误预防:在开发阶段就发现和修复bug,降低修复成本
  • 🔧 重构保障:为代码重构提供安全网,确保功能不被破坏
  • 💡 文档作用:测试用例是最好的代码使用文档
  • 📚 设计改进:编写测试促使开发者思考更好的代码设计
  • 🚀 开发效率:长期来看显著提升开发效率和代码质量

💡 行业数据:有完善单元测试的项目,bug修复成本降低80%,代码重构效率提升300%

测试的重要性:构建可靠软件的基石

单元测试不仅仅是代码验证,更是软件工程质量保证的核心实践。

javascript
// 🎉 单元测试重要性演示
// 没有测试的代码 - 脆弱且难以维护
class UserService {
    constructor() {
        this.users = [];
    }
    
    // 添加用户 - 没有测试保护
    addUser(user) {
        // 这里可能有很多潜在问题
        this.users.push(user);
        return user;
    }
    
    // 查找用户 - 边界条件未考虑
    findUser(id) {
        return this.users.find(user => user.id === id);
    }
    
    // 更新用户 - 错误处理缺失
    updateUser(id, updates) {
        const user = this.findUser(id);
        Object.assign(user, updates);
        return user;
    }
}

// 有测试保护的代码 - 健壮且可维护
class RobustUserService {
    constructor() {
        this.users = [];
        this.nextId = 1;
    }
    
    // 添加用户 - 有完整的验证和测试
    addUser(userData) {
        // 输入验证
        if (!userData || typeof userData !== 'object') {
            throw new Error('用户数据必须是对象');
        }
        
        if (!userData.name || typeof userData.name !== 'string') {
            throw new Error('用户名是必需的字符串');
        }
        
        if (!userData.email || !this.isValidEmail(userData.email)) {
            throw new Error('有效的邮箱地址是必需的');
        }
        
        // 检查邮箱唯一性
        if (this.users.some(user => user.email === userData.email)) {
            throw new Error('邮箱地址已存在');
        }
        
        // 创建用户对象
        const user = {
            id: this.nextId++,
            name: userData.name.trim(),
            email: userData.email.toLowerCase(),
            createdAt: new Date().toISOString(),
            updatedAt: new Date().toISOString()
        };
        
        this.users.push(user);
        return { ...user }; // 返回副本,避免外部修改
    }
    
    // 查找用户 - 有边界条件处理
    findUser(id) {
        if (typeof id !== 'number' || id <= 0) {
            throw new Error('用户ID必须是正整数');
        }
        
        return this.users.find(user => user.id === id) || null;
    }
    
    // 更新用户 - 有完整的错误处理
    updateUser(id, updates) {
        if (!updates || typeof updates !== 'object') {
            throw new Error('更新数据必须是对象');
        }
        
        const user = this.findUser(id);
        if (!user) {
            throw new Error(`用户ID ${id} 不存在`);
        }
        
        // 验证更新字段
        const allowedFields = ['name', 'email'];
        const updateFields = Object.keys(updates);
        const invalidFields = updateFields.filter(field => !allowedFields.includes(field));
        
        if (invalidFields.length > 0) {
            throw new Error(`不允许更新的字段: ${invalidFields.join(', ')}`);
        }
        
        // 验证邮箱唯一性(如果更新邮箱)
        if (updates.email && updates.email !== user.email) {
            if (!this.isValidEmail(updates.email)) {
                throw new Error('邮箱格式无效');
            }
            
            if (this.users.some(u => u.id !== id && u.email === updates.email.toLowerCase())) {
                throw new Error('邮箱地址已存在');
            }
        }
        
        // 执行更新
        const updatedUser = {
            ...user,
            ...updates,
            email: updates.email ? updates.email.toLowerCase() : user.email,
            name: updates.name ? updates.name.trim() : user.name,
            updatedAt: new Date().toISOString()
        };
        
        const userIndex = this.users.findIndex(u => u.id === id);
        this.users[userIndex] = updatedUser;
        
        return { ...updatedUser };
    }
    
    // 删除用户
    deleteUser(id) {
        const userIndex = this.users.findIndex(user => user.id === id);
        if (userIndex === -1) {
            throw new Error(`用户ID ${id} 不存在`);
        }
        
        const deletedUser = this.users.splice(userIndex, 1)[0];
        return { ...deletedUser };
    }
    
    // 获取所有用户
    getAllUsers() {
        return this.users.map(user => ({ ...user }));
    }
    
    // 邮箱验证辅助方法
    isValidEmail(email) {
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        return emailRegex.test(email);
    }
}

测试金字塔理论

  • 单元测试(70%):测试单个函数或类的功能
  • 集成测试(20%):测试模块间的交互
  • 端到端测试(10%):测试完整的用户场景

Jest测试框架:JavaScript测试的首选工具

Jest的强大功能和基础使用

Jest是Facebook开发的JavaScript测试框架,提供了完整的测试解决方案:

javascript
// 🔧 Jest测试框架完整示例
// userService.test.js

const RobustUserService = require('./RobustUserService');

describe('RobustUserService', () => {
    let userService;
    
    // 每个测试前重新初始化
    beforeEach(() => {
        userService = new RobustUserService();
    });
    
    // 测试用户添加功能
    describe('addUser', () => {
        test('应该成功添加有效用户', () => {
            // Arrange - 准备测试数据
            const userData = {
                name: 'Alice Johnson',
                email: 'alice@example.com'
            };
            
            // Act - 执行被测试的方法
            const result = userService.addUser(userData);
            
            // Assert - 验证结果
            expect(result).toMatchObject({
                id: expect.any(Number),
                name: 'Alice Johnson',
                email: 'alice@example.com',
                createdAt: expect.any(String),
                updatedAt: expect.any(String)
            });
            
            expect(result.id).toBe(1);
            expect(userService.getAllUsers()).toHaveLength(1);
        });
        
        test('应该拒绝无效的用户数据', () => {
            // 测试各种无效输入
            const invalidInputs = [
                null,
                undefined,
                '',
                123,
                [],
                {}
            ];
            
            invalidInputs.forEach(input => {
                expect(() => userService.addUser(input)).toThrow();
            });
        });
        
        test('应该拒绝缺少必需字段的用户', () => {
            const invalidUsers = [
                { email: 'test@example.com' }, // 缺少name
                { name: 'Test User' },         // 缺少email
                { name: '', email: 'test@example.com' }, // name为空
                { name: 'Test User', email: '' }         // email为空
            ];
            
            invalidUsers.forEach(user => {
                expect(() => userService.addUser(user)).toThrow();
            });
        });
        
        test('应该拒绝重复的邮箱地址', () => {
            const user1 = { name: 'User 1', email: 'test@example.com' };
            const user2 = { name: 'User 2', email: 'test@example.com' };
            
            userService.addUser(user1);
            
            expect(() => userService.addUser(user2)).toThrow('邮箱地址已存在');
        });
        
        test('应该正确处理邮箱大小写', () => {
            const user1 = { name: 'User 1', email: 'Test@Example.COM' };
            const user2 = { name: 'User 2', email: 'test@example.com' };
            
            const result = userService.addUser(user1);
            expect(result.email).toBe('test@example.com');
            
            expect(() => userService.addUser(user2)).toThrow('邮箱地址已存在');
        });
    });
    
    // 测试用户查找功能
    describe('findUser', () => {
        beforeEach(() => {
            userService.addUser({ name: 'Test User', email: 'test@example.com' });
        });
        
        test('应该找到存在的用户', () => {
            const user = userService.findUser(1);
            
            expect(user).not.toBeNull();
            expect(user.name).toBe('Test User');
            expect(user.email).toBe('test@example.com');
        });
        
        test('应该返回null对于不存在的用户', () => {
            const user = userService.findUser(999);
            expect(user).toBeNull();
        });
        
        test('应该拒绝无效的用户ID', () => {
            const invalidIds = [0, -1, 'abc', null, undefined, {}];
            
            invalidIds.forEach(id => {
                expect(() => userService.findUser(id)).toThrow();
            });
        });
    });
    
    // 测试用户更新功能
    describe('updateUser', () => {
        let userId;
        
        beforeEach(() => {
            const user = userService.addUser({ 
                name: 'Original Name', 
                email: 'original@example.com' 
            });
            userId = user.id;
        });
        
        test('应该成功更新用户信息', () => {
            const updates = { name: 'Updated Name' };
            const result = userService.updateUser(userId, updates);
            
            expect(result.name).toBe('Updated Name');
            expect(result.email).toBe('original@example.com');
            expect(result.updatedAt).not.toBe(result.createdAt);
        });
        
        test('应该拒绝更新不存在的用户', () => {
            expect(() => userService.updateUser(999, { name: 'Test' }))
                .toThrow('用户ID 999 不存在');
        });
        
        test('应该拒绝更新不允许的字段', () => {
            const invalidUpdates = { id: 999, createdAt: '2023-01-01' };
            
            expect(() => userService.updateUser(userId, invalidUpdates))
                .toThrow('不允许更新的字段');
        });
        
        test('应该验证邮箱唯一性', () => {
            // 添加另一个用户
            userService.addUser({ name: 'User 2', email: 'user2@example.com' });
            
            // 尝试更新为已存在的邮箱
            expect(() => userService.updateUser(userId, { email: 'user2@example.com' }))
                .toThrow('邮箱地址已存在');
        });
    });
    
    // 测试用户删除功能
    describe('deleteUser', () => {
        let userId;
        
        beforeEach(() => {
            const user = userService.addUser({ 
                name: 'Test User', 
                email: 'test@example.com' 
            });
            userId = user.id;
        });
        
        test('应该成功删除存在的用户', () => {
            const deletedUser = userService.deleteUser(userId);
            
            expect(deletedUser.id).toBe(userId);
            expect(userService.findUser(userId)).toBeNull();
            expect(userService.getAllUsers()).toHaveLength(0);
        });
        
        test('应该拒绝删除不存在的用户', () => {
            expect(() => userService.deleteUser(999))
                .toThrow('用户ID 999 不存在');
        });
    });
});

// 🚀 高级Jest功能示例
describe('Jest高级功能演示', () => {
    // 异步测试
    test('异步函数测试', async () => {
        const asyncFunction = async () => {
            return new Promise(resolve => {
                setTimeout(() => resolve('异步结果'), 100);
            });
        };
        
        const result = await asyncFunction();
        expect(result).toBe('异步结果');
    });
    
    // Mock函数测试
    test('Mock函数使用', () => {
        const mockCallback = jest.fn();
        const array = [1, 2, 3];
        
        array.forEach(mockCallback);
        
        expect(mockCallback).toHaveBeenCalledTimes(3);
        expect(mockCallback).toHaveBeenCalledWith(1);
        expect(mockCallback).toHaveBeenCalledWith(2);
        expect(mockCallback).toHaveBeenCalledWith(3);
    });
    
    // 模块Mock
    test('模块Mock示例', () => {
        // Mock axios模块
        jest.mock('axios');
        const axios = require('axios');
        
        axios.get.mockResolvedValue({ data: { message: 'success' } });
        
        // 测试使用axios的函数
        // ...
    });
    
    // 快照测试
    test('快照测试示例', () => {
        const component = {
            type: 'div',
            props: { className: 'container' },
            children: ['Hello World']
        };
        
        expect(component).toMatchSnapshot();
    });
    
    // 参数化测试
    test.each([
        [1, 2, 3],
        [2, 3, 5],
        [3, 4, 7]
    ])('加法测试: %i + %i = %i', (a, b, expected) => {
        expect(a + b).toBe(expected);
    });
});

Jest配置和最佳实践

javascript
// 🎯 Jest配置文件 - jest.config.js
module.exports = {
    // 测试环境
    testEnvironment: 'node', // 或 'jsdom' 用于浏览器环境
    
    // 测试文件匹配模式
    testMatch: [
        '**/__tests__/**/*.(js|jsx|ts|tsx)',
        '**/*.(test|spec).(js|jsx|ts|tsx)'
    ],
    
    // 覆盖率配置
    collectCoverage: true,
    collectCoverageFrom: [
        'src/**/*.{js,jsx,ts,tsx}',
        '!src/**/*.d.ts',
        '!src/index.js'
    ],
    coverageDirectory: 'coverage',
    coverageReporters: ['text', 'lcov', 'html'],
    
    // 覆盖率阈值
    coverageThreshold: {
        global: {
            branches: 80,
            functions: 80,
            lines: 80,
            statements: 80
        }
    },
    
    // 设置文件
    setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
    
    // 模块映射
    moduleNameMapping: {
        '^@/(.*)$': '<rootDir>/src/$1'
    },
    
    // 转换配置
    transform: {
        '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest'
    },
    
    // 忽略转换的模块
    transformIgnorePatterns: [
        'node_modules/(?!(module-to-transform)/)'
    ],
    
    // 清理Mock
    clearMocks: true,
    restoreMocks: true,
    
    // 测试超时
    testTimeout: 10000,
    
    // 详细输出
    verbose: true
};

// setupTests.js - 测试设置文件
import '@testing-library/jest-dom';

// 全局测试配置
global.console = {
    ...console,
    // 在测试中静默某些日志
    log: jest.fn(),
    debug: jest.fn(),
    info: jest.fn(),
    warn: jest.fn(),
    error: jest.fn()
};

// 全局Mock
Object.defineProperty(window, 'localStorage', {
    value: {
        getItem: jest.fn(),
        setItem: jest.fn(),
        removeItem: jest.fn(),
        clear: jest.fn()
    }
});

测试覆盖率:衡量测试质量的重要指标

测试覆盖率分析和提升策略

测试覆盖率是衡量代码被测试覆盖程度的重要指标:

javascript
// 🔧 测试覆盖率分析工具
class CoverageAnalyzer {
    constructor() {
        this.coverageData = null;
        this.thresholds = {
            statements: 80,
            branches: 75,
            functions: 85,
            lines: 80
        };
    }
    
    // 分析覆盖率报告
    analyzeCoverage(coverageReport) {
        this.coverageData = coverageReport;
        
        const analysis = {
            overall: this.calculateOverallCoverage(),
            byFile: this.analyzeFilesCoverage(),
            uncoveredLines: this.findUncoveredLines(),
            recommendations: this.generateRecommendations()
        };
        
        return analysis;
    }
    
    // 计算总体覆盖率
    calculateOverallCoverage() {
        const total = this.coverageData.total;
        
        return {
            statements: {
                covered: total.statements.covered,
                total: total.statements.total,
                percentage: (total.statements.covered / total.statements.total * 100).toFixed(2)
            },
            branches: {
                covered: total.branches.covered,
                total: total.branches.total,
                percentage: (total.branches.covered / total.branches.total * 100).toFixed(2)
            },
            functions: {
                covered: total.functions.covered,
                total: total.functions.total,
                percentage: (total.functions.covered / total.functions.total * 100).toFixed(2)
            },
            lines: {
                covered: total.lines.covered,
                total: total.lines.total,
                percentage: (total.lines.covered / total.lines.total * 100).toFixed(2)
            }
        };
    }
    
    // 分析文件覆盖率
    analyzeFilesCoverage() {
        const fileAnalysis = [];
        
        Object.entries(this.coverageData.files).forEach(([filePath, fileData]) => {
            const analysis = {
                file: filePath,
                statements: (fileData.statements.covered / fileData.statements.total * 100).toFixed(2),
                branches: (fileData.branches.covered / fileData.branches.total * 100).toFixed(2),
                functions: (fileData.functions.covered / fileData.functions.total * 100).toFixed(2),
                lines: (fileData.lines.covered / fileData.lines.total * 100).toFixed(2),
                uncoveredLines: this.getUncoveredLines(fileData)
            };
            
            fileAnalysis.push(analysis);
        });
        
        // 按覆盖率排序,最低的在前
        return fileAnalysis.sort((a, b) => 
            parseFloat(a.statements) - parseFloat(b.statements)
        );
    }
    
    // 查找未覆盖的代码行
    findUncoveredLines() {
        const uncoveredLines = [];
        
        Object.entries(this.coverageData.files).forEach(([filePath, fileData]) => {
            const lines = this.getUncoveredLines(fileData);
            if (lines.length > 0) {
                uncoveredLines.push({
                    file: filePath,
                    lines: lines
                });
            }
        });
        
        return uncoveredLines;
    }
    
    // 获取文件中未覆盖的行
    getUncoveredLines(fileData) {
        const uncoveredLines = [];
        
        Object.entries(fileData.statementMap).forEach(([statementId, location]) => {
            if (fileData.s[statementId] === 0) {
                uncoveredLines.push({
                    line: location.start.line,
                    column: location.start.column,
                    type: 'statement'
                });
            }
        });
        
        return uncoveredLines;
    }
    
    // 生成改进建议
    generateRecommendations() {
        const recommendations = [];
        const overall = this.calculateOverallCoverage();
        
        // 检查各项指标是否达标
        Object.entries(this.thresholds).forEach(([metric, threshold]) => {
            const current = parseFloat(overall[metric].percentage);
            if (current < threshold) {
                recommendations.push({
                    type: 'coverage',
                    metric: metric,
                    current: current,
                    target: threshold,
                    message: `${metric}覆盖率(${current}%)低于目标(${threshold}%)`
                });
            }
        });
        
        // 找出覆盖率最低的文件
        const fileAnalysis = this.analyzeFilesCoverage();
        const lowCoverageFiles = fileAnalysis.filter(file => 
            parseFloat(file.statements) < 60
        ).slice(0, 5);
        
        if (lowCoverageFiles.length > 0) {
            recommendations.push({
                type: 'files',
                message: '以下文件覆盖率较低,建议优先添加测试',
                files: lowCoverageFiles.map(file => ({
                    path: file.file,
                    coverage: file.statements + '%'
                }))
            });
        }
        
        return recommendations;
    }
    
    // 生成覆盖率报告
    generateReport() {
        if (!this.coverageData) {
            throw new Error('请先分析覆盖率数据');
        }
        
        const analysis = this.analyzeCoverage(this.coverageData);
        
        console.log('📊 测试覆盖率报告');
        console.log('==================');
        
        // 总体覆盖率
        console.log('\n📈 总体覆盖率:');
        Object.entries(analysis.overall).forEach(([metric, data]) => {
            const status = parseFloat(data.percentage) >= this.thresholds[metric] ? '✅' : '❌';
            console.log(`${status} ${metric}: ${data.percentage}% (${data.covered}/${data.total})`);
        });
        
        // 文件覆盖率(显示前5个最低的)
        console.log('\n📁 文件覆盖率 (最低5个):');
        analysis.byFile.slice(0, 5).forEach(file => {
            console.log(`  ${file.file}: ${file.statements}%`);
        });
        
        // 改进建议
        if (analysis.recommendations.length > 0) {
            console.log('\n💡 改进建议:');
            analysis.recommendations.forEach(rec => {
                console.log(`  • ${rec.message}`);
                if (rec.files) {
                    rec.files.forEach(file => {
                        console.log(`    - ${file.path} (${file.coverage})`);
                    });
                }
            });
        }
        
        return analysis;
    }
}

// 使用示例
const analyzer = new CoverageAnalyzer();

// 模拟覆盖率数据
const mockCoverageData = {
    total: {
        statements: { covered: 85, total: 100 },
        branches: { covered: 70, total: 90 },
        functions: { covered: 40, total: 50 },
        lines: { covered: 85, total: 100 }
    },
    files: {
        'src/userService.js': {
            statements: { covered: 20, total: 25 },
            branches: { covered: 15, total: 20 },
            functions: { covered: 8, total: 10 },
            lines: { covered: 20, total: 25 },
            statementMap: {
                '1': { start: { line: 10, column: 0 } },
                '2': { start: { line: 15, column: 2 } }
            },
            s: { '1': 5, '2': 0 } // 语句执行次数
        }
    }
};

const report = analyzer.analyzeCoverage(mockCoverageData);
analyzer.generateReport();

测试覆盖率最佳实践

  • 🎯 设定合理目标:不同项目类型设定不同的覆盖率目标
  • 🎯 关注质量而非数量:高覆盖率不等于高质量测试
  • 🎯 优先核心逻辑:重点测试业务核心逻辑和复杂算法
  • 🎯 持续监控:将覆盖率检查集成到CI/CD流程

💼 实战经验:覆盖率是重要指标但不是唯一指标,应该结合代码质量、测试质量综合评估


📚 JavaScript单元测试学习总结与下一步规划

✅ 本节核心收获回顾

通过本节JavaScript单元测试基础详解的学习,你已经掌握:

  1. 测试重要性深度理解:认识到单元测试在软件开发中的核心价值
  2. Jest测试框架精通:熟练使用Jest进行各种类型的测试
  3. 测试覆盖率分析技能:能够分析和提升代码测试覆盖率
  4. 测试驱动开发基础:了解TDD开发模式和最佳实践
  5. 测试策略制定能力:建立了完整的前端测试工作流程

🎯 JavaScript单元测试下一步

  1. 深入学习集成测试:掌握模块间交互的测试方法
  2. 探索端到端测试:学习Cypress等E2E测试工具
  3. 实践测试驱动开发:在实际项目中应用TDD开发模式
  4. 学习性能测试:掌握JavaScript应用性能测试技术

🔗 相关学习资源

  • Jest官方文档https://jestjs.io/docs/getting-started
  • Testing Library指南:React Testing Library最佳实践
  • 测试驱动开发教程:TDD开发模式深入学习
  • 前端测试最佳实践:业界前端测试经验总结

💪 实践建议

  1. 建立测试习惯:在日常开发中主动编写单元测试
  2. 提升测试质量:关注测试的可读性和维护性
  3. 持续学习新技术:跟进测试工具和框架的发展
  4. 团队测试文化:在团队中推广测试驱动开发理念

🔍 常见问题FAQ

Q1: 什么时候应该写单元测试?

A: 建议在编写功能代码的同时或之前编写测试。对于核心业务逻辑、复杂算法、公共工具函数,应该优先编写测试。

Q2: 测试覆盖率达到多少才算合格?

A: 没有绝对标准,一般建议:核心业务逻辑90%+,工具函数80%+,UI组件60%+。重要的是测试质量而非覆盖率数字。

Q3: 如何测试异步代码?

A: Jest支持Promise、async/await、回调函数等多种异步测试方式。使用async/await是最推荐的方法。

Q4: Mock和Stub有什么区别?

A: Mock关注行为验证(是否被调用、调用参数),Stub关注状态验证(返回特定值)。Jest的mock函数同时支持两种用法。

Q5: 如何测试私有方法?

A: 一般不直接测试私有方法,而是通过测试公有方法来间接测试。如果私有方法逻辑复杂,考虑将其提取为独立的工具函数。


🛠️ 单元测试实施指南

常见测试场景解决方案

异步操作测试

javascript
// 问题:如何测试异步操作
// 解决:使用async/await和Promise

describe('异步操作测试', () => {
    test('Promise测试', async () => {
        const fetchData = () => {
            return Promise.resolve('数据');
        };
        
        const data = await fetchData();
        expect(data).toBe('数据');
    });
    
    test('错误处理测试', async () => {
        const fetchDataWithError = () => {
            return Promise.reject(new Error('网络错误'));
        };
        
        await expect(fetchDataWithError()).rejects.toThrow('网络错误');
    });
});

DOM操作测试

javascript
// 问题:如何测试DOM操作
// 解决:使用jsdom环境和Testing Library

/**
 * @jest-environment jsdom
 */

import { fireEvent, screen } from '@testing-library/dom';

describe('DOM操作测试', () => {
    test('按钮点击测试', () => {
        document.body.innerHTML = `
            <button id="test-btn">点击我</button>
            <div id="result"></div>
        `;
        
        const button = document.getElementById('test-btn');
        const result = document.getElementById('result');
        
        button.addEventListener('click', () => {
            result.textContent = '已点击';
        });
        
        fireEvent.click(button);
        expect(result.textContent).toBe('已点击');
    });
});

"掌握单元测试是现代JavaScript开发者的必备技能。通过系统学习测试理论、Jest框架和测试覆盖率分析,你将能够编写高质量的测试代码,构建更可靠的软件系统,成为更专业的前端开发者!"