先写单元测试的12个好处

译者的话

测试驱动(TDD)的思想早有耳闻,但是我们都只是听说过它有很多好处,却很少有人实践。其实我们对其优势并没有全面的了解。最近我阅读了许多这方面非常优秀的文章。如果有时间我会多翻译一些发出来。这是单元测试系列的第二篇译文。

原文链接:Twelve Benefits of Writing Unit Tests First

系列第一篇译文可查看:打桩(Stubbing), Mocking 和服务虚拟化的差异

正文

为什么程序员讨厌写单元测试?为什么他们甚至更讨厌在写代码之前先写单元测试?你不需要回答。我已经听到过很多的借口。这些是反问句。我有一套理论,然而,真正的原因是什么呢?

大多数软件开发人员从来没有认真地尝试过 测试先行(test-first) 的开发方式。或者,如果他们尝试过了,他们并没有在一个支持性的环境中这样做。但是更多的是前者。所以他们会给出这样的托辞:“我们没有时间写单元测试。”或者:“单元测试不能使你的代码刀枪不入。”他们的反应是出于恐惧而非独立思考的结果。他们试图找出让生活不幸的原因而不是让他们的生活更好。巧合的是,偶尔你会发现这些开发人员中有的人,当你摆出事实给他看时,他自己都站不住脚了。看起来很有趣。他做这一切,从来没有收回他对先写测试的看法。这是一种邪恶的哲学。

就像 Kent Beck 在 《Test Driven Development By Example》中解释说,测试先行由三步组成:

  1. 红(Red) —— 写一个表达你将如何使用代码和你需要它去做的事情的测试。这个测试通不过(因为还没有实现),在很多UI中以红条(red bar)的形式展现。
  2. 绿(Green) —— 写出足够的代码以让测试通过,但是除此之外不能写更多。如果你需要写更多的代码,例如,检查错误,那么先写另一个测试以表达这个特性。现在,继续写足够的代码以让测试通过。
  3. 重构 —— 清除代码以移除冗余代码和改善设计。然后重新运行测试以确保你没有破坏任何东西。

重复这个过程直到你完成任务。这是一个难以置信的简单的过程。但是为什么开发者恐惧它呢?因为它需要人们在开发软件的方式上做一次根本范式的转换。

你如何解决软件问题?在学校里他们怎么教你处理的?你要做的第一件事情是什么?你在想如何解决它。你会问,“我需要写什么代码来生成一个解决方案?”那是后面的。这是你必须做的第一件事,事实上这也是他们在学校里说的,但是根据我的经验,这更多的是一种口头禅而非真正的服务。你要问的第一个问题不是“我将要写什么代码?”而是“我怎么知道我已经解决了这个问题?

我们一直被教导着事先假设说我们已经知道了如何说明我们的解决方案是否可行。这不是一个问题。像下流的行为一样,我们一看到就会知道。我们相信,在写代码之前,我们不需要去想它需要做什么。这个信念如此根深蒂固,我们大多数人都很难改变。当然,像一个我这样的捣乱份子,这个改变只不过是一个明智且可行的实验而已。

