mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2789 字
7 分钟
怎么保证软件质量
2021-03-26

一、软件质量是什么?#

提起「质量」一词,若未限定具体领域,每个人都会有不同的定义。因此,在学习如何保证软件质量之前,不妨先了解评估软件质量的国际标准 ISO/IEC 9126。该标准提出了 6 个重要的质量特性:功能性、可靠性、易用性、效率性、可维护性和可移植性。每个质量特性又可进一步划分为若干属性。属性是可在软件产品中验证或测量的实体,因不同软件产品而异,标准中未作统一定义。

graph TB subgraph "ISO/IEC 9126 质量模型" Q["软件质量"] --> F["功能性"] Q --> R["可靠性"] Q --> U["易用性"] Q --> E["效率性"] Q --> M["可维护性"] Q --> P["可移植性"] F --> F1["适合性"] F --> F2["准确性"] F --> F3["互操作性"] R --> R1["成熟性"] R --> R2["容错性"] R --> R3["可恢复性"] M --> M1["可分析性"] M --> M2["可修改性"] M --> M3["稳定性"] M --> M4["可测试性"] P --> P1["适应性"] P --> P2["可安装性"] P --> P3["一致性"] end

近年来,「测试左移」和「测试右移」的理念日益流行。究其根本,是因为质量保证本就贯穿于项目的整个生命周期。

注:有人也称其为「质量左移」和「质量右移」,因为测试是保证质量的重要手段,所以这两个术语有时会相互指代。

graph LR subgraph "质量保障全周期" direction LR A["需求分析"] --> B["设计"] B --> C["编码"] C --> D["测试"] D --> E["部署"] E --> F["运维"] subgraph "测试左移" A B C end subgraph "传统测试" D end subgraph "测试右移" E F end G["质量保障贯穿始终"] -.-> A G -.-> B G -.-> C G -.-> D G -.-> E G -.-> F end

二、定义自己的质量架构图#

质量架构图是在整个项目周期中,用于保证项目质量的方法论集合。

每款产品对质量的需求各不相同,因此需要具体场景具体分析,但可以借助一些通用的实践方法论:

graph TB subgraph "质量保障体系" P["项目计划"] --> |"明确目标时间表"| Q["质量标准"] C["沟通机制"] --> |"及时响应"| Q I["质量指标"] --> |"量化衡量"| Q T["管理工具"] --> |"高效管理"| Q R["改进机制"] --> |"持续优化"| Q E["定期评估"] --> |"发现问题"| Q end

建立详细的项目计划。 项目计划是项目实施的基础,应细化到每一个任务和环节。制定计划时,需结合项目特点与需求,明确项目目标、任务分配、时间表和质量标准。

建立有效的沟通机制。 项目实施过程中,团队成员需要持续沟通协调,以确保项目顺利进行。因此,应建立有效的沟通机制,让团队成员及时了解项目进展,并能快速响应。

设立质量控制指标。 质量控制指标是衡量项目质量的重要工具,应清晰明确,并与项目目标和质量标准相对应。设立指标时,需结合项目实际情况,确定指标的种类、内容和方法。

使用项目管理软件。 项目管理软件是用于管理项目进度、任务分配、质量控制等内容的工具。使用此类软件可以更高效地管理项目,及时发现并解决问题,采取必要的调整和应对措施。

建立质量改进机制。 项目实施过程中难免遇到质量问题,需要采取措施加以解决。因此,应建立质量改进机制,对项目中出现的质量问题及时纠正和改进。

定期进行质量评估。 质量评估是衡量项目质量的重要手段,应定期开展。评估时需收集项目的实际数据和信息,综合评估项目质量,并提出改进建议。

三、测试金字塔模型#

测试金字塔是指导测试策略的经典模型,由 Mike Cohen 提出。它强调测试应该呈金字塔分布:底层测试数量多、成本低、速度快;顶层测试数量少、成本高、速度慢。

