
From Angular to React & Back, Part 1
Cross-Framework Testing with Micro-Frontends
WHAT ❓
Cross-framework testing in micro-frontends environment
WHY❓
Increase test resilience to implementation & functional changes
HOW❓
Using test helper, assertable & the driver pattern
In a Nutshell 🥥
A demonstration of how micro-frontends implemented using different frameworks (react, angular) are tested using the same testing infrastructures.
When developed correctly, our tests need not be affected by implementation details, allowing us to test the UI in a black box manner.
Basically, we want to achieve decoupling between our test code, production code and testing framework
Shell and Micro-Frontends 🐚
We will be testing a simple application — a Pokédex.

We can click on a specific pokémon to get more information
We can browse through all Pokémons, filter them by type, or jump to a specific pokémon by name.
The application is composed of a shell (note the red header on top) and two micro frontends. The shell is hosting two MFEs: list & details.
list MFE (listing all pokémons) developed using Angular

The Pokémon details MFE, showing more detailed information about a pokémon, developed using React.

We will be using the PokéAPI server as our backend. All of our code resides in a single mono-repo (managed by Nx).
Block Diagram 📦
We have libraries implementing components and services, an application for each MFE, and a shell application

The shell depends on both MFEs, the details MFE depends on the pokemon-details library and the list MFE depends on the pokemon-list library
What about tests? The application’s end-to-end tests depend on the application, and the MFEs end-to-end tests. The test helpers library contains builder to generate random test data.

Let’s Run Some E2E Tests 🧪
Front End E2E tests refer to a helper robot that behaves like a user to click around the application and verify that it functions correctly.
We will start with the e2e tests on the pokédex application

What are we testing?
- First 10 pokémons should be displayed by default
- Correct Pokémon details should be displayed when clicking on More Info
- The next pokémon details should be displayed when next is clicked
Let’s continue with E2E tests on the Details MFE

What are we testing?
- Correct Pokémon details should be displayed
- First Pokémon (Bulbasaur) should be displayed if Pokémon name is not found
Moving on to the E2E tests on the List MFE.

What are we testing?
- First 10 pokémons should be displayed by default
- Pokémons should be filtered by type
Let’s Take a Look at the Test Code 🔬
We will start with the Pokédex application end to end tests implementation
describe('pokemon-e2e', () => {
const { beforeAndAfter, given, when, get } = new PokemonAppDriver();
beforeAndAfter();
beforeEach(() => {
when.navigateToHomePage();
});
it('should display 10 first pokeomns', () => {
then(get.list.search.card.numberOfCards()).shouldEqual(10);
});
it('show pokemon details when searching pokemon by name', () => {
when.list.header.typeName('charmeleon');
when.list.header.clickGo();
then(get.details.pokemonName()).shouldEqual('charmeleon');
});
it('show first pokemon details when searching non existing pokemon', () => {
when.list.header.typeName('whatever');
when.list.header.clickGo();
then(get.details.pokemonName()).shouldEqual('bulbasaur');
});
});
Before each test we navigate to the home page. We then test:
- That 10 first pokemons are displayed by default
- That searching a pokémon by name leads to the correct pokemon details
- That searching for an unknown pokémon, will lead to displaying details of the first pokémon
Things to note here:
- Each test tests one thing and tests it well
- There are no control flows in test code
- If a test fails — just looking at the test name in the test report can tell us what went wrong
If you are wondering where all those methods are (given, when, get) coming from — good! Stay tuned, it will all be clear soon enough
Moving on to the details MFE E2E test code
describe('when navigate to home page by name', () => {
beforeEach(() => {
when.navigateToHomePageByName('caterpie');
when.waitForPokemonName('caterpie');
});
it('pokemon name should be displayed', () => {
then(get.pokemonName()).shouldEqual('caterpie');
});
it('should render all abilities', () => {
then(get.attributes.numberOfAbilities()).shouldEqual(2);
});
it('should render ability name', () => {
then(get.attributes.pokemonAbilityText(1)).shouldEqual('run-away');
});
it('should render all types', () => {
then(get.attributes.numberOfTypes()).shouldEqual(1);
});
it('should render type name', () => {
then(get.attributes.pokemonTypeText(0)).shouldEqual('bug');
});
it('should render all moves', () => {
then(get.attributes.numberOfMoves()).shouldEqual(5);
});
it('should render move name', () => {
then(get.attributes.pokemonMoveText(2)).shouldEqual('snore');
});
it('when clicking next should show next pokemon', () => {
when.clickNext();
when.waitForPokemonName('metapod');
then(get.pokemonName()).shouldEqual('metapod');
});
it('when clicking prev should show prev pokemon', () => {
when.clickPrev();
then(get.pokemonName()).shouldEqual('blastoise');
});
});
Before each test we navigate to the home page, which is the details page. We then test:
- That the correct name is displayed
- The correct abilities are displayed
- The correct types are displayed
- The correct Pokémons are displayed when clicking Next/Prev
And as for the list MFE e2e tests:
describe('List MFE Integration Tests', () => {
let { beforeAndAfter, given, when, get } = new PokemonListAppDriver();
beforeAndAfter();
beforeEach(() => {
({ given, when, get } = new PokemonListAppDriver());
when.navigateToHomePage();
});
it('should display 10 first pokemons', () => {
then(get.search.card.numberOfCards()).shouldEqual(10);
});
it('should display 83 pokemons when selecting fairy type', () => {
given.spyOnPokemonsByTypeRequest();
when.header.clickTypesList();
when.header.typeType('fairy');
when.header.selectType(0);
when.waitForPokemonsByTypeResponse();
when.scrollToBottom();
then(get.search.card.numberOfCards()).shouldEqual(83);
});
});
Before each test we navigate to the home page, which is the list MFE page. We then test that:
- First 10 Pokémons should be displayed by default
- The correct number of Pokémons are displayed when filtering by type
Moving On to Integration Tests 🧪
Front End Integration tests refer to running the Front End application while mocking the server responses.
Let’s run detail MFE integration tests

