脚本单元测试怎做?

wen 实用脚本 49

本文目录导读:

脚本单元测试怎做?

  1. 核心思想:为什么脚本要做单元测试?
  2. 通用方法论(适用于任何脚本)
  3. 具体语言的实践指南
  4. 通用最佳实践
  5. 总结清单

针对“脚本单元测试怎么做”,这个问题涵盖面很广,因为“脚本”可以是 Shell、Python、JavaScript (Node.js)、PowerShell 等,我会从通用方法论三种最常见脚本语言的具体实践来解答。

核心思想:为什么脚本要做单元测试?

脚本往往用于自动化任务、数据处理、系统管理或CI/CD流程,一旦出错,可能导致数据丢失、服务中断或部署失败,单元测试能确保脚本中的每一个函数逻辑块按预期工作。


通用方法论(适用于任何脚本)

  1. 模块化(最重要的一步)

    • 不要把所有代码都堆在 main() 函数里。
    • 将核心逻辑拆分成独立的函数(纯函数最佳:输入相同,输出总是相同)。
    • 示例: 不要写一个500行的脚本做所有事,而是拆成 parse_config(), validate_data(), send_notification()
  2. 隔离外部依赖(Mock/Stub)

    • 脚本常涉及:文件系统(读/写文件)、网络请求(API调用)、系统命令(rm, cp)、环境变量。
    • 测试时,不能真的去创建/删除文件或调用第三方API(否则测试变成集成测试,且危险)。
    • 做法: 使用 Mock(模拟)技术,假装调用了外部命令,并返回预设结果。
  3. 断言预期结果

    • 使用断言库(如 assert)或测试框架的 expect 来验证:
      • 函数的返回值。
      • 函数是否被调用了(发送邮件函数是否被调用了一次)。
      • 是否抛出了预期的错误。
  4. 测试可重复性

    测试脚本应该可以被反复运行,且结果一致(不依赖当前时间、随机数或文件状态)。


具体语言的实践指南

Python 脚本 (pytest 是王道)

Python 是脚本测试中最成熟的语言。

工具推荐: pytest(最流行),unittest(内置)。

结构示例: 假设你有一个 utils.py 脚本,里面有一个处理数据的函数。

# utils.py
import os
def process_directory(dirpath):
    """根据目录是否存在,返回不同状态码"""
    if os.path.isdir(dirpath):
        return 200  # 存在
    else:
        return 404  # 不存在
def calculate_bonus(sales_amount):
    if not isinstance(sales_amount, (int, float)):
        raise TypeError("Sales amount must be a number")
    if sales_amount < 0:
        raise ValueError("Sales amount cannot be negative")
    return sales_amount * 0.1

编写测试文件(test_utils.py):

# test_utils.py
import pytest
from utils import process_directory, calculate_bonus
from unittest.mock import patch
# --- 测试纯函数(最简单)---
def test_calculate_bonus_positive():
    assert calculate_bonus(1000) == 100.0
def test_calculate_bonus_zero():
    assert calculate_bonus(0) == 0
def test_calculate_bonus_negative():
    with pytest.raises(ValueError): # 断言抛出异常
        calculate_bonus(-10)
def test_calculate_bonus_wrong_type():
    with pytest.raises(TypeError):
        calculate_bonus("abc")
# --- 测试涉及到文件系统(Mock外部依赖)---
def test_process_directory_exists():
    # 模拟 os.path.isdir 返回 True
    with patch('utils.os.path.isdir', return_value=True):
        assert process_directory('/any/path') == 200
def test_process_directory_not_exists():
    # 模拟 os.path.isdir 返回 False
    with patch('utils.os.path.isdir', return_value=False):
        assert process_directory('/fake/path') == 404

如何运行:

pip install pytest
pytest test_utils.py -v  # -v 显示详细输出

JavaScript/Node.js 脚本 (JestVitest)

Node.js 脚本在 CLI 工具、CI/CD 脚本中非常常见。

工具推荐: Jest(最流行,推荐)。

结构示例: 假设你的脚本 helpers.js

// helpers.js
const fs = require('fs');
function add(a, b) {
    return a + b;
}
function readConfig(filepath) {
    if (!fs.existsSync(filepath)) {
        throw new Error('Config file not found');
    }
    return JSON.parse(fs.readFileSync(filepath, 'utf-8'));
}
module.exports = { add, readConfig };

编写测试文件(helpers.test.js):

