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/*
, whereBASE_URL
is defined usingConfig
- During testing we use
msw
to intercept requests tohttp://localhost:3000
and return a mock response - Using
ConfigProvider
we define a static value forBASE_URL
that points tohttp://localhost:3000
- Using
Layer
we compose the staticConfigProvider
withPokeApi.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
!