MarioSMS
测试 OTP 与短信验证:开发者手册

测试 OTP 与短信验证:开发者手册

作者: MarioSMS team · · 阅读约 122 分钟

目录
  1. 点击“发送验证码”到短信送达之间,究竟发生了什么?
  2. 如何为手机号验证设计测试矩阵?
  3. 如何为单次测试拿到一个真实的手机号?
  4. 如何把租用的号码接入自动化测试?
  5. Python、Node.js、Go 和 PHP 分别需要哪些代码示例?
  6. 不同服务的测试差异在哪里?
  7. 国家和运营商的差异如何改变测试方案?
  8. 每套 OTP 测试都应该覆盖哪些失败场景?
  9. 短信一直收不到,该怎么排查?
  10. 如何在不烧掉预算的前提下对 OTP 流程做压力测试?
  11. 如何在生产环境中监控 OTP 送达情况?
  12. 如何让 OTP 测试在 CI 中保持稳定?
  13. 团队该如何共享号码、权限和预算?
  14. 验证测试涉及哪些隐私与法律边界?
  15. 关于 OTP 与短信验证测试的常见问题
  16. 上线短信验证前,先走一遍发布前检查清单

手机号验证的端到端测试,指的是一次完整跑通的流程:从注册表单开始,到拿到会话令牌结束,中间的短信环节走的是真实手机的接收路径。其他做法(模拟的服务商、写死的 000000、打桩的 webhook)只能测你自己的代码,测不到线上真正会出问题的那条投递链路。

一名开发者在手机和笔记本电脑上测试OTP流程

六步速览

  1. 只锁定一个目标,范围收窄。测一个服务、一个国家、一条运营商路由。想测”短信整体情况”,得到的结果没法落地。
  2. 搞到一个号码。可以是你自己持有的固定测试 SIM 卡、服务商的沙盒号码,或者为单次接码租用的号码。
  3. 提交表单之前先开启接码。发送方触发时号码必须已经上线并处于监听状态,晚五秒都不行。
  4. 在你的应用里提交手机号,并记录该请求的时间戳。
  5. 轮询查收短信,直到验证码到达或接码窗口关闭。记录时间差,精确到秒。
  6. 把验证码回填到你的验证接口,对会话做断言,然后关闭接码,释放号码。

团队最常跳过的就是第 3 步和第 6 步。跳过第 3 步,测试会变得不稳定:本地通过,CI 里挂掉。跳过第 6 步,接码会一直挂着,既花钱,又占着号码让下一轮跑不了。

用于冒烟测试的最便宜市场

国家起价服务数量库存号码数
USA$0.0120079,399,375
Germany$0.01144101,118,633
United Kingdom$0.02291389,379,749
France$0.02153131,996,906
Portugal$0.02143140,112,396
Uzbekistan$0.023819,318,261
Italy$0.03149412,533,735
Austria$0.04126183,770,041

三种测试号码来源对比

来源单次成本准备耗时真实运营商路径适用场景
抽屉里的实体 SIM 卡套餐费用,外加一个人去看手机每个号码几小时到几天手动冒烟测试,一两个国家
服务商沙盒号或魔法号码免费几分钟否,短信根本不会发出单元测试,自有逻辑的 CI 跑批
租用号码,单次接码$0.04 起,随服务和国家浮动几分钟,通过 API 或 App 完成投递验证,开拓新国家,上线前检查

你的测试套件里有 90% 是在对状态机做断言:限流、验证码过期、重试计数、错误码锁定。这部分用沙盒号码是对的。它们毫秒级跑完,且零成本。只要是在测你自己的分支逻辑,就都用它。

实体 SIM 卡适合的场景是:你需要同一个号码跨周持续存在,比如一个长期保持登录状态、用作回归测试固定装置的账号。它的成本是人力:得有人守着手机。

什么时候只能用真实租用号码

有四种情况是模拟方案回答不了的:

  • 你要在一个从未发送过的国家上线。你得知道自己的发送方 ID 能不能挺过进入印度美国的路由,短信正文是完整送达还是被截断。
  • 某个服务会过滤 VoIP 号码。你的测试号码必须是该服务认可的运营商下的真实手机号。
  • 你改了短信模板。有些发送方会重写或拦截含 URL 的正文,只有真实投递才能看到最终文本。
  • 你需要一个和个人号码分开的 QA 二号账号,出于隐私卫生的考虑,而不是为了绕开什么规则。

MarioSMS 提供真实手机号租用,一次用于一条验证,$0.04 起,价格随服务和国家浮动,覆盖 35 多个国家和数百项服务。验证码会出现在网页控制台或 App 里,通常一分钟内到达。每个号码都带倒计时,如果超时仍未收到短信,接码会自动取消,费用自动退回你的余额。也就是说,一次投递失败除了留下数据,不会让你多花一分钱。

一次通过的测试到底证明了什么

要把范围说清楚。用租用号码跑出绿灯,只证明五件事,多一件都没有:

  1. 你的应用接受了该国家的这种号码格式。
  2. 你的发送方针对这次请求确实发出了一条消息。
  3. 在你所测量的窗口内,这条路由把正文送到了一台真实手机上。
  4. 正文里的验证码和你后端生成的验证码一致。
  5. 你的验证接口为该验证码签发了会话。

它不能证明同一国家另一家运营商的投递情况,不能证明换一个发送方 ID 的情况,也不能证明四小时后高峰时段走同一条路由的情况。投递按路由来看是概率性的,一次通过只是一个样本。每次跑测都要记录秒数,而不只是记通过或失败。上周 8 秒通过、今天 47 秒通过的测试,在它真正变红之前就已经在告诉你一些东西了,而这条趋势线,正是你开工单时能拿给服务商看的东西。

点击“发送验证码”到短信送达之间,究竟发生了什么?

一个只断言“验证码出现了”的验证测试,一次性覆盖了五个彼此独立的系统。它失败时,你根本不知道是哪一环出了问题。把整条链路拆成组件,你才清楚断言该放在哪里、哪个超时属于哪一跳。

每次测试运行一个号码的成本

服务最便宜的国家价格库存号码数
TelegramCanada$0.51422,891
WhatsappUnited Kingdom$0.90197,726
GoogleUzbekistan$0.109,620
DiscordUnited Kingdom$0.04308,494
UberPortugal$0.0218,704
AmazonUzbekistan$0.0212,015

数据来源:MarioSMS 目录,2026-09-10。价格为单个号码价格,库存数已注明。

从客户端到服务商的请求路径

点击“发送验证码”会向后端发起一次 POST 请求,通常是 /auth/phone/start,携带 E.164 格式的号码。在任何短信产生之前,你的后端会做四件事。

单次验证每个服务的成本

服务最便宜的国家价格
TelegramCanada$0.51
WhatsappUnited Kingdom$0.90
GoogleUzbekistan$0.10
DiscordUnited Kingdom$0.04
InstagramSweden$0.06
TikTokUnited Kingdom$0.05
FacebookUnited Kingdom$0.06
OpenAIUnited Kingdom$0.06
  1. 号码规范化与校验。美国号码若输入为 (415) 555-0100,会转成 +14155550100。非法输入在这一步就以 400 结束,不产生任何短信费用。
  2. 检查频率限制。按号码、按 IP、按设备指纹、按账号。多数团队至少会跑三个不同时间窗口的计数器。
  3. 生成并存储验证码(见下文)。
  4. 调用短信服务商的 REST API,传入号码、短信内容和发送方配置,然后向客户端返回 200,并附上可重发的等待时间。

服务商调用是第一个“响应结果不等于你真正关心的结果”的环节。服务商返回 201 Created,意味着消息被接收进了它的队列,而不是某台手机收到了什么。在“已接收”和“已送达”之间,还隔着聚合商、运营商网关和手机本身。任何把服务商的 2xx 当作送达证明的测试,测的其实是你的 HTTP 客户端。

验证码如何生成、哈希与存储

多数实现会用密码学安全的随机源生成 4 到 8 位数字,而不是 Math.random()。写入存储的记录通常包含号码、验证码的哈希值、签发时间戳、过期时间、尝试次数计数器和一个状态字段。

存哈希,不存明文。验证时重新计算用户输入的哈希再做比对。这一点对你的测试很关键:它意味着你无法在集成测试里从数据库读出预期的验证码,而这恰恰是团队转向使用真实号码接收真实短信的原因。MarioSMS 的租用号码会以用户实际收到的方式把验证码送达,在应用内或控制台中呈现,通常一分钟内到达,同时激活计时器正在走。

过期窗口是一个产品决策,常见为 5 分钟或 10 分钟。尝试次数计数器一般在连续输错 3 到 5 次后锁定。只要你留出时钟注入点,这两者都可以在不发送任何短信的情况下测试。

运营商路由、发送方 ID 与送达回执

服务商接收消息后会选择一条路由。路由是经由一个或多个聚合商通往目标运营商的路径,并携带一个发送方身份:长号码、短号码、字母数字发送方 ID,或美国的 10DLC 已注册号码。发送方类型并非只是外观问题。有些国家屏蔽字母数字 ID,有些运营商会降低未注册流量的优先级,还有些会针对看起来像营销模板的短信内容做过滤。

随后服务商会通过 webhook 向你的回调 URL 发送送达回执(DLR)。常见状态:

DLR 状态含义你的测试该怎么做
queued已接收,尚未发往运营商继续等待,不要断言失败
sent已交给运营商开始计送达时长
delivered运营商确认手机已收到与你的轮询结果做比对
undelivered运营商拒收或丢弃记录错误码,明确报错
failed服务商侧失败换一条路由重试并告警

DLR 并不总是诚实的。有些运营商在消息交到自家内部存储时就上报 delivered,所以“收到 delivered 回执,收件箱却没有验证码”是真实且常见的情况,值得为它单独写一个测试用例。

延迟从哪里来,正常的送达窗口是什么样

延迟是各跳累加出来的,而不是集中在某一处。

环节典型耗时
客户端到你的后端几十毫秒
验证码生成与写入存储个位数毫秒
你的后端到服务商 API100 到 500 毫秒
服务商队列到聚合商不定,负载高时变长
聚合商到运营商随路由和国家而变
运营商到手机不定,高峰时段最差

前三项归你所有,基本恒定。服务商 API 调用之后的一切都不在你的掌控之内,波动也都出在那里。实际情况是:健康的路由几秒内就能把验证码送到手机,拥堵的路由可能要几分钟,坏掉的路由则永远不会到达。测试超时时间应当根据号码激活窗口来设定,而不是靠猜;并且每次运行都记录耗时秒数,这样你拿到的是一个分布,而不是一次孤例。

为什么同一条验证码会重复到达或乱序

短信是存储转发系统,不同消息之间没有顺序保证。有三种机制会造成重复和乱序。

重试。聚合商在其窗口内没有收到确认,就会重发同一条消息。手机会收到两条一模一样的短信,有时相隔 30 秒,有时相隔 6 分钟。

