
Depending on the Context — Dependency Injecting Mocks Using @lit/context
Depending on the Context — Dependency Injecting Mocks Using @lit/context
Dependency injection is one of the implementation techniques of Inversion of Control (IoC), allowing to communicate through contracts, thus making it easier to switch between different implementations (decoupling).
In the context of testing and isolation, dependency injection is useful for injecting mocks to components during Unit/Component tests.
While Angular has built in dependency injection, React and Lit leave it to the developer to choose the dependency injection method. Let’s explore one of those options: using context.
Let’s Render Some Pokémons

Consider the following Lit component, rendering pokémons:
import { PokemonService } from "@services/pokemon.service";
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
@customElement("pokemon-component")
export class PokemonCatalog extends LitElement {
@state()
pokemon!: {
next: string | null;
previous: string | null;
results: { name: string }[];
};
@state()
pokemonIndex: number = 1;
private pokemonService = new PokemonService();
override connectedCallback() {
super.connectedCallback();
this.loadPokemon();
}
loadPokemon = async () => (this.pokemon = await this.pokemonService.getPokemonByOffset());
loadNext = async () => {
this.pokemon = await this.pokemonService.getPokemon(this.pokemon.next);
this.pokemonIndex += 1;
};
loadPrev = async () => {
this.pokemon = await this.pokemonService.getPokemon(this.pokemon.previous);
this.pokemonIndex -= 1;
};
protected override render() {
return !this.pokemon
? ""
: html`
data-hook="pokemon-image"
src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${this.pokemonIndex}.png"
/>
${this.pokemon.results[0].name}
Prev
Next
`;
}
}
This component is using a service to fecth pokemon data. The service’s implementation is pretty straight forward:
export class PokemonService {
private baseUrl = "https://pokeapi.co/api/v2/pokemon";
getPokemon = async (url: string | URL) => await (await fetch(url)).json();
getPokemonByOffset = async (offset: string = "0") => {
const params = new URLSearchParams({ limit: "1", offset });
const fetchUrl = new URL(this.baseUrl);
fetchUrl.search = params.toString();
return this.getPokemon(fetchUrl);
};
}
BUT, the component is creating an instance of PokemonService internally. therefore, we cannot test this component in isolation as we cannot pass a mock service during the test.
Property Level Injection
We can easily change the design so that the Pokémon component will get the service as an input property, like so:
@property({ attribute: false })
private pokemonService?: PokemonService;
We will have to pass the service from our main compoent, PokemonApp, which resides in main.ts:
@customElement("pokemon-app")
export class PokemonApp extends LitElement {
pokemonService: PokemonService = new PokemonService();
protected override render() {
return html``;
.pokemonService="${this.pokemonService}"
>
}
}
Testing in Isolation
Now, we can pass a mock service to the component during the test.
Test Driver
First we will create a test driver, following the driver pattern. We will be using Cypresshelper and CypressLitComponentHelper. We will create a mock PokemonService, using CypressHelper, which uses ts-stubber (a generic stub creator) to create a stubbed instance of the service.
import { CypressHelper } from "@shellygo/cypress-test-utils";
import { CypressLitComponentHelper } from "@shellygo/cypress-test-utils/lit";
import { html } from "lit";
import { PokemonList, PokemonService } from "../../services/pokemon.service";
import { Pokemon } from "./pokemon.component";
export class PokemonComponentDriver {
private helper = new CypressHelper({ defaultDataAttribute: "data-hook" });
private componentHelper = new CypressLitComponentHelper();
private pokemonServiceMock = this.helper.given.stubbedInstance(PokemonService);
beforeAndAfter = () => {
this.helper.beforeAndAfter();
};
given = {
pokemon: (value: { next: string | null; previous: string | null; results: { name: string }[] }) => {
this.pokemonServiceMock.getPokemon.returns(value);
this.pokemonServiceMock.getPokemonByOffset.returns(value);
}
};
when = {
render: (element: Pokemon) => {
this.componentHelper.when.unmount(element);
this.componentHelper.when.mount(
element,
html` `
);
},
clickNext: () => this.helper.when.click("next"),
clickPrev: () => this.helper.when.click("prev")
};
get = {
nameText: () => this.helper.get.elementsText("pokemon-name"),
mock: { pokemonService: () => this.pokemonServiceMock }
};
}
Adding a Test
describe("PokemonCatalogComponent Tests", () => {
const chance = new Chance();
const { when, given, get, beforeAndAfter } = new PokemonComponentDriver();
beforeAndAfter();
describe("given pokemon data", () => {
const name = chance.word();
const url = chance.url();
const pokemon = {
results: [{ name, url }],
next: url,
previous: url
};
beforeEach(() => {
given.pokemon(pokemon);
when.render(new Pokemon());
});
it("should render pokemon name", () => {
then(get.nameText()).shouldEqual(name);
});
});
});
Running the Test
when trying to run test, we get the following error: currentDirective._$initialize is not a function.