graph TB subgraph "测试金字塔" E2E["E2E 测试<br>端到端<br>数量少 成本高"] INT["集成测试<br>模块间交互<br>数量中等"] UNIT["单元测试<br>函数/类级别<br>数量多 成本低"] end UNIT --> INT INT --> E2E style UNIT fill:#4ade80,stroke:#166534 style INT fill:#fbbf24,stroke:#92400e style E2E fill:#f87171,stroke:#991b1b
graph LR subgraph "各层测试对比" A["测试类型"] --> B["单元测试"] A --> C["集成测试"] A --> D["E2E测试"] B --> B1["速度: 毫秒级"] B --> B2["成本: 低"] B --> B3["覆盖: 函数/类"] C --> C1["速度: 秒级"] C --> C2["成本: 中"] C --> C3["覆盖: 模块交互"] D --> D1["速度: 分钟级"] D --> D2["成本: 高"] D --> D3["覆盖: 完整流程"] end
测试层级测试类型测试目标运行时间维护成本推荐比例
底层单元测试函数、类、方法毫秒级70%
中层集成测试模块间交互秒级20%
顶层E2E/UI 测试完整业务流程分钟级10%

3.1 单元测试#

单元测试是最基础的测试层级,针对最小可测试单元(函数、方法、类)进行验证。

// 单元测试示例
describe("Calculator", () => {
it("should add two numbers correctly", () => {
const calculator = new Calculator();
expect(calculator.add(2, 3)).toBe(5);
});
it("should throw error when dividing by zero", () => {
const calculator = new Calculator();
expect(() => calculator.divide(1, 0)).toThrow("Division by zero");
});
});

单元测试最佳实践

  • AAA 模式:Arrange(准备)、Act(执行)、Assert(断言)
  • 一个测试一个断言:保持测试聚焦
  • 测试边界条件:空值、边界值、异常情况
  • Mock 外部依赖:数据库、网络请求等

3.2 集成测试#

集成测试验证模块间的交互是否正确。

// 集成测试示例
describe("UserService integration", () => {
let userService: UserService;
let database: Database;
beforeAll(async () => {
database = await createTestDatabase();
userService = new UserService(database);
});
afterAll(async () => {
await database.close();
});
it("should create and retrieve user", async () => {
const user = await userService.create({
name: "John",
email: "john@example.com",
});
const retrieved = await userService.findById(user.id);
expect(retrieved.name).toBe("John");
});
});

3.3 E2E 测试#

端到端测试模拟真实用户操作,验证完整业务流程。

// E2E 测试示例 (Playwright)
test("user registration flow", async ({ page }) => {
await page.goto("/register");
await page.fill('input[name="email"]', "test@example.com");
await page.fill('input[name="password"]', "password123");
await page.click('button[type="submit"]');
await expect(page).toHaveURL("/dashboard");
await expect(page.locator(".welcome-message")).toContainText("Welcome");
});

四、编码方面#

4.1 提高代码的可测试性#

可测试性是指软件在被测试时的容易程度。提高代码的可测试性,不仅能降低编写测试的成本,还能在开发阶段更早地发现缺陷。

graph TB subgraph "可测试性关键原则" DI["依赖注入"] --> D1["Mock 替换真实依赖"] SRP["单一职责"] --> S1["测试用例更简单"] NS["避免全局状态"] --> N1["测试独立可重复"] SE["控制副作用"] --> E1["纯函数易于断言"] IA["接口抽象"] --> I1["支持测试替身"] end

依赖注入(Dependency Injection)。避免在函数或类内部直接创建依赖对象,而是通过参数、构造函数或配置注入依赖。这使得测试时可以用 Mock 对象替换真实依赖,隔离测试范围。

// 难以测试:内部创建依赖
class UserService {
private db = new Database(); // 硬编码依赖
async getUser(id: string) {
return this.db.find(id);
}
}
// 可测试:依赖注入
class UserService {
constructor(private db: Database) {} // 通过构造函数注入
async getUser(id: string) {
return this.db.find(id);
}
}

