Defining and running tests

We want to verify the correct behavior of getPokemon:

const program = Effect.gen(function* () {
  const pokeApi = yield* PokeApi;
  return yield* pokeApi.getPokemon;
});

We write a simple test that checks that getPokemon returns the expected response:

  • Provide the PokeApi layer implementation to test (PokeApi.Default)
  • Run the effect with Effect.runPromise
  • Verify that the response is equal to the expected one
const program = Effect.gen(function* () {
  const pokeApi = yield* PokeApi;
  return yield* pokeApi.getPokemon;
});

// 👇 Provide the `PokeApi` live implementation to test
const main = program.pipe(Effect.provide(PokeApi.Default));

it("returns a valid pokemon", async () => {
  const response = await Effect.runPromise(main);
  expect(response).toEqual({
    id: 1,
    height: 10,
    weight: 10,
    order: 1,
    name: "myname",
  });
});

We want to verify that the Default implementation is correct, since this is the implementation that is used in production.

This won't work yet because the default configuration tries to access process.env, which is not available in the test environment.

FAIL  src/index.test.ts > returns a valid pokemon
{
  _id: 'FiberFailure',
  cause: { _id: 'Cause', _tag: 'Fail', failure: { _tag: 'FetchError' } },
  stacks: []
}

 Test Files  1 failed (1)
      Tests  1 failed (1)
   Start at  16:18:44
   Duration  512ms (transform 74ms, setup 0ms, collect 350ms, tests 14ms, environment 0ms, prepare 39ms)

That's where ConfigProvider comes in!

Adding ConfigProvider to the program

We want to provide a ConfigProvider to PokeApi.Default so that it can retrieve BASE_URL from the static map we defined earlier.

This is no different from the usual layer composition we learned in the previous module:

const TestConfigProvider = ConfigProvider.fromMap(
  new Map([["BASE_URL", "http://localhost:3000"]])
);

const ConfigProviderLayer = Layer.setConfigProvider(TestConfigProvider);

const MainLayer = PokeApi.Default.pipe(
  // 👇 Provide the `ConfigProvider` layer to `PokeApi.Live`
  Layer.provide(ConfigProviderLayer),
);

We then use MainLayer to run the program:

const TestConfigProvider = ConfigProvider.fromMap(
  new Map([["BASE_URL", "http://localhost:3000"]])
);

const ConfigProviderLayer = Layer.setConfigProvider(TestConfigProvider);
const MainLayer = PokeApi.Default.pipe(Layer.provide(ConfigProviderLayer));

const program = Effect.gen(function* () {
  const pokeApi = yield* PokeApi;
  return yield* pokeApi.getPokemon;
});

const main = program.pipe(Effect.provide(MainLayer));

it("returns a valid pokemon", async () => {
  const response = await Effect.runPromise(main);
  expect(response).toEqual({
    id: 1,
    height: 10,
    weight: 10,
    order: 1,
    name: "myname",
  });
});

And now the test is passing:

pnpm run test
✓ src/index.test.ts (1)
   ✓ returns a valid pokemon

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  16:21:58
   Duration  318ms

Let's recap what is happening here:

  • getPokemon makes a request to ${BASE_URL}/api/v2/pokemon/*, where BASE_URL is defined using Config
  • During testing we use msw to intercept requests to http://localhost:3000 and return a mock response
  • Using ConfigProvider we define a static value for BASE_URL that points to http://localhost:3000
  • Using Layer we compose the static ConfigProvider with PokeApi.Default
  • We run the program with Effect.runPromise and verify that the response is equal to the expected one (mock)

This module is meant as an introduction to testing with effect services. There is a lot more that you can explore to make this setup even more composable.

One problem now is that we need to use Effect.provide(MainLayer) every time we want to run any program. This becomes tiresome and difficult to maintain.

Ideally we want all the resources to run the app organized in a single place that we can reuse as many times as we want.

That's next: Runtime!