本文目录导读:

针对“脚本单元测试怎么做”,这个问题涵盖面很广,因为“脚本”可以是 Shell、Python、JavaScript (Node.js)、PowerShell 等,我会从通用方法论和三种最常见脚本语言的具体实践来解答。
核心思想:为什么脚本要做单元测试?
脚本往往用于自动化任务、数据处理、系统管理或CI/CD流程,一旦出错,可能导致数据丢失、服务中断或部署失败,单元测试能确保脚本中的每一个函数或逻辑块按预期工作。
通用方法论(适用于任何脚本)
-
模块化(最重要的一步)
- 不要把所有代码都堆在
main()函数里。 - 将核心逻辑拆分成独立的函数(纯函数最佳:输入相同,输出总是相同)。
- 示例: 不要写一个500行的脚本做所有事,而是拆成
parse_config(),validate_data(),send_notification()。
- 不要把所有代码都堆在
-
隔离外部依赖(Mock/Stub)
- 脚本常涉及:文件系统(读/写文件)、网络请求(API调用)、系统命令(
rm,cp)、环境变量。 - 测试时,不能真的去创建/删除文件或调用第三方API(否则测试变成集成测试,且危险)。
- 做法: 使用 Mock(模拟)技术,假装调用了外部命令,并返回预设结果。
- 脚本常涉及:文件系统(读/写文件)、网络请求(API调用)、系统命令(
-
断言预期结果
- 使用断言库(如
assert)或测试框架的expect来验证:- 函数的返回值。
- 函数是否被调用了(发送邮件函数是否被调用了一次)。
- 是否抛出了预期的错误。
- 使用断言库(如
-
测试可重复性
测试脚本应该可以被反复运行,且结果一致(不依赖当前时间、随机数或文件状态)。
具体语言的实践指南
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 脚本 (Jest 或 Vitest)
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 脚本 (shUnit2 或 bats)
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
通用最佳实践
- 测试覆盖率不是万能的:100% 覆盖率不代表没 bug,但 0% 覆盖率相当于裸奔,目标是覆盖核心逻辑和边界条件(空值、负数、超长字符串、权限错误)。
- CI/CD 集成:在 GitLab CI、GitHub Actions 或 Jenkins 中,将
pytest、npx jest或bats作为 pipeline 中的一个步骤,测试不通过,禁止合并代码。 - 测试数据库/文件:尽量避免在单元测试里操作真实数据库或文件系统,使用临时目录 (
tempfile),并在teardown中清理,或者在内存中模拟。 - 保持测试简单:一个测试函数只做一件事,命名清晰:
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() 函数(只测试纯函数),你就已经迈出了单元测试的第一步。