用户重发。用户在第一条消息到达之前点了“重新发送”,于是有两个有效验证码同时在途。如果你的后端在重发时使旧验证码失效,那么先到的那条其实已经作废,而一个抓取“看到的第一条短信”的测试就会在系统完全正常的情况下失败。

路由切换。服务商在传输途中在聚合商之间做故障切换,可能把第二次尝试推上一条更快的路径,于是第二条消息反而先到手机。

你的轮询逻辑需要一条“哪条消息胜出”的规则。按接收时间排序,取最新的一条,并记录该号码在激活期间总共收到了多少条消息。如果一次单请求测试竟然看到了两条,说明你抓到了一个重试循环,值得在它影响生产流量之前查清楚。

如何为手机号验证设计测试矩阵?

测试矩阵把”测一下短信流程”变成一份有限的、带预期结果的用例清单。没有矩阵,团队会把同一条正常路径重测二十遍,然后在重发路径上带着 bug 上线。矩阵会强迫你列出每一个可控变量,为每个变量选定取值,并在运行任何用例之前写下应该发生什么。

MarioSMS目录,API与之一一对应

目录中的每个服务和国家同时也是一个API调用

值得变化的五个维度

五个维度覆盖了手机号验证中大多数真实缺陷:

在团队中共享号码、权限和预算

问题可行的答案
谁能花钱?一个账户,每个环境使用各自的API密钥,设置月度上限
谁能看到验证码?仪表盘和API;不要放到共享聊天中
如何避免冲突?每个测试worker使用一个激活,teardown阶段释放
如何审计?按密钥导出每月的激活历史记录
  1. 国家和号码类型。来自美国的虚拟手机号和来自印度的表现不同,送达时间和显示的发送方 ID 都不一样。号码类型同样重要,有些服务在注册环节对非 VoIP 号码VoIP 号码的处理方式不同。
  2. 输入格式。带加号的 E.164、带前导零的国内格式、空格、连字符、括号,以及重复输入两遍国家代码。每一种要么归一化成同一个值,要么给出清晰的报错。
  3. 时间点。在第 5 秒输入验证码、第 60 秒输入、过期前一秒输入、过期后一秒输入。时间类 bug 就藏在你声明的过期时间和实际 token TTL 之间的缝隙里。
  4. 尝试次数。首次验证码、重发、第三次重发,以及限流本该生效之后的那一次尝试。
  5. 验证码正确性。正确的码、错误的码、过期的码、上一次激活留下的码,以及从通知里粘贴过来带空白字符的码。

五个维度全部相乘会得到几百种组合。你不需要全跑。你要挑一个覆盖集,让每个维度的每个取值至少出现一次,然后再补上你怀疑会相互影响的特定组合,比如过期加重发。

正常路径、边界路径和滥用路径用例

把每个用例归入三个类别,优先级自然就清楚了。

正常路径用例证明功能可用。号码有效、验证码收到、在有效期内输入、账号创建成功。这里放三到四个用例,你支持的每个主要国家一个。

边界路径用例证明功能能优雅降级。短信根本没到、验证码过期后才到、用户中途换号、应用退到后台两分钟后再回来、提交和响应之间网络断开。用户真正卡住的地方就在这些场景里。

滥用路径用例证明你的限制真的生效。同一号码一分钟内请求六次、同一 IP 请求五十个不同号码、同一验证码换着数字提交 200 次。这些只能用你自己租的号码、针对你自己的系统测试,仅限 QA 用途,并遵守可接受使用规则

一份带预期结果的示例矩阵

编号国家输入格式时间点尝试次数验证码预期结果
H1美国E.16420 秒第 1 次正确账号创建,签发会话
H2印度国内格式,前导 030 秒第 1 次正确归一化为 E.164,账号创建
E1美国E.164一直没到第 1 次激活取消,余额退回,界面提供重试
E2美国E.164过期后 5 秒第 1 次正确拒绝并提示”验证码已过期”,提供重发
E3英国带空格的 E.16415 秒第 2 次第 1 个码拒绝,旧码已被重发作废
E4印度E.16445 秒第 1 次正确码 + 尾部空格自动去空格,通过
A1美国E.164不适用60 秒内第 6 次不适用发送被拦截,提示冷却剩余秒数
A2美国E.16410 秒第 1 次错误尝试 10 次触发尝试锁定,验证码作废,需重新请求

每一行都用可观察的方式写明一个预期结果。“正常”不是预期结果。“签发会话并返回 201”才是。

每个版本该跑多少用例

按节奏拆分,别每次都跑全量。

触发时机用例数需要真实短信大致成本
每个 pull request3 到 50$0
每晚构建10 到 153 到 5不到 $0.50
发版前25 到 358 到 12约 $1
认证逻辑或供应商变更后全量矩阵15 到 20几美元

按起价每个号码 $0.04 计算,一次完整的发版前测试比一杯咖啡还便宜,而且没收到短信的激活会自动退款。

哪些用例值得用真实短信,哪些可以打桩

凡是只测你自己代码的都打桩。格式归一化、TTL 计算、限流计数器、尝试锁定,全都可以对着假的供应商在毫秒级跑完。这些其实就是披着矩阵行外衣的单元测试。

当用例依赖你进程之外的东西时,才去租真实号码:真实的运营商送达、发送方 ID 的显示、你的正则对短信正文的解析、iOS 和 Android 上的自动填充行为,以及真实号码激活的时序。其中 E1 这一行尤其需要真实号码,因为测试”短信根本没到”的唯一诚实办法,就是请求一个号码然后让计时器跑完。

如何为单次测试拿到一个真实的手机号?

租一个号码做单次测试,是一个五步循环,从头到尾大约两分钟。下面的步骤以 MarioSMS 为例:一次租用的号码只用于一次验证,价格根据服务和国家不同,最低 $0.04 起。

一个能捕捉常见故障的测试矩阵

用例设置通过标准
正常流程全新号码,首次尝试验证码送达,账户创建成功
短信延迟在整个激活窗口期内轮询界面持续等待,而不是在30秒时报错
完全没有短信让窗口期过期提供重试选项,不扣费,不产生孤立账户
验证码错误输入四次错误数字出现锁定提示,尝试次数按号码计数
重新发送在同一号码上请求第二个验证码遵守限速规则,两个验证码都能通过或以最新的为准
号码重复使用已注册过的号码触发你自己的重复处理逻辑,而非崩溃
国家不匹配来自其他市场的号码显示用户真正需要看到的提示信息

在每一个上线市场都运行以上每个用例,而不仅仅是本国市场。

第 1 步,选择服务和国家

打开网页版 app.mariosms.com,或者 iOS、Android 应用,选定两件事:你要测试的目标服务,以及号码所属的国家。库存覆盖 35+ 个国家和数百种服务,所以大多数测试矩阵用一个账号就能跑完。

测试号码的隐私和政策限制

规则实际含义
只测试你自己的产品租用号码仅用于你自己的流程和QA账户
禁止冒充他人绝不能以他人名义验证账户
禁止规避封禁被封禁的账户应保持封禁状态
数据卫生不要在测试固件中保留收到的验证码或号码

此处以可接受使用政策为准:/acceptable-use/。

按你正在执行的矩阵行来选国家,别凭习惯选。美国号码印度号码在你自己的代码里表现并不一样:位数不同,你的库渲染出来的格式不同,短信正文里的发送方 ID 也不同。如果这一行写的是「IN,首次尝试,Android 自动填充开启」,那就租印度号码,而不是碰巧更便宜的美国号码。

确认之前显示的价格就是你实际支付的价格。把它连同行 ID 一起写进测试运行日志,这样以后谈预算时手里有真实数字,而不是估算。

第 2 步,租下号码并记下激活窗口

确认租用。号码出现时旁边会有一个计时器,这就是激活窗口。这个计时器代表号码归你所有、并持续监听该服务短信的时长。

记下开始时间。此刻有两个时钟在跑:激活窗口,以及你自己应用设定的 OTP 有效期。大多数让人困惑的测试结果,都来自这两个时钟对不上。如果你的应用 5 分钟就让验证码过期,而你花了 3 分钟手动把号码抄进表单,那你测的是自己的耐心,不是你的流程。

租号之前,先把待测应用停在手机号输入页面。租用应该是你粘贴前做的最后一件事。

第 3 步,把号码粘贴到待测应用里

从控制台复制号码,粘贴到手机号字段。此时有两个细节值得检查,因为它们能在短信还没出现之前就抓出真实的 bug:

  1. 你的输入框能否接受控制台给出的格式,也就是带国家代码、不含空格?
  2. 你的归一化处理产出的 E.164 字符串,和短信服务商看到的是否一致?

然后点击「发送验证码」并按下秒表;如果是脚本化运行,就记录时间戳。

第 4 步,在控制台或手机应用里读取验证码

验证码会出现在控制台或手机应用中,通常一分钟以内。根据你用的客户端,刷新页面或盯着激活记录那一行看。对于脚本化运行,REST API 返回的激活状态和控制台显示的一致,所以测试可以轮询,不必让人守着屏幕。

把验证码填进你的应用,走完整个流程。每次运行记录三件事:

字段示例值为什么重要
收到验证码用时14s你的送达 SLO 基准
完整短信正文“Your code is 481920. Do not share.”用于你的正则和自动填充测试
显示的发送方 ID短代码或字母数字会影响 iOS 上的自动填充行为

你的解析正则、iOS 自动填充域名提示、以及支持文档,都依赖短信的确切文本,而不只是那六位数字。

第 5 步,如果什么都没来,就让计时器走完

有时候短信压根不会来。那就是矩阵里的 E1 行,它是一个合理的测试结果,不是测试环境出了问题。别去动这个激活记录,让计时器自然走完。窗口关闭且没有短信时,激活会自动取消,费用会自动退回你的余额。

计时器走的时候,观察你自己的应用在做什么。重发按钮是否在你设计的间隔时间变为可点击?90 秒后屏幕是不是还在转圈?有没有任何提示告诉用户下一步该做什么?这些答案才是这次运行真正的价值。

退款机制对测试预算意味着什么

未送达自动退款,会改变你给整套测试做预算的方式。你只为收到的验证码付费,而不是为尝试次数付费。

一个 40 行的矩阵,其中 32 行成功送达、平均单价 $0.06,总共约 $1.92,而那 8 行本就设计为收不到短信的,花费 $0。按预期成功送达的数量来估算开销,再为真实抖动后的重试留一点余量。用银行卡或加密货币每个冲刺周期充值一次,而不是每次运行都充,并且整个过程都要符合可接受使用规范:针对你拥有或已获授权测试的服务做 QA 和 OTP 测试。

如何把租用的号码接入自动化测试?

一个 OTP 测试有两半,分别跑在不同的系统里。你的测试客户端驱动被测应用或 API,另一个独立的客户端则和号码服务商通信。只有当两半达成一致时,测试才算通过:应用接受了一个验证码,而这个码发送到的号码是测试在几秒前刚刚租用的。下面所有内容,都是为了让这两半保持同步,而不用在整个测试套件里到处撒 sleep。

