Schema.Struct 上的 verifyRoundTrip 为什么失败?
new TestSchema.Asserts(Schema.Struct({ a: Schema.String })).verifyRoundTrip({ seed: 1 }) 这么简单的 Schema,在 effect 4.0.5 上报的是 Shrunk input: {"a":""}、Failure: returned false。
**原因不在你的 Schema,而在比较方式:verifyRoundTrip 用 Node 的 isDeepStrictEqual 比较,而 Struct 的生成器产出的对象原型是 null,解码结果是普通对象,原型不同就判为不等。**另一个原因是 Schema.Number 的生成器会产出 -0,往返后变成 0。
verifyRoundTrip 是 TestSchema.Asserts 的一个方法,做「编码再解码、与原值比较」的往返检查,整体用法见怎么用 TestSchema.Asserts 测 Effect Schema?。下面讲这两点怎么排查出来的、哪些情况不受影响,以及怎么绕开。
为什么相同字段的对象会被判为不等?
这是我实际排查出来的两点:
- Struct 的 Arbitrary 产出的对象原型是
null(Object.getPrototypeOf得到null),而解码结果是普通对象;isDeepStrictEqual会比较原型,所以即使字段完全相同也判为不等。 Schema.Number的生成器会产出-0,NumberFromString往返后变成0,同样被严格相等拒绝。
哪些 Schema 不受影响?
Schema.Class 的实例不受影响,verifyRoundTrip 对 Payment 这样的 Class 能通过;纯 string 这类原始 Schema 也能通过。所以出问题的范围比较窄:Struct,以及会产出 -0 的数值往返。
这是 bug 吗?
这是 4.0.5 的现状,我没有去查它是不是已被确认为缺陷,所以不替它下结论。如果你用的是别的版本,先用最小例子(上面那个 Schema.Struct({ a: Schema.String }))确认现象是否还在。
要往返测试 Struct,怎么绕开?
自己用 checkEffect 加 Schema.toEquivalence,比较规则由 Schema 决定,不依赖原型:
import { Arbitrary, Effect, Schema } from "effect"
const Wire = Schema.Struct({
id: Schema.Int,
at: Schema.DateTimeUtcFromString,
n: Schema.NumberFromString
})
const roundTrip = Effect.gen(function* () {
const eq = Schema.toEquivalence(Wire)
const result = yield* Arbitrary.checkEffect(
Arbitrary.schema(Wire),
(w) =>
Schema.encodeEffect(Wire)(w).pipe(
Effect.flatMap(Schema.decodeUnknownEffect(Wire)),
Effect.map((back) => eq(back, w))
),
{ runs: 200, seed: 1 }
)
console.log(result._tag) // Passed
})
Effect.runPromise(roundTrip)checkEffect 返回的是数据,不会自己让测试失败,所以放进 vitest 时要判断 _tag,或者用 formatCheckFailure 格式化后再断言。checkEffect 的用法见 Effect v4 不用 fast-check,属性测试怎么写?。
版本是 effect 4.0.5。Struct 往返失败的现象、Object.getPrototypeOf得到null、-0变0这两点排查,以及最后一段绕法代码得到Passed,都在这个版本上跑过。是否已被确认为缺陷、其它版本是否仍然如此,没有验证。