2

I'm using fp-ts, and I write unit tests with Jest. In many cases, I'm testing nullable results, often represented with Option or Either (typically, array finds). What is the most ergonomic way to make the test fail if the result is none (taking Option as an example), and keep on going knowing that this result is some?

Here's an example of how I can solve the problem at the moment:

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');
  });
});

But having to write an if with an early return is not very ergonomic.

I could alternatively write an as assertion:

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

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

But the downside there is that I have to rewrite the some's type. In many cases, having to write it is heavy, requiring to write and export interface for the sole purpose of testing (which is not ergonomic either).

Is there any way to simplify this kind of test?

Edit: Here is a test case that's closer to real conditions:


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',
    });
  });
});
Gabriel Theron
  • 1,346
  • 3
  • 20
  • 49
  • I'm not sure if I follow. what is valid case? `Option.isNone(result)` is `true`? or what? – skyboyer Jan 07 '20 at 14:16
  • I've updated the description to hopefully make it clearer. This test case assumes that the result should be `some` (as per the content of the function) – Gabriel Theron Jan 07 '20 at 14:21
  • `expect(Option.isSome(result)).toEqual(true)`? – Lee Jan 07 '20 at 14:23
  • @Lee That's a workaround to have a nicer error while running tests. I am indeed expecting `Option.isSome(result)` to be `true` since this test expects a `some`! But if I only wrote this assertion, while the test would run fine (fail on this assertion), Typescript would be unhappy with the next line, because it would not consider `result` to be a `some` simply because of the assertion. – Gabriel Theron Jan 07 '20 at 14:27
  • You could use [`elem`](https://gcanti.github.io/fp-ts/modules/Option.ts#elem-function): `expect(elem('abcd', result)).toEqual(true)`. – Lee Jan 07 '20 at 14:34
  • This indeed works, as long as my tests are writable with `Eq`'s functions. I see two significant downsides in my case: 1) it forces me to use `Eq`'s functions instead of jest's, which are more idiomatic in my codebase 2) many of my cases test complex objects in various ways, using data like ids to find other objects. This kind of thing is not feasible with `elem` – Gabriel Theron Jan 07 '20 at 14:51

3 Answers3

2

How about toNullable or toUndefined? Given 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");
});

The problem with expect(Option.isSome(result)).toEqual(true) is, type guard isSome cannot be used to narrow result in the outer code path of expect (see here how control flow analysis works).

You can use assertion functions which are a bit leaner and combine them with fp-ts type guards, e.g.:

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');
});

I don't know if there is a good way of augmenting Jest expect function type itself with a type guard signature, but I doubt it simplifies your case instead of a simple assertion or above solutions.

halfer
  • 19,824
  • 17
  • 99
  • 186
ford04
  • 66,267
  • 20
  • 199
  • 171
  • It will indeed work for one test, but it doesn't make having several tests on the resulting value easier, which is what I'm aiming at. I've added an example that's closer to real tests in the question body. – Gabriel Theron Jan 07 '20 at 16:49
2

You can write an unsafe conversion fromSome like so:

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

And then use it in the tests

  it(`should do something with a "some" result`, () => {
    const result = someFunc('abcd');
    const myValue = fromSome(result);
    // do something with myValue
  });
MnZrK
  • 1,330
  • 11
  • 23
  • That sounds very good! The only downside is the error doesn't show in the flow of the test, but since the line is displayed by Jest, it's no trouble to see where it went wrong. – Gabriel Theron Jan 13 '20 at 09:17
  • 1
    What if you just return none instead of throwing the error? See my suggestion here: https://github.com/gcanti/fp-ts/issues/1096 – Kai Jan 21 '20 at 04:47
  • Your suggestion is exactly the same as toUndefined. And that was the original answer from @ford04. – MnZrK Jan 22 '20 at 19:53
1

This question is a little old at this point and has an accepted answer, but there are couple really nice libraries that make testing fp-ts Either and Option really pleasant. They both work really well and I can't really decide which I like better.

They allow you to write things like this:

test('some test', () => {
  expect(E.left({ code: 'invalid' })).toSubsetEqualLeft({ code: 'invalid' })
})
rjhilgefort
  • 453
  • 4
  • 12
  • Both are still actively maintained. jest-fp-ts provides slightly more different matchers and personally find the wording better, for instance: `.toBeLeft()`; `.toBeRight()`; `.toEqualLeft(value)`; `.toEqualRight(value)`; `.toStrictEqualLeft(value)`; `.toStrictEqualRight(value)`; `.toSubsetEqualLeft(value)`; `.toSubsetEqualRight(value)`. – Shmygol May 06 '22 at 14:34