Elixir使用Ecto时测试偶现Ecto.Query.CastError问题
我最近遇到了和你一模一样的问题——测试偶尔触发Ecto.Query.CastError,提示数值1234无法转为:string类型,正常运行却完全没问题。经过一番排查,终于找到根源,下面分享我的解决过程:
错误信息复盘
首先看核心错误提示,一眼就能定位问题本质:
** (Ecto.Query.CastError) deps/ecto/lib/ecto/repo/queryable.ex:382: value `1234` in `where` cannot be cast to type :string in query: from c0 in MyApp.DB.Client, where: c0.client_id == ^1234, select: c0
从栈跟踪能看到,错误出现在MyApp.DBImpl.add_update_client/1调用Repo.get_by/2查询client_id时:数据库里client_id定义的是:string类型,但传入的参数却是数值1234,类型不匹配导致转换失败。
问题定位
你提到问题只在测试中偶尔出现(约1/10概率),还怀疑和测试执行顺序有关——这恰恰是关键线索!正常运行时,所有传入的client_id都是符合预期的字符串类型,但测试场景里有个“漏网之鱼”偷偷传入了数值类型。
我排查后发现,问题出在之前写的doctest里:某个doctest示例中,不小心把client_id写成了数值1234,而不是字符串"1234"。因为测试执行顺序的随机性,这个错误输入偶尔会被触发,导致类型转换失败。
哪怕后来尝试用type(^client_id, :string)强制转换也没用?这是因为如果传入的client_id本身是数值,Ecto的type/2虽然会尝试转换,但如果代码后续有依赖类型的逻辑(比如changeset验证),还是可能出问题——而且最根本的问题是输入本身就不符合预期类型。
解决步骤
- 全面检查测试用例(包括doctest):遍历所有传入
client_id的测试代码,确保传入的是字符串类型(比如"1234"而不是1234)。 - 修正doctest中的错误输入:把数值类型的
1234改成字符串"1234",这一步直接解决了偶发的错误。 - 可选:添加前置类型校验:为了避免后续再出现类似问题,可以在
add_update_client/1函数开头添加类型检查,提前拦截错误输入:
这样能在测试阶段更早发现类型错误,不用等到Ecto查询时才抛出CastError。def add_update_client(client) do %{client_id: client_id} = client unless is_binary(client_id) do raise ArgumentError, "client_id must be a string, got: #{inspect(client_id)}" end client_db = DB.Client |> Repo.get_by(client_id: client_id) create_update_client(client_db, client) end
总结
这种偶发的测试错误,大多和测试数据的不一致性有关,尤其是doctest这类容易被忽略的地方。只要确保所有测试输入的类型和生产环境保持一致,就能彻底解决这类问题。
内容的提问来源于stack exchange,提问作者GenericJam