一个正在等待验证短信的租用号码

激活窗口期就是你的测试应遵守的超时时间

请求、轮询、提取、断言循环

这个流程在任何语言里都是一样的。五个步骤,按顺序执行:

上线前检查清单

检查项完成标准
每个上线市场均已用真实号码测试验证码送达且账户创建成功
已测试过期路径窗口期耗尽后提供重试选项,不产生费用
已测试重试和锁定机制限制生效且提示文案清晰
已测试回退方案如提供语音或邮箱路径,确保其可用
监控已上线仪表盘按国家展示输入率和延迟
预算已设上限设置测试支出的每日限额
  1. 针对目标服务和国家请求一次激活。你会拿到一个激活 ID 和一个 E.164 格式的手机号。
  2. 把这个号码提交给被测应用,并触发发送验证码的操作。
  3. 轮询激活状态,直到出现短信正文或计时器到期。
  4. 用针对该服务的正则规则从短信文本中提取验证码。
  5. 提交验证码,对得到的会话或错误做断言,然后关闭这次激活。

第 1、3、5 步是服务商侧的调用。第 2、4 步是你自己的。把它们放在不同的模块里,这样服务商一变,只需要动一个文件。轮询这一半的可用实现可以在 PythonNode.js 指南里找到,Go 和 PHP 的循环写法也完全一致。

第 2 步的顺序很关键。先请求号码,再触发发送。如果你在号码激活存在之前就触发发送,这条短信没有落脚点,测试会因为一个和你代码毫无关系的原因而失败。

选择一个不浪费额度的轮询间隔

在 MarioSMS 上验证码通常一分钟内就会到达,所以有意义的轮询窗口是 60 到 180 秒,具体取决于服务。以 1 秒间隔轮询 120 秒,每次测试就是 120 次请求。跑 40 行测试矩阵,就是 4,800 次请求,只为了检测大约 32 条短信。

改成退避策略。一个可行的时间表:

已用时间间隔窗口内请求数
0-10s不轮询0
10-40s3s10
40-120s5s16
120s 到计时结束10s6 次或更少

这样每次测试大约 32 次请求,而不是 120 次,最坏情况下也只给一个通过的测试增加 5 秒延迟。前 10 秒实际上是空窗期,因为短信要先后经过发送方、聚合商和运营商,之后才可能被读取到。

当测试并行运行时,给每次 sleep 加上 200 到 500 毫秒的抖动。二十个 worker 卡在精确的 5 秒网格上轮询,会每隔 5 秒产生二十个同时发出的请求,在任何限流器眼里这都是一次突发流量。

安全地从短信文本中解析验证码

千万不要直接取正文里的第一段连续数字。真实短信里还有别的数字:短代码、年份、客服电话号码、退订编号。对 “Your code is 481920, valid for 10 minutes. Reply STOP to 40404” 做贪婪的 \d+ 匹配,可能返回 10 或者 40404。

改成基于长度和上下文来锚定:

  • 匹配固定长度的分组:6 位验证码的服务用 \b(\d{6})\b,4 位的用 \b(\d{4})\b。不要写一个正则同时兼顾两者。
  • 在你知道短信模板的情况下,优先使用带引导语的正则,引导语匹配不上时再回退到纯长度匹配。
  • 把每个服务的正则和验证码长度存在同一份 fixture 里,这样模板变了只需要改一行。
  • 解析失败时把完整正文打进日志。一个只报 “no match” 而不给正文的失败测试,凌晨两点根本没法用。
  • 当出现两个长度都正确但内容不同的候选时,直接判定失败。宁可大声报错,也不要提交错误的那个。

字母数字混合的验证码也是存在的。如果某个服务可能发送 A4K9QZ,你的正则需要的是字符类,而不是 \d。可以查看 OTP 术语条目,了解你可能遇到的各种格式。

处理收不到短信的情况,又不让测试挂死

每次激活都自带计时器。如果计时器到期前没有短信到达,激活会被取消,费用会自动退回你的余额,所以一次静默测试的成本是 $0。但你的测试仍然需要自己的截止时间。

把测试超时设得比激活计时器短,而不是更长。如果计时器是 20 分钟,就把轮询循环限制在 180 秒然后退出。等服务商侧过期,会把一个 3 分钟的测试套件拖成 20 分钟。

超时时要做三件事:显式取消激活;记录国家、服务和已用秒数;把结果标记为 no_delivery 而不是 failed。短信没到是一个投递信号,不是断言坏了。把两者都归为红色的测试套件,会失去发现某个国家突然静默的能力。

在两次运行之间清理状态

清理逻辑永远放在 finally 块里,包括断言在循环中途抛异常的情况。关闭或取消激活,别让任何东西一直挂着占用你的余额。

账号那一侧也需要清理。为一次验证租用的号码是一次性的,所以第 1 次运行创建的账号,在第 2 次运行租到另一个号码时就成了孤儿。要么在清理阶段通过你自己的 API 删除测试账号,要么给测试账号打上运行 ID 标签,然后定期批量清理。把号码、激活 ID 和账号 ID 一起存进本次运行的产物里,这样清理任务能同时找到两头。

绝不要缓存号码留到下次运行复用。租用只覆盖一次验证,而 fixture 里一个过期的号码会让测试基于错误的前提失败一整周,才有人去查。

Python、Node.js、Go 和 PHP 分别需要哪些代码示例?

四种语言,一套约定。如果你们组织里的每个客户端都暴露同样的三个方法,那么写过 Python 测试套件的测试人员,不用翻 API 文档就能读懂 Go 的套件。

MarioSMS中收到的一条验证码

验证码会同时出现在应用内和API返回中

四种语言共用的辅助接口

接口只定义一次,然后按语言分别实现。三个方法就覆盖了完整的租用生命周期:

从号码到收到验证码的一次测试激活

一次激活:号码、窗口期、验证码、释放

方法输入返回失败行为
rent(service, country)服务标识、ISO 国家代码激活 ID、E.164 格式的手机号缺货时抛异常,不扣费
wait_for_code(activation_id, timeout)激活 ID、秒数OTP 字符串超时抛异常,激活自动取消并退款
release(activation_id)激活 ID幂等,调用两次也安全

两条规则保证实现不会走样。rent 绝不会只返回号码而不返回激活 ID,因为没有 ID 的号码是释放不掉的。wait_for_code 轮询时使用调用方传入的截止时间,而不是写死在辅助函数里的常量,这样慢速的印度线路和快速的美国线路就能用同一份代码、配不同的预算。

退款机制决定了错误处理的写法。号码激活如果在时间窗口内没收到短信,会自行取消,费用退回你的余额,所以 wait_for_code 超时不花一分钱。也就是说,激进的超时设置成本极低。设 90 秒,别设 10 分钟。

Python:requests 加 pytest fixture

requests.Session 并挂载 HTTPAdapter,把连接复用和重试策略集中在一处管理。把辅助函数包进函数级作用域的 pytest fixture,yield 出号码,在 teardown 块里释放,这样即便断言失败,激活也照样归还。

@pytest.fixture
def rented_number(sms_client):
    act = sms_client.rent(service="telegram", country="us")
    try:
        yield act
    finally:
        sms_client.release(act.id)

轮询用 time.monotonic() 而不是 time.time(),否则运行途中系统时钟一调整,时间窗口就会被拉长或截断。完整的请求和响应结构见 Python 指南

Node.js:fetch 加 Playwright

Node 18 及以上自带 fetchAbortSignal.timeout(),不需要额外的 HTTP 库。每次轮询请求都带上 signal: AbortSignal.timeout(10_000),这样socket 卡住时 10 秒就会失败,而不会一直挂到整个测试套件超时。

在 Playwright 里,把租用放进 worker 级或 test 级的 fixture,清理逻辑注册在 fixture 的 teardown 里,而不是 test.afterEach。Playwright 的 fixture 按逆序拆解,所以在浏览器上下文之前创建的租用,会在上下文关闭之后才释放,而这正是应用仍持有会话时你想要的顺序。Node.js 指南演示了如何用 setTimeout promise 写轮询循环,而不是忙等的 while 循环。

Go:用 context 截止时间

Go 把超时约定摆在明面上。每个辅助方法都以 ctx context.Context 作为第一个参数,并把它传给 http.NewRequestWithContext。预算由调用方设定:

ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
code, err := client.WaitForCode(ctx, act.ID)

轮询间隔用 time.NewTicker(3 * time.Second),并用 select 同时监听 ticker.Cctx.Done()。在 defer 中释放时要用一个全新的 context,因为延迟调用执行时,父 context 早就失效了。这个小错误在大多数初版代码里都会导致释放悄无声息地失败。

PHP:Guzzle 加 PHPUnit

Guzzle 的 handler stack 支持中间件,把重试逻辑放在那里写一次,不用在每个调用点重复。在客户端配置里分别设置 timeoutconnect_timeout,因为 DNS 卡顿和响应缓慢需要不同的上限。

在 PHPUnit 里,不要用 setUp 去租号码。在测试方法内部租用,并在 finally 块里释放;或者用 data provider 提供国家代码,让同一个测试主体覆盖美国和其他市场,无需重复代码。

各语言中的重试、退避和错误分类

三类错误,只有一类值得重试:

  1. 临时性错误(HTTP 429、502、503、连接重置)。用指数退避重试,起始 1 秒,上限 8 秒,最多 4 次。
  2. 终结性错误(该服务和国家组合缺货、余额不足、API key 无效)。立即失败,并在错误信息里打印出服务和国家。
  3. 空轮询(还没收到短信)。这不是错误。继续轮询,直到调用方设定的截止时间。

常见的 bug 是把空轮询当成临时性错误,塞进退避循环。退避会把轮询间隔推到 8 秒,于是第 41 秒到达的验证码要到第 48 秒才被读到,而那时断言早就跑完了。轮询保持 3 秒的固定节奏,退避只留给 HTTP 失败。

不同服务的测试差异在哪里?

一套测试在某个服务上跑通,却在另外四个服务上失败,通常说明里面藏着写死的假设:验证码是六位、短信 20 秒内到达、短信正文只有一行。这些假设在某些地方成立,在另一些地方就不成立。下面按类别列出那些会让粗糙的测试翻车的行为。

用于负载测试、库存号码数量最多的市场

国家库存号码数服务数量起价
United Kingdom37,001,905189$0.02
Italy24,353,864118$0.03
Austria12,315,289105$0.04
Canada11,766,48137$0.05
Indonesia8,380,97645$0.04
USA2,954,215139$0.01
Australia2,255,940109$0.04
Germany2,147,090123$0.01

数据来源:MarioSMS 目录,2026-09-10。价格为单个号码价格,库存数已注明。

即时通讯类应用及其应用内验证码格式

