For quite some time now, integration testing has been one of the most useful ways to test our Angular applications. In the previous article of this Angular Testing series, I discussed the recent importance of component testing and how it not only helps enshrine the UI of our Angular components but also helps us create a cleaner architecture with less (or even no) business logic in our components.
Jordan Powell explains it best in his NX-Conf talk, specifically about the differences between component testing and end-to-end testing. He posits that component testing lives somewhere between Unit Testing and End-to-End testing. With Cypress, you can take the best features of integration tests and apply them to a single component.
I took it a step further in the Testing Revisited article and pinpointed exactly where it falls, directly between integration testing and unit testing. I don’t think I can rightfully post an article without giving a definition somewhere, and now is the best time to drop one. Merriam-Webster defines “integrate” as:
to form, coordinate, or blend into a functioning or unified whole
Or even better:
to incorporate into a larger unit
But most importantly, “to integrate something is to unit several pieces into a single functioning piece.” In the Angular sense, this would be the complete view of a particular page in our application.
Angular Architecture
Let’s take our concept from previous articles that an Angular component is a building block.
We’ve tested that our block has eight studs. The block is red, rectangular, and will hurt if someone steps on it with no shoes. It is logged in canon. We can grab it at any point and reuse it across our application. We are certain we know how it works and functions. It’s been well-tested and vetted in isolation.
So now let’s assume that this block is any component in our application. The block itself isn’t very fun and most of our end-users won’t be pleased with just a red Leggo block, now need to combine it with other pieces. As the creators of the red block, we have an intent on what we want to do with it, but that intent may not always come quickly to other builders. What if they try to place the studs on top of a different set of studs on a different block? What if the color isn’t the right color for this set?
In an Angular application, I subscribe to the school of atomic design. I prefer to have one component that manages the state of the current view. Think of it as a quarterback distributing the ball to the other players on the field. The results are the same whether I’m using a standalone component to represent the view or a module-based component. One component has a stateful provider that distributes that state via inputs to its children components. This makes testing and debugging far easier than having multiple stateful components. Let’s see what something like that looks like.
@Component({
selector: ‘app-pokedex’,
standalone: true,
template: `<main>
<team-viewer [team]=”pokemonStore.team$ | async”></team-viewer>
<ng-container *ngIf=”(pokemonStore.pokemon$ | async) as pokemonList”>
<ng-container *ngFor=”let pokemon of pokemonList”>
<pokemon-card [pokemon]=”pokemon”></pokemon-card>
</ng-container>
</ng-container>
</main>`,
imports: [
RouterLinkWithHref,
AsyncPipe,
NgIf,
NgForOf,
PokemonCardComponent,
TeamViewerComponent
],
providers: [
PokemonStore
],
styleUrls: [‘pokedex.component.scss’]
})
export class PokedexComponent {
constructor(public readonly pokemonStore: PokemonStore) {}
}
A “top-level” component like this could be component-tested, theoretically. But we get more out of it by testing how each piece is used to construct this page component. How does each piece work together to perform the intent of the page?
screenshot from pokemon.com/us/pokedex
Let’s assume this screenshot from pokemon.com is a fully fleshed-out view of the PokedexComponent with several smaller components that make up the page. It may have a card component that displays the pokemon’s image. That card may have several smaller components that display the pokemon’s number, name, and type. Those types may have a directive associated with them that changes the color of the type based on an enum. Each of those pieces would have its component test files, so there is no point in testing those again in PokedexComponent. Each individual piece is tested, but we don’t know how those pieces work together.
The Pokemon value provided from the PokedexComponent to the PokemonCard is the important piece here. Since we have a store provided to the PokedexComponent, we can change those values several times in our tests to test how ChangeDetection affects our child components. We could test how many pokemon are loaded at a given time if we wanted to lazy-load a certain amount of cards. We could test click-actions and what happens when we select a given pokemon card. What is the URL upon completion? Does the store change? What happens to other cards in the DOM when I click on a sibling card?
Mocking Data
One of the fun things about Integration tests is that we don’t care about live data (similar to a component test). With Cypress, and to a lesser extent with jasmine, we can intercept and stub out API calls with fixtures. The mocking portion is the most important part of Integration Tests. Instead of relying on the backend to provide data for our tests, we can create a fixture of data.
// Pokemon Type
export interface Pokemon {
name: string;
types: PokemonTypes[];
id: number;
image: string;
}// pokemon-list.json
[{
name: ‘Bulbasaur’,
types: [‘Grass’, ‘Poison’],
id: 1,
image: ‘http://pokemon.com/api/images/bulbasaur’
},{
name: ‘Ivysaur’,
types: [‘Grass’, ‘Poison’],
id: 2,
image: ‘http://pokemon.com/api/images/ivysaur’
}]
Let’s assume we have an array of Pokemon that receives from a backend API similar to what we have in the fixture data. We don’t want to test that over 700 pokemon properly display. We probably would like to see some different types and maybe enough to test the layout on different viewports, but this should get the point across for demonstration purposes.
it(‘should display a list of cards equal to the pokemon data received’, () => {
cy.intercept(
‘GET’,
`https://www.pokemon.com/us/api/pokedex/kalos`,
{ fixture: ‘pokedex/pokemon-list.json’ }
);
cy.visit(‘pokemon.com/us/pokedex’);
cy.get(‘pokemon-card’)
.should(‘have.length’, 2);
});
This test has a few important features that, with this being such a small demonstration, have a lot of abilities that we can use to get a ton of value in our workspace.
The first portion, the ‘cy.intercept’ takes three arguments: The request method, the request URL, and the mocked response. pokemon.com/us/pokedex receives all of its data from that ‘/kalos’ endpoint. We don’t want to call this endpoint; we want to test the data we’ve created. The endpoint called is using a GET request, so we want to tell the runner to look for a GET request at `/api/pokedex/kalos,` and when we make the request, we want to intercept that and send the data provided in the “pokemon-list.json” file.
In the next part, we tell cypress to navigate to “pokemon.com/us/pokedex.” If we’re testing as part of our development process, this would be against our localhost and the Pokedex route. When we land on that page, the data used will be the mocked data we created. Instead of showing all of the pokemon, we’d only see Bulbasaur and Ivysaur.
Result of mocking the endpoint
The interface doesn’t quite match up, but we do successfully see two cards, and we have the names working as intended. This is a fun start to an integration test; properly mocking the rest of the interface from the “/kalos” endpoint would make the images work as we want.
Why not just use E2E tests?
End-To-End tests are expensive. They take long to run since you’re making real back-end requests. We have to bank on the backend sending data, that said data hasn’t changed since we wrote our tests, and we can’t always create or modify existing data.
When we’re writing integration tests, we don’t have to worry about the backend. We’re free to test our major workflows and ensure that the UI will do what we expect with certain responses from the backend. Do we want to check a success case where the backend sends a statusCode of 200? We can mock it and test that workflow with mock data. We can complete a form, click the submit button, and simulate the response in a single test. Want to do the same thing but simulate a 404? A 500? All of these are possible without ever affecting any saved data in a database.
The greatest benefit we get from component testing is the fact that it lets us write more surgical integration tests. Instead of writing “hybrid-tests” that increase the cost of our integration tests, we can write tests solely testing a page’s workflow or primary features. If you have earlier build environments, you can test your workflows independently of a backend, you can save your end-to-end tests for Production, and ensure that your endpoints are returning the data that you expect.
Join us at ng-conf 2023!
ng-conf | June 14–15, 2023
Workshops | June 12–13, 2023
Location | Salt Lake City, UT
ng-conf 2023 is a two-day single-track conference focused on building the Angular community. Come be a part of this amazing event, and meet the Angular Team and the biggest names in the community. Learn the latest in Angular, build your network, and grow your skills.
Angular Testing: Integration Testing was originally published in ngconf on Medium, where people are continuing the conversation by highlighting and responding to this story.