AI 写代码总是乱?吴恩达力推的测试驱动开发(TDD),能帮你兜底
最后总结一下测试驱动开发,编写测试用例的一些重要关键点。1)命名一定要见名知意,容易理解函数名和变量名应该像一个句子一样描述其意图,让人一看就能理解,这样 AI 也容易理解你这个测试是干嘛用的2)单一职责原则【gzh:和平本记】每个测试用例只测试一个具体的行为或场景。不要把多个检查混在一个测试里。3)使用 Arrange-Act-Assert 结构这是一种标准的测试模式,它清晰地分开了「准备数据」
为什么 AI 写代码总是乱?因为它没目标。
测试驱动开发(TDD) 就是给它戴上眼罩还能跑直线的护栏。
你有没有遇到过这种情况,你对 AI 说,「帮我写个登录功能」
结果,它给你加了个验证码功能,但你根本没打算要。或者它只支持密码登录,完全不管手机号、第三方登录。【gzh:和平本记】
更糟糕的是,你改了一个小 bug,却莫名其妙导致其他功能挂掉。
这就是 AI 编程的三大痛点:
1、模糊性
AI 天生就具有创造性和模糊性,你用自然语言描述一个需求,比如:帮我写一个登录功能。
AI 对这句话的理解可能有十几种方式。它可能会添加「记住我」功能,或者选择一个你并不喜欢的验证库,或者仅仅支持密码登录,不支持手机号等其他第三方登录。
这种不确定性是 AI 编程最大的挑战。
2、缺乏全局感知
AI 缺乏对整个代码库的全局理解。
你让它修复一个小 bug,它可能会在修复的同时,无意中破坏了几个其他不相关的功能。
这种「回归」问题在 AI 编程中非常常见,而且极难察觉。
3、调试成本高
传统的编程流程是:写代码 -> 手动测试 -> 发现 bug -> 人工调试。
而调试是整个编程环节中最耗费心智和时间的部分,有的 bug 你在调试过程中甚至能直接把你弄崩溃。
那有没有办法,把这匹野马拉回跑道?
答案是:测试驱动开发(TDD)。
测试驱动开发(TDD) 怎么解决这些问题?
TDD 的核心,就是先写测试,再写代码。这一步,彻底改变了你和 AI 的协作模式。
1、把模糊需求变成精确目标:
我们通过编写测试用例,将模糊的自然语言需求,转化为精确、可执行的机器语言规范。
1)没测试时
你想让 AI 帮你写一个用户登录注册功能,AI 随便发挥,最后是不是你想要的?看运气。
2)有测试时
我们先把测试写好:
当输入有效邮箱和密码时,数据库里必须多一条记录,并且函数返回一个包含用户ID的对象。
当密码少于8位时,必须抛出PasswordTooShortError
异常。
这样 AI 在写代码的时候就能明白他的目标是什么,测试用例可以将 AI 的创造力约束在一个确定的轨道上,迫使它精准地实现你想要的行为,而不是它自己想象的行为。
2、给你一张回归安全网:
一套全面的测试套件,就是一张「回归」安全网。
当你让 AI 进行任何修改(无论是添加新功能还是修复 bug)后,你只需运行一遍完整的测试。
如果某个不相关的测试突然失败了,你就立刻知道 AI 的这次修改产生了副作用。
你可以马上让 AI 撤销修改或用 git reset
,并告诉它:你的修改破坏了 test_user_profile_update
这个测试,请在不破坏它的前提下重新修复。
有了这张网,AI 再也不敢牵一发而动全身。【gzh:和平本记】
3、调试从此更轻松:
传统流程:写代码 → 手动测试 → 找 bug → 人工调试。
TDD 流程:写测试 → AI 写代码 → 跑测试 → 把失败日志丢给 AI。
AI 比人类更擅长读机器日志。你只需要做反馈提供者,不用再一行行 debug。
如何快速上手?
现在我们知道的测试驱动开发的好处,那怎么样去编写清晰、全面的测试用例呢?
1、核心原则
编写清晰、全面的测试用例,本质上就是在为 AI 设定一个明确、无歧义的目标和验收标准。
所以,编写测试用例的黄金法则是:只描述「做什么」 (What),不要规定「怎么做」 (How)。
1)做什么
当输入 X 时,系统应该产生输出 Y,并且其状态应该从 A 变为 B。
2)怎么做
不要在测试用例里告诉 AI 要用哪个算法、哪个设计模式。那是它的工作。
你的任务就是定义所有可能的行为场景和预期结果。
2、实际案例
我们以一个常见的需求为例:
创建一个用户注册函数
第1步:定义核心成功路径
首先,思考这个功能在最理想、最正常的情况下应该如何工作。
1)行为
一个新用户使用有效的邮箱和密码进行注册。
2)预期结果
-
数据库中应该新增一个用户记录。
-
新用户的邮箱和密码应该被正确保存。
-
函数应该返回成功状态或新创建的用户对象。
测试用例举例:
# 文件名: test_user_registration.py
def test_successful_registration_with_valid_credentials():
"""
测试:当提供有效的邮箱和密码时,用户应该能成功注册。
"""
# 1. 准备 (Arrange) - 定义输入数据
email = "test@example.com"
password = "a_strong_password123"
# 2. 执行 (Act) - 调用我们想让AI实现的函数
result, new_user = register_user(email, password)
# 3. 断言 (Assert) - 验证结果是否符合预期
assert result.is_success() is True # 检查返回状态
assert new_user is not None # 确保返回了用户对象
assert new_user.email == email # 检查邮箱是否正确
assert database.find_user_by_email(email) is not None # 确认用户已存入数据库
测试函数的名称 test_successful_registration_with_valid_credentials
要取的非常直白,让 AI 能直接从名字理解测试的意图。
通过断言(assert
)部分就可以知道验证结果是否符合预期。【gzh:和平本记】
第2步:定义已知的失败路径和边界情况
这一步你需要思考,在哪些情况下这个功能应该会失败?
这部分至关重要,因为它为 AI 提供了护栏,防止它写出有漏洞的代码。
场景1:无效的邮箱格式
1)行为
用户尝试用一个格式不正确的邮箱注册。
2)预期结果
注册应该失败,不能在数据库中创建用户,并返回一个明确的错误信息。
测试用例举例:
def test_registration_fails_with_invalid_email_format():
"""
测试:当邮箱格式无效时,注册应该失败。
"""
email = "not-an-email"
password = "a_strong_password123"
result, new_user = register_user(email, password)
assert result.is_success() is False # 必须失败
assert "Invalid email format" in result.error_message # 检查错误信息
assert database.find_user_by_email(email) is None # 确保数据库里没有创建用户
场景2:密码太短
1)行为
用户尝试使用一个过短的密码注册。
2)预期结果
注册失败,并提示密码太短。
测试用例举例:
def test_registration_fails_with_password_too_short():
"""
测试:当密码少于8个字符时,注册应该失败。
"""
email = "test@example.com"
password = "short" # 假设我们规定密码至少8位
result, new_user = register_user(email, password)
assert result.is_success() is False
assert "Password is too short" in result.error_message
场景3:邮箱已被注册
1)行为
用户尝试使用一个已经存在于数据库中的邮箱进行注册。
2)预期结果
注册失败,并提示邮箱已被使用。
def test_registration_fails_when_email_already_exists():
"""
测试:当邮箱已经被注册时,再次注册应该失败。
"""
# 准备:先在数据库里创建一个用户
existing_email = "existing@example.com"
database.create_user(email=existing_email, password="some_password")
# 执行:尝试用同一个邮箱再次注册
result, new_user = register_user(existing_email, "another_password")
assert result.is_success() is False
assert "Email already in use" in result.error_message
第3步:将测试用例交给 AI
现在,你已经有了一个清晰、全面的测试文件 test_user_registration.py。接下来,你与 AI 的互动就变得非常简单和高效:
1)提供上下文
将整个测试文件的代码提供给 AI。
2)提示词和 AI 交互
这是我的测试文件
test_user_registration.py
。
请在user_service.py
文件中编写register_user
函数的实现,确保它能够通过所有这些测试。这个函数需要与一个database
对象交互。
3)迭代与反馈
AI 会生成第一版代码,然后你把测试运行起来,这时候很有可能会有几个测试失败。
你将测试失败的完整输出日志直接复制粘贴给 AI,说:这个测试失败了,这是日志,请修复它。
AI 就会根据错误日志自动去修复代码问题,然后重复这个过程,直到所有的测试全部通过。
这样你的用户注册功能就算完整开发好了,就算后面再开发其他功能的时候不小心修改了这部分的代码,你只要跑一遍测试用例,就知道这部分功能有没有问题。
关键技巧总结
最后总结一下测试驱动开发,编写测试用例的一些重要关键点。
1)命名一定要见名知意,容易理解
函数名和变量名应该像一个句子一样描述其意图,让人一看就能理解,这样 AI 也容易理解你这个测试是干嘛用的
2)单一职责原则【gzh:和平本记】
每个测试用例只测试一个具体的行为或场景。不要把多个检查混在一个测试里。
3)使用 Arrange-Act-Assert 结构
这是一种标准的测试模式,它清晰地分开了「准备数据」、「执行操作」和「验证结果」,AI 非常擅长理解这种结构化的逻辑。
Arrange(准备):为你的测试设置好所有的「前提条件」和「输入数据」。
Act(执行):执行你真正想要测试的那个核心动作或函数。
Assert(断言):验证执行步骤的结果是否符合你的预期。
使用AAA模式的清晰测试案例:
def test_successful_login_returns_auth_token():
# Arrange (准备)
email = "test@test.com"
password = "password123"
# 假设用户已经存在于数据库中
create_user_in_database(email, password)
# Act (执行)
auth_token = login_service.login(email, password)
# Assert (断言)
assert auth_token is not None
assert isinstance(auth_token, str)
assert len(auth_token) == 32 # 假设token是32位
4)断言要精确
不要只用 assert True
。要断言具体的值、状态变化、或特定的错误信息。
举例,一个assert True
的不精确断言
定义了一个用户注册的函数,这个函数能够成功执行,但是这个函数并没有去操作数据库,返回的对象也是空的。
现在我们编写一个测试,使用不精确断言:
这样,最后断言的结果是成功的,测试通过。
但其实代码功能是不准确的,因为它压根没有去操作数据库,用户数据根本没有被保存。
我们预期的是他要去操作数据库保存用户数据。
使用精确断言:
你要检查函数的返回值或对象的属性是否是你期望的那个确切的值。
这样 AI就知道了,它不仅要创建用户,还必须正确地设置用户的 email
和 is_active
属性。
5)覆盖边界和失败
你思考的失败场景越全面,AI 生成的代码就越健壮。【gzh:和平本记】
总结来说, TDD 与 AI 协作,是用确定性来驾驭不确定性。它就像给一匹强大的野马套上了缰绳和马鞍,让你能够引导它的力量,去往你想去的方向,而不是被它带着到处乱跑。
更多推荐
所有评论(0)