即时通讯类应用在首次注册时会通过短信下发验证码,但其中不少还会在账号已经登录在另一台设备上时,通过自家的应用内通道下发同一个验证码。你的测试账号不会有那台设备,所以只按短信路径来规划,并且要预留应用延迟一段时间后回落到短信的时间。

在应用中选择测试市场

选择你要测试的市场,行内显示库存和价格

这里有两个解析上的坑。第一,验证码往往夹在一个较长的句子里,后面还跟着一段安全提示,因此”取正文中第一串数字”这种粗糙的正则可能匹配到客服电话号码上。要把正则锚定到该发送方所使用的那个标签词上。第二,有些即时通讯类应用会为 Android SMS Retriever 附上一段隐藏的 app hash 后缀,也就是在验证码后面多出一个 11 个字符的令牌。比对之前要先把它剥掉。

即时通讯类应用的重新发送通常在首次发送后被限制一分钟以上,而且这个限制是按手机号计的,不是按会话计的。连点三次”重新发送”的重试循环,只会把自己的等待时间拉得更长。

社交与交易平台的注册流程

社交和交易平台的注册流程通常把手机验证放在邮箱验证、验证码图形挑战或资料填写之后,也就是说短信并不是你的测试要做的第一件事。请把前置步骤的耗时和 短信验证 断言分开计算,否则你为整个流程设定的超时,会在验证码请求还没发出去之前就用完了。

交易平台还有第二个坑。其中不少会在首次发布商品或首次发消息时再次要求验证,而不是在注册时。如果你的测试只覆盖注册,那第二道挑战就是没被测到的。把它写成一个独立的测试,并使用独立的号码,不要复用注册时的号码,因为第一次运行的那个激活已经关闭了。

网约车与配送类应用的短时间窗口

网约车和配送类应用是常见服务里验证码有效期最短的一类,往往只有 60 到 120 秒,而且其中有几家在输入框填满的瞬间就自动提交。这两点叠加起来,会狠狠惩罚轮询过慢的实现。如果你的轮询每次检查之间睡 5 秒,而司机端应用 60 秒就让验证码过期,那你一次睡眠就烧掉了窗口的 8%。

自动提交还会打破经典的 Selenium 写法:先往输入框里输入,再点击”提交”,因为这个点击落在页面跳转之后。正确做法是输入验证码,然后等待成功元素或”提交”按钮,哪个先出现就用哪个。

金融与钱包类应用更严格的校验

金融和钱包类应用对号码的校验最严。它们往往会拒绝已知的 VoIP 号码,要求号码归属国与账号所属国一致,并且在三次验证码错误后就锁定账号,而不是五次。有些还会把验证码绑定到设备指纹上,因此在一台机器上取到的验证码拿到另一台机器上输入会失败,报出的错误又很笼统,看上去就像验证码输错了。

测试这类服务时,请使用与账号资料同一国家的 非 VoIP 号码,并把账号锁定当作一等公民的预期结果,为它单独写一个测试,而不是等它在 CI 里出事之后才发现。

倾向于回落到语音来电的服务

有些服务在一两次短信失败之后会切换到语音来电,还有少数在某些国家默认就走语音。只读短信的测试看到的是一个空收件箱,于是报出一个”下发失败”,而实际上那是一次通道切换。

有两种应对方式。一是在界面上识别这种回落(按钮文案会变成”给我打电话”),把它作为一个合法分支来断言。二是当已知某个服务在某个国家倾向于走语音时,直接把这个组合从短信测试矩阵中剔除,而不是任由它每晚失败。在 MarioSMS 上,如果在激活窗口内没有短信到达,激活会自动取消,费用会自动退回你的余额,所以一个倾向语音的服务只会花掉你的时间,不会花掉你的钱。

验证码长度与时间窗口的服务对照表

验证码长度、有效期和重新发送行为因服务而异,而且会随时间变化。请在你自己的环境里实测,而不是相信一份静态清单,并把测得的结果记录成下面这样的表格。

类别常见验证码长度验证有效期重发限制测试要点
即时通讯类应用6 位数分钟60 秒以上剥掉 app hash 后缀
社交网络5 到 6 位数分钟30 到 60 秒发送前有图形验证码
交易平台4 到 6 位数分钟60 秒首次发布商品时有第二道挑战
网约车 / 配送4 到 6 位60 到 120 秒30 秒填满即自动提交
金融 / 钱包6 到 8 位很短60 秒以上国家需匹配,3 次错误即锁定
开发者工具6 位数分钟30 秒除短信外通常还支持 TOTP

有效期那一列,请填入你自己实跑测出的秒数。每行有两个数字就足以确定轮询的截止时间:观测到的最短有效期,以及观测到的最长送达时间。如果两者之间的差距不足 10 秒,那这个服务就需要比测试套件里其他服务更快的轮询频率。

国家和运营商的差异如何改变测试方案?

一条验证流程并不是单一系统。它由你的代码、一家消息服务商、一条国际路由、一家目标运营商和一部手机的本地化设置组成,而当你更换国家代码时,这五者中有四个都会变。同一家服务商,4 秒就能把验证码送达美国号码的构建,发往印度号码时可能 90 秒都超时,而你这边不会报任何错。

用于租用号码并轮询验证码的API代码

号码格式、E.164 与本地拨号的怪癖

所有号码都用 E.164 存储(加号、国家代码、用户号码,不带空格和短横线),只在展示层做格式化。故障往往来自用户输入的内容和 E.164 所需格式之间的落差。

用于限制测试支出的余额界面

一个余额,一个统一的地方来限制测试套件的支出

  1. 前导的中继零。英国、德国和意大利的用户会输入 07911 123456。英国的 E.164 格式要求 +447911123456,但意大利手机号会保留首位数字,所以一刀切地去掉零会把号码改错。
  2. 国内号码长度不固定。有些国家的手机号根据运营商号段是 8 到 11 位,因此写死 length === 10 的校验会误拒有效号码。
  3. 国家代码冲突。+1 涵盖美国、加拿大以及 20 个左右的加勒比地区。把「仅限美国」的规则写成 startsWith('+1'),等于把它们全放行了。
  4. 粘贴带来的杂质。不间断空格、短破折号和 Unicode 数字会从密码管理器和通讯录里带进来。先做归一化,再做校验。

每个租用号码都要用三种输入形态测试:完整 E.164、选对国家的国内格式、以及选错国家的国内格式。第三种应当以一条可读的提示失败,而不是抛出 500。

发送方 ID 规则与字母数字发送方

From 字段在每个市场的监管方式都不同,而你的断言很可能依赖它。

市场类型实际收到的内容对测试的影响
允许字母数字发送方,无需注册你的品牌名回复退订的测试没有意义,发送方是单向的
字母数字发送方需预先注册已注册的 ID,否则消息被丢弃预发环境里未注册的 ID 会静默失败
禁止字母数字发送方长号码或短代码任何针对发送方文本的断言都会失效
动态路由选择每次发送的号码都不同永远不要断言某个确切的发送方值

从短信正文里解析验证码,绝不要从发送方解析。正则要写在消息文本上,并使用有边界的模式(\b\d{6}\b),对发送方只断言「存在且非空」。

运营商过滤与被拦截的内容

目标运营商会对短信内容做垃圾信息过滤,某些市场的过滤规则比其他市场严格得多。常见触发过滤的内容包括短链接、全大写单词、货币符号、多于一个 URL,以及未注册的发送方身份。结果通常是静默丢弃:服务商报告已受理,手机却始终没响。

准备两条测试:一条发送你线上使用的确切模板,另一条发送刻意设计成容易被过滤的变体。如果两条在 A 国都能收到,而在 B 国只有第一条能收到,那你发现的是模板问题,不是基础设施问题。消息形态的基础知识可参见 /glossary/sms-verification/。

挑选能覆盖大部分风险的五个国家

只要按不同维度而非市场规模来挑,五个就够了。

  1. 你用户量最大的国家,不管是哪个。
  2. 一个国内号码为 10 位、运营商过滤严格的国家,比如印度(/numbers/india/)。
  3. 一个美国以外的 +1 国家,用来抓出 startsWith('+1') 之类的 bug。
  4. 一个带前导中继零的欧盟国家,用来检验你的归一化逻辑。
  5. 一个你的服务商走冷门路由的国家,这样投递变慢能在用户反馈之前先暴露出来。

美国(/numbers/united-states/)通常落在第 1 位或第 3 位。每次发版前跑完整套,每次合并只跑用户量最大的那个国家。

把价格和库存当作规划输入

MarioSMS 的号码价格低至 $0.04 起,按服务和国家浮动,覆盖 35 多个国家和数百种服务,均有现货。这带来两个规划上的结论。第一,价格排序告诉你哪些国家可以放开量跑、哪些只适合抽样,所以把高频回归放在便宜的一端,把五国矩阵固定成每周一次的节奏。第二,库存是按服务、按国家分别计算的,写死单一国家的测试套件会在这个组合缺货时挂掉。运行时读取库存,并回退到列表中的下一个国家。

每个号码只处理一次验证,带有倒计时,如果没有收到短信,激活会自动取消,费用自动退回你的余额。因此投递失败的测试不花钱,这让在慢速市场做负面测试变得可以承受。

时区对投递和测试排期的影响

运营商队列在当地清醒时段最繁忙,某些市场在高峰期对营销类和交易类流量的限速方式也不一样。一个 09:00 UTC 的每夜任务,打到印度是下午,打到美国西海岸是凌晨 02:00,于是你的延迟数据描述的是两种完全不同的网络状况,取平均后毫无意义。

在每次测量的同时记录目标地的当地小时数。然后为每个国家安排两次运行,一次在当地 10:00,一次在当地 22:00,比较两者的 p95 投递时间。差距超过 15 秒,说明你的线上超时时间应该按繁忙时段来设定,而不是按空闲时段。

每套 OTP 测试都应该覆盖哪些失败场景?

一个验证流程大概有六个活动部件,而每一个出问题时从外面看都很正常。登录成功了,仪表盘加载出来了,测试也是绿的。下面这些失败场景,恰恰是只跑正常路径的测试抓不到的,所以每一条都需要针对失败本身写测试,而不是针对成功。

一个QA团队在多台设备上测试注册流程

后端接受了已过期的验证码

大多数团队会在配置里设一个 TTL(常见是 5 分钟或 10 分钟),然后就再也没验证过这个检查到底有没有生效。验证码落在一张带 expires_at 字段的表里,如果查询条件只有 code = ?user_id = ?,漏掉了时间戳,那么一个旧验证码就能永远用下去。

正在运行验证测试的持续集成流水线

写一个测试:先请求验证码,然后把时钟冻结或快进到 TTL 之后,再提交这个验证码。断言返回 HTTP 400 或 401,错误体为 code_expired。同时断言这条记录要么被删除、要么被标记,让第二次调用同样无法成功。如果你的测试框架控制不了服务端时间,可以直接插入一条 expires_at 已经是过去时间的记录,再通过公开接口提交它。

登录成功后验证码仍可重复使用

