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
PokeApilayer 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
Defaultimplementation 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 318msLet's recap what is happening here:
getPokemonmakes a request to${BASE_URL}/api/v2/pokemon/*, whereBASE_URLis defined usingConfig- During testing we use
mswto intercept requests tohttp://localhost:3000and return a mock response - Using
ConfigProviderwe define a static value forBASE_URLthat points tohttp://localhost:3000 - Using
Layerwe compose the staticConfigProviderwithPokeApi.Default - We run the program with
Effect.runPromiseand 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!