单一职责原则。每个函数或类只做一件事,职责越单一,测试用例越简单。一个负责多件事情的函数需要测试各种组合情况,测试复杂度呈指数增长。

避免全局状态。全局变量和单例模式会带来隐式依赖,导致测试之间相互干扰。尽量使用局部状态或显式传递上下文,保证测试的独立性和可重复性。

控制副作用。将纯逻辑与 I/O 操作分离。纯函数给定相同输入总返回相同输出,易于断言;副作用(如数据库操作、网络请求、文件读写)则需要通过 Mock 或集成测试覆盖。

接口抽象。面向接口编程而非具体实现,便于在测试时替换为测试替身(Test Double),如 Stub、Mock、Fake 等。

graph TB subgraph "测试替身类型 Test Doubles" D["Dummy"] --> D1["占位 不使用"] S["Stub"] --> S1["预设响应"] M["Mock"] --> M1["验证交互"] F["Fake"] --> F1["简化实现"] SP["Spy"] --> SP1["记录调用"] end

4.2 面向对象的语言#

面向对象语言(如 Java、C#、TypeScript)的测试策略应充分利用其封装、继承和多态特性,同时遵循设计原则以降低耦合度。

遵循 SOLID 原则。SOLID 是保证代码可维护性和可测试性的五大设计原则(详见代码设计的原则):

  • S(单一职责):每个类只承担一个职责,测试时只需关注一个维度
  • O(开放封闭):通过抽象扩展功能而非修改代码,避免破坏现有测试
  • L(里氏替换):子类可替换父类,便于用 Mock 对象模拟行为
  • I(接口隔离):接口粒度要小,Mock 实现更简单
  • D(依赖倒置):依赖抽象而非具体实现,支持依赖注入

使用依赖注入框架。Spring、NestJS 等框架提供了完善的 IoC 容器,自动管理对象生命周期和依赖关系。测试时可轻松替换 Bean 为 Mock 实现。

// Spring 示例:通过 @MockBean 替换真实依赖
@SpringBootTest
class OrderServiceTest {
@MockBean
private PaymentGateway paymentGateway;
@Autowired
private OrderService orderService;
@Test
void shouldProcessOrderSuccessfully() {
when(paymentGateway.charge(any())).thenReturn(true);
orderService.processOrder(order);
verify(paymentGateway).charge(any());
}
}

Mock 框架的应用。JUnit + Mockito、Jest、Vitest 等工具可以在运行时动态创建代理对象,模拟依赖行为,验证方法调用。关键是用 Mock 隔离外部依赖,聚焦被测单元的逻辑正确性。

避免过度使用继承。深层继承链会增加测试复杂度,子类需要理解父类的所有行为。优先使用组合(Composition)替代继承,通过依赖注入组装行为,测试时可以单独测试各组件。

4.3 面向函数的语言#

函数式语言(如 Haskell、F#、Clojure)和多范式语言中的函数式风格(如 JavaScript、Python、Rust),其测试策略围绕纯函数和不可变性展开。

graph TB subgraph "函数式测试策略" PF["纯函数"] --> P1["确定性测试"] IM["不可变数据"] --> I1["无状态干扰"] HO["高阶函数"] --> H1["函数组合测试"] PT["属性测试"] --> T1["自动生成用例"] SI["副作用隔离"] --> S1["边界 Mock"] end

拥抱纯函数。纯函数没有副作用,相同输入永远产生相同输出。这种确定性使测试变得极其简单——只需验证输入输出的映射关系,无需考虑外部状态。

-- Haskell 纯函数示例
add :: Int -> Int -> Int
add x y = x + y
-- 测试简单直接
testAdd = do
assertEqual "1 + 2 = 3" (add 1 2) 3
assertEqual "0 + 0 = 0" (add 0 0) 0

不可变数据结构。不可变性消除了状态变化带来的复杂性,无需担心测试修改了全局状态影响其他测试。数据转换形成清晰的数据流管道,测试只需验证管道每个阶段的输出。

高阶函数与组合。高阶函数接收函数作为参数或返回函数,支持将复杂逻辑拆解为可复用的小函数。通过函数组合(Composition),可以从简单函数构建复杂行为,测试时可逐一验证基础函数,再验证组合后的整体行为。

// 函数组合示例
const pipe =
<T>(...fns: Array<(x: T) => T>) =>
(x: T) =>
fns.reduce((v, f) => f(v), x);
const double = (x: number) => x * 2;
const increment = (x: number) => x + 1;
const doubleThenIncrement = pipe(double, increment);
// 测试基础函数
test("double works", () => expect(double(3)).toBe(6));
test("increment works", () => expect(increment(6)).toBe(7));
// 测试组合函数
test("pipe works", () => expect(doubleThenIncrement(3)).toBe(7));

属性测试(Property-based Testing)。不同于单元测试的「示例驱动」,属性测试通过定义数据应满足的属性(如交换律、结合律),由框架自动生成大量随机测试用例。Haskell 的 QuickCheck、JavaScript 的 fast-check 是典型代表。

// fast-check 示例:验证数组反转两次等于原数组
fc.assert(
fc.property(fc.array(fc.integer()), arr => {
expect(reverse(reverse(arr))).toEqual(arr);
})
);

副作用隔离。函数式编程将副作用推到系统边界,核心业务逻辑保持纯函数。测试时,纯函数部分进行快速单元测试,副作用部分通过 Mock 或集成测试覆盖。这种分离大大降低了测试复杂度。

五、CI/CD 与自动化质量保障#

graph LR subgraph "CI/CD 质量流水线" A["代码提交"] --> B["静态分析"] B --> C["单元测试"] C --> D["集成测试"] D --> E["构建"] E --> F["部署测试环境"] F --> G["E2E 测试"] G --> H["生产部署"] end

5.1 静态代码分析#

静态分析工具可以在不运行代码的情况下发现问题:

工具类型工具示例检测内容
代码检查ESLint、Pylint、golangci-lint语法、风格、潜在问题
类型检查TypeScript、mypy类型错误
安全扫描SonarQube、Snyk安全漏洞
代码复杂度SonarQube、Codacy圈复杂度、重复代码

5.2 测试覆盖率#

测试覆盖率是衡量测试充分性的重要指标,但不应盲目追求 100%。

graph TB subgraph "覆盖率目标建议" C["覆盖率"] --> C1["核心业务: 80%+"] C --> C2["工具类: 90%+"] C --> C3["UI 层: 50%+"] C --> C4["配置代码: 可不测"] end

覆盖率误区

  • 高覆盖率 ≠ 高质量:可能测试都是浅层的
  • 追求 100% 可能导致过度测试
  • 应关注关键路径和边界条件的覆盖

六、小结#

graph TB subgraph "质量保障体系总结" Q["软件质量"] --> T["测试策略"] Q --> C["代码质量"] Q --> P["流程保障"] T --> T1["测试金字塔"] T --> T2["测试左移"] T --> T3["测试右移"] C --> C1["可测试性设计"] C --> C2["SOLID 原则"] C --> C3["代码审查"] P --> P1["CI/CD"] P --> P2["静态分析"] P --> P3["覆盖率监控"] end

保证软件质量需要从多个维度入手:

  • 测试策略:建立合理的测试金字塔,平衡测试成本和覆盖范围
  • 代码质量:提高可测试性,遵循设计原则
  • 流程保障:自动化 CI/CD,持续监控质量指标

质量不是测试出来的,而是设计和构建出来的。好的设计加上完善的测试,才能真正保证软件质量。


七、参考#


参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

怎么保证软件质量
https://blog.souloss.com/posts/programming/how-to-ensure-software-quality/
作者
Souloss
发布于
2021-03-26
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时