验证码应该是一次性的。常见的 bug 是 verify 处理函数直接返回了会话,却没有把验证码标记为已消费,于是同样的六位数字在接下来四分钟里能用三次。当这条短信留在通知历史或共享收件箱里时,这就成了问题。

在同一个测试里断言两次。第一次调用返回 200 和一个会话令牌。第二次用完全相同的入参调用,返回 400,且不返回令牌。然后断言第二次调用没有创建第二条会话记录,因为有些实现虽然拒绝了验证码,却仍然从缓存路径里签发了令牌。

没有尝试次数上限的暴力破解

六位数字是一百万种组合,听起来挺安全,直到你算一算攻击者实际需要多少次。在 10 分钟窗口内且没有次数上限的情况下,一个每秒 50 次请求的脚本远在过期之前就能跑完整个空间。修复办法是给每个验证码加一个尝试计数器,通常是 3 到 5 次,再加上按手机号维度的限流。

让测试循环提交错误验证码直到配置的上限,然后提交正确的验证码,断言它同样被拒绝。如果 5 次错误后锁定了,但第 6 次用正确验证码还能通过,那这个锁定毫无意义。

重发与验证之间的竞态

用户在第 29 秒点了重发,第一条短信在第 30 秒到达,然后他们输入了第一个验证码。这能不能通过,取决于重发是否会让旧验证码失效,还是两个验证码同时有效。两种策略都说得通,但测试必须钉死你选的是哪一种。

用两个线程并发地发出重发请求和第一个验证码的验证请求。断言结果有且只有一种。如果旧验证码仍然有效,就断言两者各自用自己的验证码都能成功,且从未签发第三个验证码。如果重发会让旧码失效,就断言第一个验证码返回 code_superseded。相关内容:租用号码上的号码激活窗口与你的应用 TTL 是各自独立结束的,所以一个跑得足够久、会同时撞上两者的测试,需要能区分这两类失败。

通过不同错误提示实现的枚举

注册返回「号码已被使用」,登录返回「未找到账号」。两种不同的文案,于是任何人拿着一份号码清单,都能把它们分成客户和非客户两堆。响应时间也会泄露同样的信息:已注册的路径要跑一次密码哈希,未注册的路径则立即返回。

断言已注册号码与未注册号码之间状态码相同、响应体逐字节一致。时间方面,每个分支跑 50 次请求,断言中位数差值小于 50 毫秒。未注册的一侧要用一个全新租用的号码来测,这样状态才是真正干净的;同时用美国和一个非美国的国家各测一遍,因为有些后端会按国家代码走不同分支。

关于 SIM 卡交换和号码回收的假设

运营商会在停机一段时间后回收号码,而 SIM 卡交换能在几分钟内把一个号码转到新设备上。如果你的应用把手机号当成永久身份,那么这两种情况都会把账号交到陌生人手里。

要测的是找回流程,而不是注册流程。断言仅凭一个已验证的号码、在没有第二因子的情况下无法重置密码;断言在已有账号上发生的重新验证事件会写入一条审计记录。断言更换账号绑定的号码会使活跃会话失效,并向原来的联系方式发送通知。

服务商静默失败却返回 200

短信网关接收了消息,返回 200 和一个消息 ID,然后在运营商那一端把它丢了。你的发送接口报告成功,你的测试也通过了,因为它只检查了 API 响应。这个缺口最后会以工单的形式浮现。

永远不要只对发送响应做断言。要对真实收件箱里实际读到的验证码做断言,而这正是租用号码能提供的。然后断言你的送达回执处理逻辑在 120 秒内记录了一个终态状态,并且回执缺失时会抛出异常,而不是被当成已送达写进去。

失败场景与断言对照表

失败场景测试中的触发方式能抓住它的断言
接受已过期验证码将时钟快进到 TTL 之后400 code_expired,记录已消费
验证码重复使用用同一验证码验证两次第二次返回 400,没有第二个会话
无尝试次数上限N 次错误验证码,然后一次正确的达到上限后正确验证码也被拒绝
重发竞态并发的重发与验证有且只有一种已写明的结果
枚举已注册号码 vs 未注册号码响应体一致,中位数差值低于 50 毫秒
SIM 卡交换在已有账号上重新验证写入审计记录,会话失效
服务商静默失败发送后读取真实收件箱从短信中读到验证码,120 秒内收到回执

表里每一行的成本是一个租用号码,$0.04 起,而没有收到任何内容的激活会自动退款,所以把这七项全部跑一遍,整套测试单次完整运行的花费不到一美元。

短信一直收不到,该怎么排查?

验证码没到,通常有五种可能,而且它们分布在不同的环节:客户端根本没发出去、服务商拒绝了、运营商丢弃了、目标号段有问题,或者号码本身就是废号。按顺序排查,每一环都先拿到一份证据再往下走。瞎猜的成本比逐项核实高得多。

激活失败会自动退款

第一步,确认请求确实离开了客户端

在怪罪下游之前,先证明 App 真的发出了这次调用。打开这次测试的网络日志,找到发往你的发送接口的 POST 请求。你需要确认三件事:请求发出去了、里面带的手机号格式和你预期的一致、返回的是 2xx。

移动网络信号塔,过滤在此发生

这一环常见的问题都挺乏味。客户端的某条校验规则悄悄吞掉了提交。号码存成了 07700 900123,发出去时没带国家码。之前某次测试留下的重试保护,让按钮又禁用了 60 秒。过期的鉴权 token 让发送变成了 401,而界面上只显示了一个普通的加载动画。

需要留存的证据:UTC 格式的请求时间戳、实际发出的 E.164 字符串、HTTP 状态码、响应体,以及客户端的构建号。

第二步,检查服务商的响应和消息 ID

你自己后端返回 2xx,不代表服务商接收了这条短信。去看服务商针对这次发送返回的响应内容,有两种结果值得关注。

如果拿到了 message ID,说明服务商已经把它排进队列了。把这个 ID 记下来。它是唯一能把你的日志和运营商日志串起来的钥匙,没有它,找客服沟通根本推进不下去。

如果返回的是错误,要看错误码,而不是那段给人看的文字说明。服务商会用不同的错误码来区分前缀不可路由、目标国家被屏蔽、发送方 ID 未在该国注册、余额不足、触发限流。每一种对应的解决办法都不一样,而其中三种的提示文字往往是一模一样的。

第三步,读取送达回执状态

回执(DLR)是运营商给出的答复。状态通常在 5 到 30 秒内确定,如果 120 秒后回执还卡在 queued,这本身就是一条线索。

状态含义下一步动作
queued / accepted服务商还压着,运营商未确认等 120 秒,之后按路由问题处理
sent已交给运营商,但没有确认回来确认这条路由到底会不会返回回执
delivered运营商确认已送达手机问题出在读取环节或收件箱,而不是发送
undelivered运营商先接收,随后丢弃过滤、发送方 ID 或内容的问题
failed直接被拒号码有误、号段被封或 SIM 卡失效

如果回执是 delivered 却看不到验证码,那说明问题出在你的轮询逻辑、解析器或时间窗口上,而不是短信链路。在把问题归到送达之前,先自己去读一遍收件箱的原始内容。

第四步,换一个国家或另一个服务测试

单条路由失败说明不了问题的范围。把同样的内容发到第二个目的地,然后做对比。如果美国号码20 秒就收到了,而印度超时,那就是这条通道上的路由或过滤问题,而不是模板坏了。如果两边都失败,问题就在上游,出在你自己的代码或服务商账户上。

按服务再做一次同样的对照。如果你自己 App 的验证码能到,而某个第三方服务的验证码到不了,那就是对方在过滤或限流,得由他们那边来解决。

第五步,换号码,只重试一次

单个号码是会出问题的。SIM 卡被回收、某个号段被某个特定发送方拉黑、运营商对某一整块号码停掉了 A2P 流量。租一个新号码,把完全相同的发送流程再走一遍。

在 MarioSMS,每个号码一次只用于一次验证,价格低至 $0.04;如果在激活时间窗内没收到短信,激活会自动取消,费用自动退回你的余额,所以再失败一次的重试是不花钱的。两个不同号码连续两次失败,就有力地说明是路由或服务的问题。一次失败接着一次成功,说明问题出在第一个号码上,记录下来,然后继续往前走。

只重试一次,不要重试五次。一分钟内向同一个目的地反复发送会触发防轰炸规则,把原本清晰的信号变成噪音。

记录哪些字段,能让下次排障更快

这些字段要在发送时就记下来,而不是等出了问题再补:

  1. request_id,来自你自己的后端,并透传到客户端。
  2. provider_message_id,原样保存服务商返回的值。
  3. msisdn_e164,做哈希处理或只保留后 4 位。
  4. country_iso2service_key
  5. sent_at_utc,精确到毫秒。
  6. dlr_status 以及 dlr_received_at_utc,原地更新。
  7. activation_id,来自号码租赁服务商,这样号码和发送记录能落在同一行上。
  8. attempt_number,以及它所替代的上一次尝试的 ID。

有了这八个字段,一半以上的短信故障只要读一行记录就能定位,不用再靠时间戳去关联四套系统。PythonNode.js 示例在租号时都会返回激活 ID,所以第 7 项只需多写一行赋值。

如何在不烧掉预算的前提下对 OTP 流程做压力测试?

给每个虚拟用户都租一个真实号码的压测,那不是测试,那是账单。按每个号码 $0.04 算,模拟 5,000 次注册一轮就要 $200,如果这套测试在每次合并到 main 时都跑,一天就得付四次。正确的做法是把两件事分开测,因为它们出问题的原因根本不一样。

OTP经历的各个阶段

阶段负责方典型耗时此处可能出现的问题
请求已受理你的后端200毫秒以内限速、校验、重复提交
消息已排队你的短信服务商1秒以内路由选择、发送方ID被拒
运营商已接收运营商1到10秒过滤、发送方被屏蔽、灰色路由
已送达手机网络2秒到数分钟漫游、拥塞、设备关机
验证码已输入用户数秒到数分钟过期、重试、自动填充

你的日志通常只记录到第二阶段,这正是真实号码测试能发现而监控系统会遗漏问题的原因。

把压力测试和送达测试分开

吞吐量测试回答的是:你的 API 能不能扛住每秒 500 次发送请求,涉及连接池、数据库写入争用、队列深度,以及限流器本身。送达测试回答的是:发往印度某台真实手机的验证码能不能收到、内容对不对。前者需要量,不需要运营商;后者需要真实号码,几乎不需要量。

用于监控OTP送达情况的仪表盘
测试类型单轮请求量真实号码成本来源运行时机
吞吐量1,000 到 100,000 次请求仅计算资源每次合并
限流50 到 500 次请求仅计算资源每次合并
送达抽样5 到 30 次激活按激活计费每晚
发版卡点20 到 60 次激活按激活计费上线前

在边界处模拟服务商

