Testing configuration with ConfigProvider

In the live version of the app BASE_URL is retrieved from process.env, which is the default configuration provider in effect.

package.json
{
  "name": "effect-getting-started-course",
  "version": "1.0.0",
  "main": "src/index.js",
  "author": "Sandro Maglione",
  "license": "MIT",
  "scripts": {
    "dev": "BASE_URL=https://pokeapi.co tsx src/index.ts",
    "tsc": "tsc",
    "test": "vitest"
  },
  "devDependencies": {
    "@types/node": "^20.14.10",
    "msw": "^2.3.1",
    "tsx": "^4.16.2",
    "typescript": "^5.5.3",
    "vitest": "^2.0.2"
  },
  "dependencies": {
    "@effect/platform": "^0.59.2",
    "@effect/schema": "^0.68.26",
    "effect": "^3.5.6"
  }
}

For testing instead we want to define a new provider that returns http://localhost:3000 instead of https://pokeapi.co.

All requests to http://localhost:3000 will be intercepted by msw that will return the mock response.

ConfigProvider: provider for configuration

Config is a service that retrieves primitive values based on a key:

export class PokeApi extends Context.Tag("PokeApi")<PokeApi, PokeApiImpl>() {
  static readonly Live = PokeApi.of({
    getPokemon: Effect.gen(function* () {
      // 👇 Get value with `BASE_URL` key using `Config`
      const baseUrl = yield* Config.string("BASE_URL");

      const response = yield* Effect.tryPromise({
        try: () => fetch(`${baseUrl}/api/v2/pokemon/garchomp/`),
        catch: () => new FetchError(),
      });

      if (!response.ok) {
        return yield* new FetchError();
      }

      const json = yield* Effect.tryPromise({
        try: () => response.json(),
        catch: () => new JsonError(),
      });

      return yield* Schema.decodeUnknown(Pokemon)(json);
    }),
  });
}

The source of the values passed to Config is defined with ConfigProvider.

For testing we usually define a new ConfigProvider that provides static values instead of accessing process.env.

We do this by using ConfigProvider.fromMap:

import { ConfigProvider } from "effect";

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

ConfigProvider.fromMap takes a Map of key-value pairs and returns a new ConfigProvider that provides the values defined in the map.

There are other ways to define a ConfigProvider, you can check the API reference to learn about them all.

Providing ConfigProvider as a layer

Since ConfigProvider is a service, we can wrap it in a layer to provide it to our program.

We use Layer.setConfigProvider to create a layer from a ConfigProvider:

import { ConfigProvider, Layer } from "effect";

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

const ConfigProviderLayer = Layer.setConfigProvider(TestConfigProvider);

With this we have ConfigProviderLayer that we can compose as a normal layer. We are going to provide this to our program next.