// helpers.test.js
const { add, readConfig } = require('./helpers');
const fs = require('fs');
// Jest 会自动模拟 fs 模块
jest.mock('fs');
describe('Helper Functions Unit Tests', () => {
    test('add function works correctly', () => {
        expect(add(1, 2)).toBe(3);
        expect(add(-1, 1)).toBe(0);
    });
    test('readConfig throws error if file missing', () => {
        // 模拟 fs.existsSync 返回 false
        fs.existsSync.mockReturnValue(false);
        expect(() => readConfig('missing.json')).toThrow('Config file not found');
    });
    test('readConfig returns parsed JSON if file exists', () => {
        const mockData = { version: 1, name: 'test' };
        fs.existsSync.mockReturnValue(true);
        fs.readFileSync.mockReturnValue(JSON.stringify(mockData));
        const result = readConfig('config.json');
        expect(result).toEqual(mockData); // 使用 toEqual 比较对象
    });
});

如何运行:

npm install --save-dev jest
npx jest --verbose

Shell 脚本 (shUnit2bats)

Shell 测试最棘手,因为没法原生 Mock,但可以用 bats(Bash Automated Testing System)。

工具推荐: bats (Bash)。

结构示例: 假设你的脚本 deploy.sh

#!/bin/bash
# deploy.sh
deploy_application() {
    local app_name="$1"
    if [ -z "$app_name" ]; then
        echo "Error: App name required" >&2
        return 1
    fi
    echo "Deploying $app_name..."
    # 实际会调用 kubectl,但这里用 echo 模拟
    return 0
}

编写测试文件(test_deploy.bats):

#!/usr/bin/env bats
# test_deploy.bats
# 导入脚本中的函数(注意:source 会使测试环境受影响,要小心)
setup() {
    # 在每个测试前,定义或 source 函数
    source ./deploy.sh
}
@test "deploy without app name should fail" {
    run deploy_application  # 不传参
    [ "$status" -eq 1 ]     # 检查退出码是否为1
    [ "$output" = "Error: App name required" ]  # 检查输出
}
@test "deploy with valid app name should succeed" {
    run deploy_application "my-service"
    [ "$status" -eq 0 ]
    # 注意:输出会包含 "Deploying my-service..."
    [ "$output" = "Deploying my-service..." ]
}

Mock 外部命令(例如避免真的调用 kubectl): 在测试脚本中,可以暂时覆盖命令:

@test "deploy should call kubectl" {
    # 创建一个假的 kubectl 命令
    function kubectl() {
        echo "kubectl called with: $@"  # 记录调用参数
    }
    export -f kubectl  # 导出给子 shell 使用
    run deploy_application "my-app"
    [ "$status" -eq 0 ]
    [[ "$output" == *"kubectl called with:"* ]]  # 验证 kubectl 被调用了
}

如何运行:

# 安装 bats
brew install bats-core  # macOS
# 或从 GitHub 下载
bats test_deploy.bats

通用最佳实践

  1. 测试覆盖率不是万能的:100% 覆盖率不代表没 bug,但 0% 覆盖率相当于裸奔,目标是覆盖核心逻辑边界条件(空值、负数、超长字符串、权限错误)。
  2. CI/CD 集成:在 GitLab CI、GitHub Actions 或 Jenkins 中,将 pytestnpx jestbats 作为 pipeline 中的一个步骤,测试不通过,禁止合并代码
  3. 测试数据库/文件:尽量避免在单元测试里操作真实数据库或文件系统,使用临时目录 (tempfile),并在 teardown 中清理,或者在内存中模拟。
  4. 保持测试简单:一个测试函数只做一件事,命名清晰:test_input_is_empty_returns_error

总结清单

脚本语言 推荐框架 关键点
Python pytest + unittest.mock 最舒适,Mock 功能强大,非常适合数据处理类脚本
JavaScript (Node) Jest 对异步支持好,内置 Mock 和覆盖率报告
Shell (Bash) bats 最基础,很难做深度 Mock,适合验证逻辑分支和错误处理
PowerShell Pester .NET 生态,功能强大,可以 Mock Cmdlet

给新手建议:Python 脚本 开始练习单元测试,因为 pytest 的报错信息非常友好,且 Mock 机制很人性化,写好你的第一个 test_add() 函数(只测试纯函数),你就已经迈出了单元测试的第一步。

抱歉,评论功能暂时关闭!