接缝要留在你自己的短信客户端接口上,而不是 HTTP 库那一层。一个接口,带 send(to, body)fetchCode(activationId) 两个方法,就能让你既有一个 0 毫秒返回固定验证码的假实现,又有一个真正对接服务商的真实实现。在压测场景下,假实现也要模拟延迟和失败:带抖动地睡眠 200 到 900 毫秒,2% 的调用返回 429,0.5% 返回 500。一个永远在 1 毫秒内返回 200 的 mock,会把你压测本来就想抓的超时和重试风暴全部藏起来。

用合成号码测试限流

限流逻辑的键是手机号字符串、IP 和设备指纹。这三样背后都不需要一个能真正租到的号码。在服务商永远不会分配的号段里生成合法的 E.164 字符串,对同一个号码在 60 秒内发 60 次请求,然后断言第 4 次返回 429 并带上 Retry-After 响应头。同样地,用同一个 IP 对 500 个不同的合成号码发请求,检查 IP 维度的限流桶。给这些记录打上 synthetic=true 标记,免得有人为一个压根没租过的号码去排查”短信怎么没收到”。

用低频抽样验证真实送达

真实激活应该放在每晚的定时任务里,而不是每次提交都跑的测试套件里。抽样要覆盖真正会变化的维度:3 到 5 个国家、你排名前 3 的服务,再加一个重复尝试的场景。每晚十二到二十次激活,就能发现发送方 ID 失效、或者某个国家不再能收到某个服务的短信,这些是任何 mock 都抓不到的。由于每个租用号码都带有一个激活窗口,没收到短信时会自动把费用退回你的余额,所以某天晚上某条线路挂掉,几乎不会产生成本。直接复用你现有测试套件里的 PythonGo 轮询循环,让这个夜间任务只是换了套配置,而不是另写一套代码。

估算送达抽样的成本

先把账算清楚,再去排期。用每轮激活次数乘以每个周期的轮数,再乘以对应服务和国家的价格。价格从 $0.04 起,各有不同,所以要按你测试矩阵里最高的价格算,而不是最低的。

频率每轮激活次数每月轮数按 $0.04 算按 $0.25 算
每晚抽样1530$18.00$112.50
每周深度矩阵604$9.60$60.00
每次发版卡点258$8.00$50.00

激活失败的退款会让实际支出低于上面这些数字。但预算还是按上限来留,然后把充值提醒设在上限的 40%。

防止测试循环失控的护栏

  1. 在代码里限制单次进程运行的激活数量,比如 40 次,计数器一旦触顶就强制退出。
  2. 限制每个 CI 项目每小时的激活数量,用共享计数器来校验,而不是本地变量。
  3. 如果账户余额低于本轮的预估上限,就拒绝启动,避免跑到一半留下半个未测完的矩阵。
  4. 设置 15 分钟的挂钟时间强杀,因为卡住的轮询循环租号码的速度比正常跑还快。
  5. finally 块里释放每一次号码激活,这样即使断言抛出异常,也不会让某个租用一直占着窗口。
  6. 触发护栏时直接让构建失败,而不是悄悄跳过,因为静默跳过看起来就像一个通过了的送达测试,而它其实压根没跑。

如何在生产环境中监控 OTP 送达情况?

CI 测试套件只能证明流程在提交代码那一刻是通的。生产环境的监控才能证明它在周二凌晨两点依然可用,哪怕某个国家的运营商开始悄无声息地丢弃来自某个 sender ID 的流量。这两者之间的空白地带,正是工单诞生的地方,而解决办法就是一小组指标,配上你真的愿意为之呼叫值班的告警阈值。

每个市场的最低测试矩阵

维度需覆盖的取值原因
国家每一个上线市场格式、区号和过滤规则各不相同
号码状态全新、曾使用过、刚释放重复处理逻辑
时间立即、临近过期、过期后窗口期逻辑
尝试次数首次、重试、超出限制锁定行为
渠道短信、语音回退(如有提供)回退路径

最重要的四个指标

按小时跟踪四个数字,并按国家和服务商拆分。其余的一切都只是下钻分析。

一个团队在商定如何共享测试号码
指标定义为什么会波动
发送受理率调用短信服务商 API 返回成功状态的次数 / 总发送尝试次数服务商故障、凭证过期、号码格式错误
送达率状态报告标记为已送达的数量 / 已受理的发送量运营商过滤、sender ID 报备失效、被判定为垃圾信息
首条验证码耗时(p95)从发送请求到收到回执事件的秒数聚合商队列积压、线路切换
验证完成率提交了正确验证码的用户 / 请求了验证码的用户以上任何一项,外加 UX 问题和输错率

发送受理率和送达率回答的是两个不同的问题。服务商可以受理你 100% 的请求,却只送达其中的 60%,而只有状态报告能告诉你这一点。

按国家和运营商拆分送达率

汇总后的送达率会掩盖真正要命的故障。假如你 85% 的流量是美国号码,送达率 97%,而 6% 来自某家印度运营商,送达率只有 40%,那么全局数字看起来是 93%,没人会察觉。先按国家分桶,再按运营商或者服务商能提供的 MCC-MNC 分桶,然后针对分桶而不是总量设置告警。

在允许某个分桶触发告警之前,先设一个最低量级门槛。一小时内 10 条发送、送达率 70%,那是噪声。50 条发送、送达率 70%,而 14 天基线是 96%,那就是线路出问题了。把每个分桶和它自己的历史基线比较,因为一个平时就在 88% 的国家,不该因为今天还是 88% 就把你呼起来。

首条验证码耗时要看分位数,不要看平均值

首条验证码耗时的平均值几乎毫无用处。大多数验证码 10 秒内就到了,所以少数几条 90 秒才送达的短信对均值影响甚微,却彻底毁掉了尾部那批用户的体验。要分别跟踪 p50、p95 和 p99。

盯住 p95 和你的重发按钮倒计时之间的关系。如果重发按钮 30 秒后解锁,而 p95 爬到了 34 秒,那你刚刚制造了一波重复发送、翻倍的支出,以及一条会让用户正在输入的第一条验证码失效的第二条验证码。这是一个送达问题,但它以一张抱怨“验证码不对”的工单形式出现。

验证完成漏斗

埋点记录五个步骤,并把计数存成一个漏斗:

  1. 用户提交手机号。
  2. 服务商受理发送请求。
  3. 收到状态报告。
  4. 用户输入验证码。
  5. 后端校验通过。

第 3 步到第 4 步的流失属于注意力和 UX 问题,包括自动填充是否生效。第 4 步到第 5 步的流失则是验证码到得太晚、用户从更早的一条短信里抄了验证码,或者在你同时支持双因素认证应用的情况下,TOTP 链路上出现了时钟偏差。把这两段流失拆开,能让移动端团队和消息团队不必围着一个混在一起的数字互相扯皮。

告警阈值,以及什么该呼叫值班

只为人能在接下来 30 分钟内解决的问题呼叫值班。其余的一律开工单。

信号阈值处理方式
发送受理率低于 95% 持续 10 分钟呼叫值班
任意每小时发送量 50 条以上国家的送达率连续 2 个窗口低于其 14 天基线 15 个百分点呼叫值班
首条验证码耗时 p95高于重发倒计时持续 15 分钟呼叫值班
验证完成率环比上周下降 10 个百分点开工单
单个运营商送达率低于 60% 持续 2 小时开工单
服务商 webhook 延迟回执到达时已超过 5 分钟开工单

送达率告警要求连续两个窗口才呼叫值班。运营商会出现短暂的、能自行恢复的下跌,而单窗口触发只会训练值班的人无视这条告警。

一套 20 分钟的每周复盘流程

  1. 打开按发送量排序的送达率表格,看前 10 个国家,记下任何波动超过 5 个百分点的分桶。
  2. 对比各国首条验证码耗时的 p95 与上周的差异。
  3. 读一遍完成漏斗,找出最大的单段流失。
  4. 给每个有问题的国家租一个号码,手动跑一遍完整流程,使用临时手机号,这样一次检查只花几分钱,而不是升级成一次客服事故。
  5. 为最糟糕的那个分桶开一张工单,附上国家、运营商、状态报告样本和时间戳。

第 4 步是各团队最常跳过的一步。用真实号码在可疑线路上跑真实流程,能把“巴西的送达率看起来不太妙”变成“验证码 45 秒才到,而我们的重发倒计时是 30 秒”,后者是一句可以动手修的话。

如何让 OTP 测试在 CI 中保持稳定?

只要一套 OTP 测试用到真实号码和真实运营商,它就永远不可能像单元测试那样确定。目标不是把波动降到零,而是让流水线里的红灯真正说明你的代码出了问题,而不是凌晨三点某个运营商队列卡住了。要做到这一点,就得按成本和不稳定程度拆分测试,再输出足够的上下文,让打开失败报告的人两分钟内就能动手。

值得明确测试的故障模式

故障如何复现正确行为
完全没有短信让激活窗口期过期提供重试选项,不产生任何费用,不出现半创建状态的账户
短信延迟到达验证码在你的超时之后才到达要么接受它,要么清楚说明为什么不接受
重复请求连续点击两次发送只生成一个验证码,而不是两个相互冲突的验证码
反复输错验证码输入四次错误验证码锁定按号码计数,提示信息说明应如何处理
号码重复使用用已存在的号码注册触发你的合并或拒绝逻辑,绝不能出现堆栈跟踪报错
国家不匹配来自不受支持市场的号码提示信息中列出所支持的市场

哪些测试应该跑在每个 pull request 上

Pull request 的流水线应该在 10 分钟内跑完,而且每次运行不产生任何费用。以下这些放进每个 PR:

一个人正在手机上填写注册表单
  1. 针对 mock 短信服务商的契约测试,验证你的代码发出的请求结构正确,解析的验证码格式也正确。
  2. 验证会话的状态机测试:已创建、验证码已发送、验证码已输入、已验证、已过期、已锁定。
  3. 用假时钟测试限流和重发倒计时逻辑,让 30 秒的重发窗口在 3 毫秒内测完。
  4. 验证码校验的边界情况:错误的验证码、过期的验证码、重复使用的验证码、带空格的验证码、上一个会话的验证码。
  5. 每个服务商接口保留一份录制的 HTTP fixture,每周刷新一次,这样接口结构变化就会以 diff 的形式暴露出来。

这些测试都不需要租号码。如果某个 PR 改动的正是短信适配器本身,那就加一条真机冒烟测试,用标签控制,让贡献者按需触发。

哪些测试应该放进夜间任务

真实送达测试属于定时任务,不该跟着每次 push 跑。一个夜间任务租 6 到 10 个号码,按每个 $0.04 起算也就花几毛钱,却能每天给你一个信号,覆盖你真正在用的那些线路。

测试类型触发方式是否使用真实号码单次运行典型数量
Mock 契约测试每个 PR40 到 200
单国家真机冒烟合并到 main1
主要国家真机矩阵每晚6 到 12
全国家覆盖扫描每周30 到 60
压力与吞吐按需部分不定