What is Happening Here?
Apparently lit adds method when we pass a property object, and as we pass a mock object, those methods don’t exist. One way to overcome this, is pass a method for getting the service, instead of the service itself. This will require some changes in the implementation of pokemon-component:
@property({ attribute: false })
private getPokemonService?: () => PokemonService;
private pokemonService: PokemonService;
override connectedCallback() {
super.connectedCallback();
this.pokemonService = this.getPokemonService();
this.loadPokemon();
}
As well as some changes in the main PokeonApp:
@customElement("pokemon-app")
export class PokemonApp extends LitElement {
pokemonService: PokemonService = new PokemonService();
protected override render() {
return html``;
.getPokemonService="${() => this.pokemonService}"
>
}
}
And in the test driver, we have to pass the mock service when rendering the component:
when = {
render: (element: Pokemon) => {
this.componentHelper.when.unmount(element);
this.componentHelper.when.mount(
element,
html``
);
},
clickNext: () => this.helper.when.click("next"),
clickPrev: () => this.helper.when.click("prev")
};
But now, our test will pass:

Let’s Add a Context
Consider an application where multiple components are depending on the same service (like a logger). Instead of injecting the service to each component individually, we can use a context.
@lit/context is very similar to React’s Context, or to dependency injection systems like Angular’s. Using context involves a context object, a provider and a consumer, which communicate using the context.
Context Definition
for the implementation of the provider, we will use createContext from @lit/context:
import { createContext } from "@lit/context";
export const pokemonServiceContext = createContext("__pokemon_context__");
Context Provider
We will create the context provider in our PokemonApp component.
import { provide } from "@lit/context";
@customElement("pokemon-app")
export class PokemonApp extends LitElement {
@provide({ context: pokemonServiceContext })
pokemonService: PokemonService = new PokemonService();
protected override render() {
return html``;
}
}
Context Consumer
In pokemon-component we will consume the service from the context provider, like so:
@consume({ context: pokemonServiceContext })
@property({ attribute: false })
private pokemonService?: PokemonService;
Injecting a Mock Service
Now all we have to do is inject the mock service during the test
import { provide } from "@lit/context";
const pokemonServiceMock = new CypressHelper().given.stubbedInstance(PokemonService);
@customElement("pokemon-service-provider")
export class PokemonServiceProvider extends LitElement {
@provide({ context: pokemonServiceContext })
pokemonService: PokemonService = pokemonServiceMock;
protected override render() {
return html``;
}
}
.
.
.
when = {
render: (element: Pokemon) => {
this.componentHelper.when.unmount(element);
this.componentHelper.when.mount(
element,
html``
);
},
clickNext: () => this.helper.when.click("next"),
clickPrev: () => this.helper.when.click("prev")
};
Running the Test
the test will pass, and we can see the getPokemonByOffset stub is being called during the test.

Summary
We explored different ways to inject dependencies to a lit component, the first of which was property level injection, where we stumbled upon a caveat when passing a mock object as an object property and found a way to overcome it. We then experimented with using @lit/context for dependency injection and injected a mock service during the test.