fp-ts 和笑话:Option 和 Either 的人体工程学测试?

fp-ts and jest: ergonomic tests for Option and Either?

我正在使用 fp-ts,我用 Jest 编写单元测试。在许多情况下,我正在测试可为 null 的结果,通常用 OptionEither 表示(通常是数组 finds)。如果结果是none(以Option为例),最符合人体工程学的方法是什么使测试失败,并且知道这个结果是一些?

这是我目前如何解决问题的示例:

function someFunc(input: string): Option.Option<string> {
  return Option.some(input);
}

describe(`Some suite`, () => {
  it(`should do something with a "some" result`, () => {
    const result = someFunc('abcd');

    // This is a fail case, here I'm expecting result to be Some
    if(Option.isNone(result)) {
      expect(Option.isSome(result)).toEqual(true);
      return;
    }

    expect(result.value).toEqual('abcd');
  });
});

但是必须提前 return 编写 if 不是很符合人体工学。

我也可以写一个 as 断言:

  // ...
  it(`should do something with a "some" result`, () => {
    const result = someFunc('abcd') as Option.Some<string>;

    expect(result.value).toEqual('abcd');
  });
  // ...

但缺点是我必须重写 some 的类型。在很多情况下,必须编写它很繁重,需要编写和导出接口仅用于测试目的(这也不符合人体工程学)。

有什么方法可以简化这种测试吗?

编辑:这是一个更接近真实情况的测试用例:


interface SomeComplexType {
  id: string,
  inputAsArray: string[],
  input: string;
}

function someFunc(input: string): Option.Option<SomeComplexType> {
  return Option.some({
    id: '5',
    inputAsArray: input.split(''),
    input,
  });
}

describe(`Some suite`, () => {
  it(`should do something with a "some" result`, () => {
    const result = someFunc('abcd');

    // This is the un-ergonomic step
    if(Option.isNone(result)) {
      expect(Option.isSome(result)).toEqual(true);
      return;
    }

    // Ideally, I would only need this:
    expect(Option.isSome(result)).toEqual(true);
    // Since nothing will be ran after it if the result is not "some"
    // But I can imagine it's unlikely that TS could figure that out from Jest expects

    // Since I now have the value's actual type, I can do a lot with it
    // I don't have to check it for nullability, and I don't have to write its type
    const myValue = result.value;

    expect(myValue.inputAsArray).toEqual(expect.arrayContaining(['a', 'b', 'c', 'd']));

    const someOtherThing = getTheOtherThing(myValue.id);

    expect(someOtherThing).toMatchObject({
      another: 'thing',
    });
  });
});

toNullable or toUndefined怎么样?给定 Option<string>, toNullable returns string | null.

import { toNullable, toUndefined } from "fp-ts/lib/Option";

it(`should do something with a "some" result`, () => {
  expect(toNullable(someFunc("abcd"))).toEqual("abcd");
});

expect(Option.isSome(result)).toEqual(true) 的问题是,类型保护 isSome 不能用于在 expect 的外部代码路径中缩小 result(参见 here控制流分析的工作原理)。

你可以使用更精简的 assertion functions 并将它们与 fp-ts 类型保护结合起来,例如:

import { isSome, Option } from "fp-ts/lib/Option"

function assert<T>(guard: (o: any) => o is T, o: any): asserts o is T {
  if (!guard(o)) throw new Error() // or add param for custom error
}

it(`a test`, () => {
  const result: Option<string> = {...}
  assert(isSome, result)
  // result is narrowed to type "Some" here
  expect(result.value).toEqual('abcd');
});

我不知道是否有使用类型保护签名来增强 Jest expect 函数类型本身的好方法,但我怀疑它简化了您的案例,而不是简单的断言或以上解决方案。

您可以像这样编写不安全的转换 fromSome

function fromSome<T>(input: Option.Option<T>): T {
  if (Option.isNone(input)) {
    throw new Error();
  }
  return input.value;
}

然后在测试中使用它

  it(`should do something with a "some" result`, () => {
    const result = someFunc('abcd');
    const myValue = fromSome(result);
    // do something with myValue
  });

这个问题在这一点上有点老,并且有一个公认的答案,但是有几个非常好的库使测试 fp-ts EitherOption 非常愉快。它们都很好用,我真的无法决定我更喜欢哪一个。

它们允许你这样写:

test('some test', () => {
  expect(E.left({ code: 'invalid' })).toSubsetEqualLeft({ code: 'invalid' })
})