给夜间矩阵排序时,把量最大的市场放在最前面。这样即便任务被中途掐断,你至少已经知道了美国印度的送达情况。

安全地保管 API 密钥和余额上限

把 API 密钥放进 CI 的密钥库,绝不要放进仓库,也绝不要放进 fixture 文件。三条规则能挡掉大部分事故:

  1. CI 用的密钥和生产监控用的密钥分开,这样吊销其中一个不会让另一个也失明。
  2. 在日志输出中对密钥打码,任何回显手机号的响应体在进入构建日志前先做清洗。
  3. 用余额来兜底,而不是靠信任。CI 账户只保持一个小额的可用余额,按计划充值,这样跑飞的循环会卡在余额上,而不是整晚不停地跑。

在矩阵启动前加一个预检步骤,检查余额,如果低于本次运行的预计成本就立刻失败并给出清晰提示。一个 5 秒内就报出「余额 $0.31,本次运行需要 $0.90」的构建,好过跑到一半才抛出 14 条让人摸不着头脑的错误。

隔离不稳定的送达测试

运营商延迟是真实存在的,那不是你的 bug。用策略来处理它,而不是到处加重试:

  1. 短信等待时间要基于实测数据设定,不要靠拍脑袋。如果到达时间中位数是 12 秒、p95 是 48 秒,那就等 90 秒。
  2. 真机测试最多重试一次,而且只在超时的情况下重试,验证码断言失败时绝不重试。
  3. 激活超时且没有收到短信时,按「结果不明确」处理,而不是判定失败。租用的号码会被取消,费用退回余额,所以重试只多花一次激活的钱,而不是两次。
  4. 统计每个测试在 14 天内的不稳定率。超过 10% 的挪到隔离任务里,照跑照报,但不阻塞流水线。
  5. 每周复查隔离区。在里面待了一个月的测试,要么对应一个真实的产品 bug,要么对应一条你该停止支持的线路。

让失败结果可以直接行动

失败信息才是这里真正的产出。每条真机测试失败都应该打印出服务、国家、激活 ID、号码租用时间、等待超时的时间点,以及经过的秒数。如果收到了短信但解析失败,把原始短信内容附上,数字打码。结果以 JUnit XML 格式输出,让 CI 按国家分组,每次夜间运行往团队频道发一行汇总,内容是数字,不是形容词。

一个流水线布局示例

jobs:
  unit:        # every PR, mocked, ~4 min
  contract:    # every PR, recorded fixtures
  smoke-live:  # merge to main, 1 number, US
  matrix-live: # nightly cron, 8 numbers, 6 countries
  quarantine:  # nightly, non-blocking, reports only

把所有真机任务放在同一个文件里,共用一个 setup 步骤:租号码、轮询面板或 API 取验证码、结束时释放会话。轮询的辅助代码和你应用里用的是同一份,所以同样的 Python 接收短信Node.js 示例,只要把等待常量调大,就能直接当作 CI 的 fixture 使用。

团队该如何共享号码、权限和预算?

当四个工程师各自维护个人余额、没人清楚哪次运行花了多少钱时,测试号码的管理很快就会变得一团糟。解决办法是:一个共享账户、按环境划分密钥,再加上一条命名规则,让每个租用的号码都能追溯到发起请求的测试套件。

每次验证尝试应记录的内容

字段重要性
请求ID将该次尝试与服务商日志关联起来
国家和区号显示哪些市场出现故障
服务商和路由比较不同厂商之间的送达情况
首次输入验证码所需时间真实的用户可感知延迟
结果已送达、已过期、验证码错误、已放弃

按国家和小时汇总这些数据;某个国家的数据突然变得稀少,往往说明出现了送达问题。

一个账户,按环境分配密钥

创建一个由团队持有的 MarioSMS 账户,而不是挂在某个随时可能离职的个人名下。然后为本地、CI 和预发布环境分别签发 API 密钥。余额共用,密钥各异,这样吊销其中一个不会影响另外两个。

密钥使用方典型用量轮换周期
local开发者笔记本每天 1 到 5 个号码员工离职时
ci流水线运行器的密钥变量每天 10 到 40 个每季度
staging定时冒烟测试任务每天 4 到 8 个每季度
load吞吐量实验突发用量,之后闲置每次实验后

密钥要存放在密钥管理器里,绝不能提交到代码仓库。本地密钥放进 .env 文件并写入 .gitignore,CI 密钥则设为受保护变量,避免在任务日志中被打印出来。

按测试套件跟踪支出

号码起价 $0.04,具体价格因服务和国家而异,所以每晚租用 30 个号码的套件,和在高价国家租用 30 个号码的套件,花费并不相同。要在租用时就记录成本,而不是等到月底再算。

  1. 让租号辅助脚本为每次激活写一行记录:时间戳、套件名称、服务、国家、价格、激活 ID。
  2. 把这些记录发送到存放测试产物的同一位置(一开始,在任务输出里生成一个 CSV 就够了)。
  3. 每周按套件汇总,并与仪表盘上的余额下降情况做对比。
  4. 对任何环比增长超过 20% 的套件展开排查。

退款在这里很关键。如果在激活时限内没有收到短信,激活会自动取消,费用也会自动退回余额,因此除非你把取消的记录标注出来,否则记录的支出会高于实际成本。加一列 refunded,每次运行后做一次对账。

用银行卡或加密货币充值,以及何时充值

余额可通过银行卡或加密货币充值。设定一条能覆盖用量最大套件整整一周的下限,余额跌破时就充值。如果每晚运行要用 40 个号码、平均单价约 $0.10,一周大约是 $28,那么 $50 的下限既能留出充足余量,又不会积压用不上的资金。

设个日历提醒吧,别等到凌晨两点 CI 里租号失败才发现。余额见底会让流水线变红,而在有人去查仪表盘的那二十分钟里,它看起来就像是应用本身出了 bug。

QA 与工程之间如何交接一次运行

当 QA 发现验证流程出问题时,工程师需要四样东西才能复现:国家、服务、激活 ID 和短信的完整内容。把这些设为工单模板里的必填项。激活 ID 是关键锚点,因为它能把这次运行对应到某个具体租用的虚拟手机号及其计时器。

应用界面的截图有帮助,但原始短信正文更有用,因为发送方 ID 和验证码长度会随线路不同而变化,而这种差异往往正是问题所在。

记录用了哪些号码,以及为什么用

在仓库里维护一张普通表格,由租号辅助脚本自动更新,而不是手工填写。列包括:日期、套件、国家、服务、激活 ID、结果(收到验证码、超时、已退款)。保留 30 到 90 天后删除,因为这属于用完即弃的运维数据,长期保留只会带来不必要的隐私麻烦。每个号码同一时间只用于一次验证,所以日志始终保持一次尝试一行。

请仅将其用于隐私保护、团队合法拥有的第二账户,以及 QA 或 OTP 测试,具体请参见可接受使用规则。

用于手动测试的 iOS 和 Android 应用

自动化能覆盖重复场景,但探索性测试仍然靠手工完成。iOS 应用、Android 应用以及 app.mariosms.com 上的网页版显示的是同一份余额和同一份激活列表,所以测试人员用手机就能租号、等验证码到达(通常一分钟内),再粘贴到被测应用里,全程不用碰终端。从 35+ 个国家的现有库存中选择国家,点击租用,这次激活就会出现在共享仪表盘上,团队其他成员都能看到。

验证测试涉及哪些隐私与法律边界?

租用号码是测试工具包中很常见的一部分,它和团队经手的其他任何凭据一样,需要承担同等的责任。这些边界并不复杂,但必须写在某个地方,让你的 QA 工程师、外包人员和新入职的同事在租用第一个号码之前就能读到。

生产环境中应设置告警的指标

指标健康状态告警触发条件
验证码输入率按国家和小时保持稳定与上周同一小时相比下降三分之一
输入耗时中位数数十秒翻倍
已过期的激活占比小且稳定某个国家出现激增
重试率较低在没有发布新版本的情况下持续上升

隐私保护是核心使用场景

大多数团队开始租用号码,是因为不想让个人手机号出现在测试数据里。一位 QA 主管用个人手机号注册了十二个预发布环境账号,这个号码就留在了十二个数据库中,其中一些会被导出,一些会被备份到没人追踪的地方。营销短信随之而来。如果这些系统中有任何一个被攻破,密码重置请求也会接踵而至。

租用号码切断了这条链路。它完成一次验证,激活任务结束,号码回到号码池。测试账号与真人的联系方式之间不会留下任何关联。同样的逻辑也适用于测试竞品注册流程的创始人,或是复现客户问题的技术支持工程师:他们需要的是一个能用的号码,而不是自己的号码。

出于正当理由的第二个账号

有大量正当工作需要不止一个账号。社交媒体运营既要管理品牌账号,也有自己的个人账号。开发者会在同一个平台上把沙盒账号和生产账号分开。技术支持团队需要一个客户视角的账号,以便看到用户看到的界面。本地化测试人员需要一个在目标市场注册的账号,这通常意味着需要该国家的号码,比如印度号码美国号码

请查看你所注册服务的条款。有些服务完全允许多账号,有些在存在业务合作关系时允许,有些则加以限制。号码从哪里来,并不会改变这些条款的内容。

在自有或已获授权的系统上做 QA 与 OTP 测试

最清晰的情形是测试自己的产品。发送方、验证码格式、重试策略和账号生命周期都由你掌控,因此除了运营商链路之外,整个测试不会触及任何第三方。把测试账号写入预发布数据库,打上标签,并按计划定期删除。

测试他人的系统时,先取得书面授权。一份签署的渗透测试范围文件、一份明确列出系统名称的客户合同,或是一个有公开政策的漏洞赏金项目,都算数。来自并不拥有该系统的人的口头许可则不算。OTP 接口上的频率限制和滥用检测的存在自有其道理,狂轰滥炸一个你未被邀请测试的接口就是问题,无论号码从哪里来。

绝对不可接受的行为

有两件事永远越界。第一是重新登入一个你已被封禁的账号,无论封禁原因是垃圾信息、欺诈、骚扰还是违反条款。封禁是平台针对你本人做出的决定,而不是针对你的手机号。第二是冒充他人:用真人的姓名注册、盗用身份,或创建看起来像某家公司或某个人的账号,而你并不是他们。

欺诈、垃圾信息推广,以及任何针对他人的行为,同样属于不可接受的使用范围。MarioSMS 会因此封停账号。完整政策见 /acceptable-use/

测试产物的数据留存

OTP 测试会产生各种产物:含号码的日志、验证界面的截图、CI 任务输出、测试账号的数据库记录。即便号码是租来的,也要把所有这些都当作个人数据来对待。

产物建议留存期处理方式
含激活 ID 的 CI 日志14 到 30 天对号码后 4 位做掩码处理
失败运行的截图7 天从产物存储桶自动删除
预发布环境中的测试账号记录直到本次冲刺结束按标签删除,不要手动删
收到的验证码短信正文不要存储对提取出的验证码做断言,然后丢弃正文