What are we testing?
- That an outgoing request with the correct Pokémon name is being fired
- That the correct details are displayed
Note, as this is an integration test, we are using randomly generated test data, and not the actual data from the back end
Let’s run list MFE integration tests

What are we testing?
- That an outgoing request with the correct Pokémon type is being fired
- That the correct Pokémons are displayed
Same here, as this is an integration test, we are using randomly generated test data, and not the actual data from the back end
Let’s Take a Look at the Test Code 🔬
List MFE integration tests code:
describe('List MFE Integration Tests', () => {
let { beforeAndAfter, given, when, get } = new PokemonListAppDriver();
beforeAndAfter();
const chance = new Chance();
const NUMBER_OF_TYPES = 7;
const NUMBER_OF_POKEMONS = 12;
const types = Builder()
.count(NUMBER_OF_TYPES)
.results(
chance.n(
() => Builder().name(chance.word()).build(),
NUMBER_OF_TYPES
)
)
.build();
const type = aType(NUMBER_OF_POKEMONS);
const pokemons = aNamedAPIResourceList(NUMBER_OF_POKEMONS);
beforeEach(() => {
({ given, when, get } = new PokemonListAppDriver());
given.typesResponse(types);
given.pokemonsResponse(pokemons);
given.pokemonsByTypeResponse(type);
when.navigateToHomePage();
});
it('should fetch pokemons of selected type when selecting type', () => {
when.header.clickTypesList();
when.header.selectType(3);
then(get.pokemonsByTypeRequest()).shouldEndWith(types.results[3].name);
});
it('should display all type pokemons when selecting type', () => {
when.header.clickTypesList();
when.header.selectType(3);
when.scrollToBottom();
then(get.search.card.numberOfCards()).shouldEqual(NUMBER_OF_POKEMONS);
});
});
We are generating random test data in order to test the list mfe with no backend. Before each test we set the preconditions: intercepting outging http requests, and setting the responses. Only then we navigate to the home page.
We then test :
- That an outgoing http request with the correct type is fired
- That Pokémons are displayed according to the mock response we created.
Mocking the backend sever response (which we will not do during an E2E test), enables us to test edge cases, simulate errors, and test our application without the need of a fully functional backend server
And same goes for the details MFE integration tests:
describe('Pokemon Details Integration Tests', () => {
let { beforeAndAfter, given, when, get } = new PokemonDetailsAppDriver();
beforeAndAfter();
const chance = new Chance();
const id = chance.integer({ min: 1, max: 100 });
const pokemonResponse = aPokemon(id);
beforeEach(() => {
({ beforeAndAfter, given, when, get } = new PokemonDetailsAppDriver());
});
describe('when navigate to home page by name', () => {
beforeEach(() => {
given.image.mockImageResponse('default.png');
given.mockPokemonResponse(pokemonResponse);
when.navigateToHomePageByName(pokemonResponse.name);
});
it('should fetch pokemon by name', () => {
then(get.pokemonRequestUrl()).shouldEndWith(`/${pokemonResponse.name}`);
});
it('pokemon name should be displayed', () => {
then(get.pokemonName()).shouldEqual(pokemonResponse.name);
});
it('should render all abilities', () => {
then(get.attributes.numberOfAbilities()).shouldEqual(
pokemonResponse.abilities.length
);
});
it('should render ability name', () => {
const testFocus = chance.integer({
min: 0,
max: pokemonResponse.abilities.length - 1,
});
then(get.attributes.pokemonAbilityText(testFocus)).shouldEqual(
pokemonResponse.abilities[testFocus].ability.name
);
});
// More tests...
});
Before each test we are setting the preconditions using test data, we then test that the correct Pokémon information is displayed.
Things to note here: the test code is phrased as an acceptance criterion:
given , when , then expect should
Component Tests 🧪
When testing a UI component in isolation — it is referred to as Component test. We will mount the component, detached from the rest of the UI, while mocking dependencies such as services.
A word About Test Doubles 👯♀️
Spies and stubs are known as test doubles. Similar to how stunt doubles do the dangerous work in movies, we use test doubles to replace troublemakers and make tests easier to write.
Stubs are used as replacements for actual method and can be manipulated to return the values we want them to return during the test.
Spies, spy on functions, and can later be interrogated, to verify that methods that should have been called during the test, were actually called.
Testing a Component in Isolation 🔍
We are mounting the pokemon list component and testing its behavior, while mocking services and other dependencies

Pokemon list component is composed from subcomponents, which are also covered by component tests.

And same goes for Pokemon Details Component tests, including sub components. Each component is mounted into the browser and tested separately.

Let’s Take a Look at the Test Code 🔬
Let’s start with the details component.
describe('When rendering PokemonDetails component', () => {
let { beforeAndAfter, given, when, get } =
new PokemonDetailsComponentDriver();
beforeAndAfter();
const chance = new Chance();
const id = chance.integer({ min: 2, max: 100 });
const pokemon = aPokemon(id);
beforeEach(() => {
({ beforeAndAfter, given, when, get } =
new PokemonDetailsComponentDriver());
given.pokemon(pokemon);
when.render(PokemonDetailsComponent);
});
it('onNext should be called when Next is clicked', () => {
when.clickNext();
then(get.onNextSpy()).shouldHaveBeenCalledOnce();
});
it('onPrev should be called when Next is clicked', () => {
when.clickPrev();
then(get.onPrevSpy()).shouldHaveBeenCalledOnce();
});
it('pokemon name should be displayed', () => {
then(get.pokemonName()).shouldEqual(pokemon.name);
});
it('should render all abilities', () => {
then(get.attributes.numberOfAbilities()).shouldEqual(
pokemon.abilities.length
);
});
it('should render ability name', () => {
const testFocus = chance.integer({
min: 0,
max: pokemon.abilities.length - 1,
});
then(get.attributes.pokemonAbilityText(testFocus)).shouldEqual(
pokemon.abilities[testFocus].ability.name
);
});
});
We are generating random test data and setting the pre-conditions before each test, we mount the component, and then we test the component in isolation, using spies to verify events are emitted correctly.
And same goes with pokemon-list component, but as it is implemented using Angular, we need to prepare the test bed, and add the required providers — this is we inject mock services.
describe('Pokemon List Component Tests', () => {
const chance = new Chance();
const pokemon = Builder()
.name(chance.word())
.id(chance.integer({ max: 100, min: 1 }))
.build();
const types = chance.n(() => chance.word(), 10);
const { when, given, get, beforeAndAfter } = new PokemonListComponentDriver();
beforeAndAfter();
const testConfig = {
imports: [BrowserAnimationsModule, NoopAnimationsModule],
providers: [
{ provide: PokemonService, useValue: get.mock.pokemonService() },
{ provide: Router, useValue: get.mock.router() },
{
provide: ActivatedRoute,
useValue: {
root: Builder()
.routeConfig(Builder().build())
.build(),
},
},
],
};
beforeEach(() => {
given.search.card.picture.mockImageResponse('default.png');
when.render(PokemonListComponent, testConfig);
given.types(types);
given.pokemons([pokemon as { name: string; id: number }]);
});
it('given pokemon, should render pokemon name', () => {
then(get.search.card.pokemonNameText()).shouldEqual(pokemon.name);
});
it('when clicking more info, should navigate', () => {
when.search.card.clickMoreInfo();
then(get.navigateByUrlSpy()).shouldHaveBeenCalled();
});
it('when selecting pokemon type, should filter pokemon list', () => {
when.header.clickTypesList();
when.header.selectType(3);
then(get.filterByTypeNameSpy()).shouldHaveBeenCalledWith(types[3]);
});
it('when typing name and clicking go should navigate to pokemon details', () => {
when.header.typeName(pokemon.name);
when.header.clickGo();
then(get.navigateByUrlSpy()).shouldHaveBeenCalledWith(
'/details/name/' + pokemon.name
);
});
});
We then set the pre-conditions before each test, and then we test the component in isolation, using mock services to set the test data and spies to verify events are emitted correctly.
Stay tuned for Part 2!
In part 1 we showed the different test levels and the test code. Part 2 will reveal the testing infrastructures enabling this seamless way of testing components, MFEs and applications.
Resources
Happy Testing!