这里为那些可以实现这一飞跃的人提供了几个你将会体验到的好处。所有这些我都经历过了。但是别只是听我在这里说,你自己试试看。

  1. 单元测试证明你的代码真的可用。 这意味着你的代码很少有bug。不,单元测试不能替代系统测试和验收测试。但是它们确实补充了它。更少的bug,让软件质量保证(Software Quality Assurance, SQA)变得更好。
  2. 你得到一个低层级的回归测试套件。你可以随时重复执行并且看到不止是出现了什么错误,而且能知道bug出在哪里。很多团队将执行单元测试做为常规编译的一部分。这是一种低成本的在进入系统性测试编译之前抓出bug的方式。
  3. 你能在没有破坏设计的情况下改进它。 这其实是上面提到的第三步(重构)的一部分。因此,测试先行的代码通常不需要重构。我曾接手过一些非常神经质的系统,像一个精神错乱的人一样,你都不能理清它们。拥有单元测试,你可以做非常强大的重构,这些重构可以让你从大多数精神错乱系统的挑战中脱身。
  4. 写单元测试让写代码变得更加有趣。 你知道你的代码需要做什么。然后你让它去做。甚至如果你没有一个正常工作的系统,你也可以看到你的代码真正运行起来并且可以正常工作的样子。你得到一种非常强烈的“我成功了!”的成就感。现在每分钟重复执行一次。如果你想变得非常嗨,为你的工作感到骄傲,以此激励持续前进,尽管尝试测试先行的开发模式吧!
  5. 它们展现出实质性的工作进展。 你不必为了让系统的所有部分都组装到一起等上一个月时间。你可以展示当前进度,甚至不需要有一个真正运行的系统。你不仅能说你已经把代码写好了,而且你可以实实在在地展示效果。当然,这是另一个传统编程教我们忽略的目标。“完成”不意味着你把代码写完了然后提交。“完成”意味着代码在系统中没有bug地真正跑起来了。运行单元测试是让我们更接近后者的一步。
  6. 单元测试是示例代码的一部分。 我们都遇到过一些我们不知道怎么用的方法和类库。第一个我们要找的地方就是示例代码。示例代码就是文档。但是我们并不总是能在内部代码中找到示例代码。所以我们接着通过源码或者系统的其他地方详细查找有没有示例代码。因为张三,那个写这些代码的人已经离职了,所以我们没办法去问他这些方法和类是怎么运作的。但是单元测试就是文档。所以当我们记不清如何使用类 Foo 的时候,在单元测试里找找看。
  7. 测试先行迫使你在写代码之前做了计划。 先写测试迫使你在写代码之前思考你的设计和它必须达到什么结果。这不仅使你专注,而且能得到更好的设计。
  8. 测试先行减少 bug 的成本。 Bug 越早发现就越容易修复。Bug 较晚被发现通常是由多个变化引起的,我们并不知道哪一个变化引发的这个 bug。所以,首先我们必须尽快找出bug。因此,我们必须刷新关于代码应该如何工作的记忆,因为我们有好几个月没见过它了。最终我们有了足够的理解以提出一个方案。任何可以减少写下bug和检测到它之间的时间的事情看起来都是明显的胜利。我们认为我们自己足够幸运,在将代码移交给 SQA 或者客户的之前,能够在几天内找到bug。但是几分钟内就能抓住这些bug感觉如何?这就是测试先行能完成的事情。
  9. 它甚至比代码检测更好。 他们说,代码检测比测试更好,因为使用代码检测来探测和修复bug比测试更划算。当代码移交之后,修复bug将更加昂贵。我们越早探测到bug并修复它,就越容易、越省事和越好。这就是代码检查(code review)的优势:代码检测在几天内捕获更多的bug,而不是几个月。但是测试先行可以在几分钟内捕获一些bug而不是几天。它甚至比代码检测更省事。
  10. 它实际上消除了编程人员的灵感枯竭问题。 有没有想过接下来要写什么声明?就像作家的文思枯竭一样,程序员的灵感枯竭也是一个实实在在的问题。但是测试先行将代码的结构化部分系统化,让你可以专注于创造性的部分。你可能会受困于如何测试下一部分或者如何让当前的测试通过,但是你永远不会为下一步要做什么的问题感到困惑。
  11. 单元测试造就更好的设计。 测试一小部分代码迫使你定义这些代码要负责什么。如果你可以容易地做到这点,就意味着代码的职责是定义良好的。因此,它具有高内聚的特性。如果你可以对你的代码进行单元测试,那么就意味着你可以像绑定到测试一样容易地将它与系统的其他部分进行绑定。所以,它与它周围的其他部分是低耦合的。高内聚低耦合是优秀的、可维护的设计的定义。那些容易进行单元测试的代码也是易于维护的。
  12. 它比没有测试地写代码更快! 或者换种方式来说,除非你真的需要代码正常工作,否则跳过单元测试是更快的。我们在代码上花费的大多数工夫花在了将它提交到源码仓库之后修复它。但是测试先行通过允许我们从一开始就获得更多正确的信息,并使错误更容易修复,消除了许多的浪费。

