Closed samhh closed 1 year ago
Since https://github.com/unsplash/sum-types/pull/45 we now have this behaviour (equality works for non-nullary sum types but not for nullary sum types):
import * as Sum from '@unsplash/sum-types';
import * as O from 'fp-ts/Option';
type Weather = Sum.Member<'Sun'> | Sum.Member<'Rain', number>;
const Weather = Sum.create<Weather>();
// Passes ✅
it('works', () => {
expect(O.some({ value: Weather.mk.Rain(5) })).toEqual(O.some({ value: Weather.mk.Rain(5) }));
});
// Fails ❌
/*
Expected: {"_tag": "Some", "value": {"value": [Function nonNullary]}}
Received: serializes to the same string
*/
it('works', () => {
expect(O.some({ value: Weather.mk.Sun })).toEqual(O.some({ value: Weather.mk.Sun }));
});
I just ran into the following scenario in which Jest's output is really confusing/misleading:
Sum.enumerateNullary
.Jest's diff output is confusing/misleading because the diff suggests that these two sum types are very different, but they are actually the same.
import * as Sum from 'shared/facades/Sum';
type MyUnion = Sum.Member<'A'>;
const MyUnion = Sum.create<MyUnion>();
it('test', () => {
const Enum = Sum.enumerateNullary<MyUnion>()(['A']);
expect(MyUnion.mk.A).toEqual({ prop: Enum[0] });
});
Output:
Expected: {"prop": {Symbol(@unsplash/sum-types internal tag key): "A", Symbol(@unsplash/sum-types internal value key): null}}
Received: [Function nonNullary]
Example of sum type inside an object:
import * as Sum from 'shared/facades/Sum';
type MyUnion = Sum.Member<'A'>;
const MyUnion = Sum.create<MyUnion>();
it('test', () => {
const Enum = Sum.enumerateNullary<MyUnion>()(['A']);
expect({ prop: MyUnion.mk.A }).toEqual({ prop: Enum[0] });
});
Output:
Object {
- "prop": Object {
- Symbol(@unsplash/sum-types internal tag key): "A",
- Symbol(@unsplash/sum-types internal value key): null,
- },
+ "prop": [Function nonNullary],
}
Side note: this isn't related to equality testing with Jest but this issue seems related, i.e. nullary sum types behave differently depending on how they were created.
Specifically with Web's enumerateNullary
, I think the discrepancy might be here. If we drop the (null as any)
it should match the proper constructor.
I don't know if that will allow Jest to match them, mind, given they're functions at that point.
This adds a custom toEq
matcher to Jest's expect
:
expect.extend({
toEq<A>(received: A, expected: A, eq: Eq<A>) {
return {
pass: eq.equals(received, expected),
message: () => 'Expected values to be equal.',
};
},
});
declare global {
namespace jest {
interface Matchers<R, T> {
toEq: (value: T, eq: Eq<T>) => R;
}
}
}
// Example:
expect(a).toEq(b, MyEq);
However, on its own this isn't much better than doing expect(eq.equals(a, b)).toBe(true)
. The failure message is too generic and unlike toEqual
it doesn't provide any information to aid debugging. We could improve the message using a Show
type class, but ideally we would show a diff of expected/received like toEqual
does:
it('works', () => {
expect({ foo: 1, bar: 2 }).toEqual({ foo: 1, bar: 3 });
});
expect(received).toEqual(expected) // deep equality
- Expected - 1
+ Received + 1
Object {
- "bar": 3,
+ "bar": 2,
"foo": 1,
}
import * as Sum from '@unsplash/sum-types';
import * as O from 'fp-ts/Option';
type Weather = Sum.Member<'Sun'> | Sum.Member<'Rain', number>;
const Weather = Sum.create<Weather>();
it('works', () => {
expect(O.some({ value: Weather.mk.Rain(5) })).toEqual(O.some({ value: Weather.mk.Rain(6) }));
});
- Expected - 1
+ Received + 1
Object {
"_tag": "Some",
"value": Object {
"value": Object {
Symbol(@unsplash/sum-types internal tag key): "Rain",
- Symbol(@unsplash/sum-types internal value key): 6,
+ Symbol(@unsplash/sum-types internal value key): 5,
},
},
}
I'm not sure how we could implement this for toEq
. For reference, here's the code for toEqual
.
Alternatively, seeing as toEqual
gives us diffs for free then we could solve this by converting nullary sum types into objects:
expect(f(a)).toEqual(f(b))
// We could alias this e.g. `expect(a).toEqualWithSumTypes(b)`
… where f
deeply traverses the object properties and replaces nullary sum type functions with regular objects and regular properties. This function might also help us address the issue with console.log
(https://github.com/unsplash/sum-types/issues/48), i.e. you'd be able to do console.log(f(x))
.
NB with the merged solution the following will still fail:
diff --git a/test/unit/index.ts b/test/unit/index.ts
index 142de4b..394c8c0 100644
--- a/test/unit/index.ts
+++ b/test/unit/index.ts
@@ -24,8 +24,9 @@ describe("index", () => {
it("are value-equal", () => {
type Weather = Member<"Sun"> | Member<"Rain", number>
const Weather = create<Weather>()
+ const Weather2 = create<Weather>()
- expect(Weather.mk.Sun).toEqual(Weather.mk.Sun)
+ expect(Weather.mk.Sun).toEqual(Weather2.mk.Sun)
expect(Weather.mk.Sun).not.toEqual(Weather.mk.Rain(123))
expect(Weather.mk.Rain(123)).toEqual(Weather.mk.Rain(123))
expect(Weather.mk.Rain(123)).not.toEqual(Weather.mk.Rain(456))
Essentially, the call to create
creates a unique identity/reference for the sum. With the current type-based API it's either this or allowing members of different sum types but the same tag names to come up equal. I think this is a lesser evil.
Of course, we can do away with a lot of this complexity by shifting nullary sums out of mk
, or requiring that they take null
or something. Maybe we should consider that.
Note also we still have this problem: https://github.com/unsplash/sum-types/issues/52.
More of a tracking issue than anything else.
toEqual
fails presumably because we're using symbols.toMatchObject
falsely passes for that reason.The current workaround is to test their serialized forms. A hypothetical testing framework that used
Eq
would workaround this issue.