验证码本身有效期很短,通常是 5 到 10 分钟,但一条在日志里躺了一年的短信正文,仍然是一条发往真实号码的消息记录。

把可接受使用页面指给同事

把链接放在三个地方:新入职 QA 的上手文档、调用该 API 的测试仓库的 README,以及团队用来申请预算充值的那个频道里的置顶消息。当有人问某个使用场景是否被允许时,请依据政策原文来回答,而不是凭记忆。如果这个场景确实不明确,就在租用之前咨询客服,而不是等账号建好之后再说。

关于 OTP 与短信验证测试的常见问题

测试验证码一般多久能收到?

大多数验证码会在发送请求后一分钟内出现在应用或控制台里。差异来自线路,而不是号码本身。发往主流运营商的本地线路通常 5 到 15 秒就能送达,而经过多次转接的跨境线路可能要 40 秒以上。把轮询超时设为 120 秒,比这更快就算意外之喜,别当成必然。

在CI中保持测试套件稳定

问题解决方法
超时导致的不稳定将测试超时时间设置得比激活窗口期更长,而不是更短
并行运行发生冲突每个worker使用一个号码,在teardown阶段释放
预算失控限制每天的运行次数,冒烟测试使用最便宜的市场
日志中出现敏感信息在测试输出中对号码和验证码进行脱敏处理

如果完全收不到短信会怎样?

每个号码都有一个激活时间窗。如果窗口关闭时仍未收到短信,激活会自动取消,费用会自动退回你的余额。你不需要提交工单,也不用等人工审核。对测试套件来说,这一点比听上去更重要:一次出错的构建触发 200 次失败激活,除了浪费几分钟之外不会产生任何费用。不过还是要把这次取消记为测试失败,因为收不到验证码本身就是关于发送方的真实信号。

一个号码能收两条验证码吗?

不能。MarioSMS 的一个号码一次只对应一次验证。在同一个激活窗口内重发,有时确实会落在同一个号码上,但不要按这个来设计。如果你的测试用例覆盖“用户点两次重发并使用第二个验证码”,请把重试路径当作独立场景处理,并确认该流程是否需要重新租用号码。默认一个号码对应一个验证码,你的测试才能保持确定性。

一个测试号码要多少钱?

价格从 $0.04 起,因服务和国家而异。供应充足的国家里的廉价服务接近这个下限;而在较小市场中、对发送方规则要求严格的服务,号码会更贵。给测试套件估算成本时,用每次运行的号码数量乘以你矩阵中最高的那个价格,而不是最低的,然后再和工程师追查一个不稳定的手工测试所耗费的成本做比较。

每个测试用例都需要不同的号码吗?

只要涉及创建账号,就需要;只要涉及验证“该号码已注册”这条分支,也需要。复用号码会引入跨用例污染,导致用例 B 通过只是因为用例 A 留下了残留状态。纯粹的送达检查(短信到底能不能走通这条线路)可以在一次激活内共用同一个号码,但这是唯一安全的重叠场景。

怎样不靠干等来测试过期?

三种办法,按推荐程度排序:

  1. 在后端拨动时钟。通过仅用于测试的接口把 OTP 记录的 created_at 往前调,然后提交验证码。
  2. 在测试环境缩短 TTL。30 秒的过期时间能让你在一分钟内跑完一次真实的墙钟测试。
  3. 租一个号码,收到验证码,然后把它拖过真实的时间窗。这种方式慢且贵,但只有它能完全按线上实际情况检验生产环境的 TTL。

CI 里用第 1 种,每个版本发布前用一次第 3 种。

整个流程能通过 API 自动化吗?

可以。REST API 覆盖号码租用、验证码获取和取消,示例见 PythonNode.jsGo 和 PHP。一个可用的自动化用例只需要四次调用:申请号码、触发应用发送、轮询获取验证码、释放号码或让激活自然过期。把这些包成一个 fixture,这个测试读起来就和其他集成测试没什么两样。

有多少个国家可选?

现有库存覆盖 35+ 个国家和数百种服务,具体可用性会随供应变化。请在租用时查询库存,不要把国家硬编码进测试配置。如果你的测试矩阵必须用到某个特定市场,加一个跳过条件,让套件报告“XX 无库存”,而不是抛出一个红色失败、让工程师白白排查一个小时。

模拟器能收到短信吗?

收不到真实短信。iOS Simulator 没有射频模块,也没有 SIM 卡。Android 模拟器可以通过控制台注入短信(在 5554 端口执行 sms send),这能测试你的解析和自动填充逻辑,但完全说明不了运营商的送达情况。把两件事分开:注入消息用于验证 UI 行为,租用真实号码用于验证送达链路。

怎么测试语音电话回退?

回退通常在短信尝试失败后触发,或者用户点击“改用电话呼叫”时触发。要稳定走到这条分支,让短信尝试自然超时,而不是想办法强行制造错误。先确认应用在正确的时机显示了回退入口,再单独验证语音服务商已配置妥当。为接收短信而租用的号码只能收文字消息,因此语音相关的检查要在你自己的服务商控制台上做。

该用自己的私人号码测试吗?

不该,有四个原因:你的号码会累积注册状态,从而干扰结果;你没法把“新用户”路径测两遍;队友无法复现你的运行结果;而且你的私人号码会出现在共享的 CI 日志里。请使用租用号码,别让私人号码进入代码仓库。

验证失败的 bug 报告里该写什么?

字段示例
时间戳(UTC)2026-09-10 14:22:05
国家和服务印度,通讯类应用
号码(仅后 4 位)…4471
激活 IDact_8812f
发送请求状态HTTP 200,发送方已接受
超时时长118 秒
看到的发送方 ID(如有)
应用版本4.7.1 (2211)

八个字段,客服或你的短信服务商就能追溯线路,不用再来回追问。

怎样测试限流又不会被封?

把限流测试指向你自己的后端,配上一个短信发送的 stub。请求计数是你自己的逻辑,不需要真实消息。在 60 秒内向 stub 发起 10 次发送,断言 429 响应和 retry-after 响应头,然后用一个真实的租用号码跑一遍正常流程,确认限流没有拦住正常流量。

下周还能复用同一个号码吗?

请默认不能。每次租用只对应一次激活,同一个号码不会一直等着你。把测试设计成与身份无关:不要有期望某个特定手机号字符串的 fixture,不要有写死号码的黄金文件,也不要有必须跨运行存活的账号。在测试运行时生成身份,结束时清理掉。

上线短信验证前,先走一遍发布前检查清单

把这份清单打印出来,从上到下逐条走。只有在真实环境里亲眼看到对应行为之后,才能打勾。下面每一条,都曾有团队上线时踩过坑。

测试计划中的费用去向

运行使用号码数大致成本
每次部署的冒烟测试1到2个几美分
每个市场的完整矩阵测试6到10个不到1美元
每周回归测试,覆盖8个市场60到80个几美元
负载测试,1,000次注册1,000个按目录价格计算需数十美元

目录价格,2026-09-10;失败的激活会自动退款,所以实际账单会更低。

验证码生成与存储检查

  1. 验证码来自密码学安全的随机源,而不是 Math.random() 或带种子的伪随机数生成器。
  2. 验证码长度和字符集固定且有文档记录(OTP 流程通常默认 6 位数字)。
  3. 验证码以哈希形式存储,并且记录中绑定了手机号和用途,这样为登录签发的验证码就无法被重放到密码重置上。
  4. 有效期以绝对时间戳存储,在服务端校验,即使客户端传来的是过期值也照样生效。
  5. 验证码一次性使用。签发会话的那个事务内,同时把记录标记为已消费。
  6. 比对采用常量时间,这样响应耗时就不会泄露前面匹配了几位。
  7. 手机号在存储前和查询前都归一化为 E.164 格式,让 +1 415 555 01424155550142 指向同一条记录。

下发与兜底检查

  1. 发送调用设有超时(5 到 10 秒),重试策略不会在响应慢但其实成功的情况下重复下发。
  2. 服务商错误被映射为面向用户的状态:号码无效、不支持该国家、运营商拒收、服务商故障。
  3. 有重新发送按钮,前 30 到 60 秒内置灰,并且复用同一个验证码,而不是每点一次就生成新的。
  4. 至少接好一条兜底通道(第二家服务商、语音电话或邮件),并且你手动触发过,而不只是看过文档。
  5. 超长短信内容、非拉丁字符的发送方 ID 以及模板里的 Unicode,都在每个上线国家用真机实测发送过。
  6. 消费并存储送达回执,这样”已发送”和”已送达”在你的数据里是两个独立状态。

滥用与限流检查

  1. 按手机号、按 IP、按账号、按设备指纹分别设限,每个维度都有各自的时间窗口。
  2. 无论号码是否已注册,返回的响应完全一致,防止号码枚举。
  3. 设置每小时和每天的成本上限,触顶直接硬性阻断,而不只是发个告警。
  4. 高费率目的地要么走白名单,要么直接封禁,因为短信刷量盯上的恰恰是你并不做生意的那些号段。
  5. 验证失败次数设上限(通常是 5 次),达到上限后作废整条记录,而不只是拒绝本次。

监控与告警检查

指标何时告警为什么重要
发送成功率比 7 天基线低 5 个百分点服务商或账号出问题
分国家送达率任一国家 1 小时内下跌 10 个百分点运营商拦截
验证码输入耗时中位数超过 90 秒上游下发变慢
验证完成率在某一个平台上骤降客户端有 bug,不是下发问题
每小时花费超过你设定的上限刷量,或者重试死循环

每条告警都要有明确负责人,以及”是电话叫醒还是开工单”的处理级别,这些要在上线前定好,而不是等第一次事故时再临时决定。

文档与应急手册检查

  1. 应急手册页面里写明服务商、账号负责人、支持渠道和预期响应时间。
  2. 切换服务商的步骤写成具体命令或配置项,而不是大段文字描述。
  3. 客服团队有一套应对”我一直没收到验证码”的话术,覆盖国家、运营商、漫游、免打扰名单登记和手机端拦截等情况。
  4. 测试凭据、沙箱号码和租号流程都有文档,让新来的工程师第一天就能跑通整套测试。
  5. 手机号和验证码记录的数据留存期限白纸黑字写明具体天数,删除任务也已排期。
  6. 内部对可接受用途的立场清晰:支持第二账号、QA 测试和隐私防护,不支持封号规避和冒充他人。

每次发布后的五分钟冒烟测试

  1. 在你最大的市场从 MarioSMS 租一个号码,比如美国,$0.04 起,具体价格视服务而定。
  2. 从已部署的版本触发一次发送,同时开始计时。
  3. 确认验证码在激活时间窗口内出现在面板上(通常不到一分钟;如果一直没收到,激活会自动取消并退款)。
  4. 输入验证码,确认会话正常签发,然后把同一个验证码再提交一次,确认被拒绝。
  5. 故意触发一次失败(验证码过期),确认错误提示文案和客服预期听到的说法一致。