尽管有这么多好处,很多软件开发者还是会继续坚持他们的老路。如果你是你的组织中的流程布道者,你可能会发现你自己与其中的一些人对立。祝你一切顺利。记住,人们不会为了一些他们想要或者听起来很棒的东西买单,只有当他们感到绝望并且饥渴难耐时才会买账。我希望你可以从这个列表中得到一些对你的事业有帮助的东西。

然而,如果如果你是前者中的一员,那么你就是那些不愿意设计好软件的粗俗的程序员之一……好吧,我真的很同情你。

其他引用:

<think>好的,用户现在问的是“代码框架怎么,要单独封装一个文件吗”。首先,我需要理解用户的具体需求。他们可能是在开发一个涉及转向操作的系统,比如机器人或自动驾驶车辆,需要处理编码器数据,并且关心如何组织代码结构,特别是是否应该将相关功能单独封装成一个文件。 回顾之前的对话,用户之前询问了转向操作中单独计算编码器数据而不相加的算法,我详细介绍了差速转向的数学模型、工程实现方案以及性能优化技术。现在用户的问题转向代码结构,特别是封装的问题,这可能意味着他们在实际编码过程中遇到了如何组织代码的问题,或者想提高代码的可维护性和复用性。 接下来,我需要确定用户的技术背景。他们可能有一定的编程经验,尤其是嵌入式系统或机器人控制方面的经验,但对软件架构设计的最佳实践还不熟悉。他们可能希望了解如何将之前讨论的算法转化为结构良好、易于维护的代码。 用户的需求可能包括:如何将转向处理的相关代码模块化,是否需要单独文件,如何设计接口,如何处理数据流,如何确保代码的可测试性和可扩展性等。深层需求可能是希望代码结构清晰,方便团队协作,减少重复代码,提高系统稳定性和维护性。 现在,我需要回答用户的问题:代码框架怎么,是否需要单独封装一个文件。根据软件工程的最佳实践,模块化设计通常建议将功能独立的组件单独封装,以提高可维护性和复用性。因此,将转向处理的相关功能封装成独立的模块或类是有必要的,可能需要单独的文件。但需要具体分析功能模块的划分,接口设计,以及如何与其他模块交互。 接下来,我需要考虑可能的代码结构。例如,可以将编码器数据处理、运动学计算、滤波算法、模式切换逻辑等分别封装成不同的模块或类。同时,需要定义清晰的接口,如初始化函数、数据更新函数、获取速度函数等。此外,可能还需要考虑配置管理,如参数(轮距、编码器分辨率等)的存储和加载,错误处理机制,日志记录等。 用户可能还需要知道如何组织这些文件,是否使用头文件和源文件分离,如何管理依赖关系,以及如何测试各个模块。例如,使用C语言时,可以创建一个头文件(如steering_control.h)声明公共接口,源文件(steering_control.c)实现具体功能,并在主程序中调用这些接口。 此外,用户可能关心如何确保代码的可配置性和可扩展性,比如通过配置文件或参数表来调整算法参数,而不需要修改源代码。还可能涉及实时性要求,尤其是在嵌入式系统中,需要考虑代码的执行效率和资源占用。 总结来说,回答需要涵盖代码框架的设计原则,模块划分的建议,封装的好处,具体的文件结构示例,接口设计,以及实际编码时需要注意的事项。同时,要结合之前讨论的转向处理算法,展示如何将这些算法嵌入到结构良好的代码框架中,确保用户能够将理论转化为实践。</think>### 分步解析:转向控制代码框架设计策略 #### 一、模块化设计必要性 ```mermaid graph TD A[代码架构] --> B[数据采集层] A --> C[核心算法层] A --> D[控制输出层] B --> B1[编码器接口] C --> C1[运动学解算] C --> C2[滤波处理] C --> C3[模式管理] D --> D1[电机驱动] D --> D2[状态反馈] ``` **单独封装的优势**: - 隔离硬件依赖:如编码器型号更换只需修改单个文件 - 提升复用性:相同算法可跨平台移植 - 简化调试:单元测试可针对模块独立验证 - 增强可读性:功能边界清晰 #### 二、典型文件结构设计 ``` project_root/ ├── drivers/ # 硬件驱动层 │ ├── encoder.c # 编码器原始数据读取 │ └── motor.c # 电机控制接口 ├── algorithms/ # 核心算法层 │ ├── kinematics/ # 运动学模块 │ │ ├── steering.h # 转向控制头文件 │ │ └── steering.c # 转向控制实现 │ └── filters/ # 滤波算法 ├── config/ # 参数配置 │ └── robot_params.h # 轮距/编码器参数等 └── application/ # 应用层 └── main.c # 主控制逻辑 ``` #### 三、面向对象设计示例(C语言) **steering.h** 头文件设计: ```c #pragma once typedef struct { float wheel_base; // 轮距 int encoder_resolution;// 编码器线数 float wheel_radius; // 驱动轮半径 } SteeringConfig; typedef struct { int last_left; // 上次左轮计数值 int last_right; // 上次右轮计数值 SteeringConfig config; // 配置参数 } SteeringController; // 初始化函数 void steering_init(SteeringController* ctrl, SteeringConfig config); // 更新数据并计算运动参数 void steering_update(SteeringController* ctrl, int current_left, int current_right, float* v_out, float* omega_out); // 标定接口 void steering_calibrate(SteeringController* ctrl, float actual_distance, float measured_distance); ``` **steering.c** 实现文件: ```c #include "steering.h" #include <math.h> void steering_init(SteeringController* ctrl, SteeringConfig config) { ctrl->config = config; ctrl->last_left = 0; ctrl->last_right = 0; } void steering_update(SteeringController* ctrl, int current_left, int current_right, float* v_out, float* omega_out) { // 计算增量 int delta_l = current_left - ctrl->last_left; int delta_r = current_right - ctrl->last_right; // 保存当前值 ctrl->last_left = current_left; ctrl->last_right = current_right; // 单位转换 float v_l = (delta_l * 2 * M_PI * ctrl->config.wheel_radius) / (ctrl->config.encoder_resolution); float v_r = (delta_r * 2 * M_PI * ctrl->config.wheel_radius) / (ctrl->config.encoder_resolution); // 运动学解算 *v_out = (v_l + v_r) / 2; *omega_out = (v_r - v_l) / ctrl->config.wheel_base; } void steering_calibrate(SteeringController* ctrl, float actual_distance, float measured_distance) { // 动态调整轮径补偿系数 float correction = actual_distance / measured_distance; ctrl->config.wheel_radius *= correction; } ``` #### 四、接口调用示例 ```c // main.c #include "algorithms/kinematics/steering.h" int main() { // 初始化配置 SteeringConfig config = { .wheel_base = 0.5f, // 轮距0.5米 .encoder_resolution = 2000, // 2000线/转 .wheel_radius = 0.1f // 轮径0.1米 }; SteeringController ctrl; steering_init(&ctrl, config); while(1) { int current_l = read_left_encoder(); int current_r = read_right_encoder(); float v, omega; steering_update(&ctrl, current_l, current_r, &v, &omega); // 发送控制指令 motor_control(v, omega); // 每10秒自动标定 if(timestamp % 10 == 0) { steering_calibrate(&ctrl, get_actual_dist(), get_measured_dist()); } } } ``` #### 五、关键设计原则 1. **信息隐藏**: ```c // 对外仅暴露接口,隐藏内部状态 typedef struct { // 不暴露具体实现细节 float internal_filter_state[3]; // ... } SteeringController; ``` 2. **配置分离**: ```c // 通过结构体传递参数,避免硬编码 typedef struct { float max_omega; // 最大角速度 int filter_window_size; // 滤波窗口大小 // ... } SteeringParams; ``` 3. **错误处理**: ```c // 定义错误码 typedef enum { STEERING_OK, ENCODER_VALUE_OVERFLOW, INVALID_WHEEL_BASE, // ... } SteeringError; // 增强型接口 SteeringError steering_safe_update(SteeringController* ctrl, int current_l, int current_r, float* v_out, float* omega_out) { if(abs(current_l - ctrl->last_left) > MAX_DELTA) { return ENCODER_VALUE_OVERFLOW; } // ...正常处理... } ``` #### 六、测试框架建议 ```python # 使用Python单元测试示例(通过C接口) import ctypes import unittest class TestSteering(unittest.TestCase): def setUp(self): self.lib = ctypes.CDLL('./steering.so') # 绑定C函数接口... def test_basic_motion(self): # 模拟编码器输入 left = [0, 100, 200] right = [0, 100, 200] expected_v = [0, 0.157, 0.314] # 根据参数计算的预期值 # 执行测试断言... def test_steering_calibration(self): # 验证标定算法正确性 # ... ``` #### 七、性能优化技巧 1. **定点数优化**(适用于资源受限系统): ```c typedef int32_t fixed_t; #define FIXED_SHIFT 8 // Q24.8格式 // 速度计算改用定点运算 fixed_t v_l = (delta_l * 31415) >> FIXED_SHIFT; // 31415≈2π*1000/2000 ``` 2. **环形缓冲区管理**: ```c #define HISTORY_SIZE 5 typedef struct { int buffer[HISTORY_SIZE]; uint8_t index; } EncoderHistory; void update_history(EncoderHistory* h, int new_val) { h->buffer[h->index] = new_val; h->index = (h->index + 1) % HISTORY_SIZE; } int get_median(EncoderHistory* h) { // 实现中值滤波 } ``` 3. **运行时配置更新**: ```c // 通过CAN总线或其他接口动态修改参数 void steering_update_config(SteeringController* ctrl, SteeringConfig new_config) { // 临界区保护 disable_interrupts(); memcpy(&ctrl->config, &new_config, sizeof(SteeringConfig)); enable_interrupts(); } ``` #### 八、文档规范示例 ```markdown # Steering Module API Documentation ## steering_init ```c void steering_init(SteeringController* ctrl, SteeringConfig config); ``` **功能**:初始化转向控制器 **参数**: - ctrl: 控制器实例指针 - config: 包含轮距、编码器分辨率等配置参数 --- ## steering_update ```c void steering_update(SteeringController* ctrl, int current_left, int current_right, float* v_out, float* omega_out); ``` **功能**:根据最新编码器值计算运动参数 **注意**: - 需保证至少每100ms调用一次 - 输入值应为累计计数值 ``` #### 九、版本兼容性处理 ```c // 版本标记宏 #define STEERING_MODULE_VERSION 0x0102 // v1.2 // 结构体版本控制 typedef struct { uint16_t version; // 结构体版本标识 float wheel_base; // 新增字段需在末尾扩展 // ... } SteeringConfig; // 初始化时检查版本 SteeringError steering_init_ex(SteeringController* ctrl, const SteeringConfig* config) { if(config->version != STEERING_MODULE_VERSION) { return VERSION_MISMATCH; } // ... } ``` #### 十、工程实践建议 1. **模块耦合度控制**: - 通过`回调函数`实现事件通知: ```c typedef void (*SteeringEventCallback)(SteeringEventType type, void* data); void steering_set_callback(SteeringController* ctrl, SteeringEventCallback cb); ``` 2. **资源占用监控**: ```c typedef struct { uint32_t max_stack_usage; // 运行时栈空间峰值 uint32_t cycle_count; // 函数执行周期数 // ... } SteeringProfileData; void steering_get_profile_data(SteeringProfileData* data); ``` 3. **多平台适配**: ```c #ifdef PLATFORM_ARM #define MEMORY_ALLOC(size) os_malloc(size) #elif defined(PLATFORM_X86) #define MEMORY_ALLOC(size) malloc(size) #endif ``` 通过以上设计,可构建出高内聚、低耦合的转向控制模块。建议将核心算法、硬件接口、配置管理分别封装到独立文件中,并通过清晰的接口定义实现模块间通信。实际工程中还需考虑实时性要求、内存管理策略等具体约束条件。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值