Catalin Ciubotaru avatar

Test Driven Development in an Angular World - Part 3

Here are part 1 and part 2 for some context.

What is a Smart Component

One of the recommended ways of structuring Angular applications is by using something called Smart and Dumb Components. Or Container and Presentation Components. They both mean the same thing:

  • You structure your app by having components that act as orchestrators. They use services, get data, process events, decide what to put on screen. These are the Smart/Container components.
  • You use small reusable components that don’t do any processing. They are in charge of showing data, bubbling events up, styling etc. These are the Dumb/Presentation components.

Testing the Dumb component is pretty straight forward. It’s even simpler than what we did while testing the FavoriteMovies component here. You just have to check that the correct information is displayed and that the events are bubbled up. You should not have to worry about any dependencies, modules etc.

Where we left off

So, in the previous articles we learned how to test a regular component and an HTTP Service. Let’s take this one step further by having a look at a Smart Component. We were using Angular with Jest, so let’s keep using that. We don’t wanna change frameworks every other week.

Architecture considerations

Now, before we go into code, let’s think a little about what we want our structure to be.

I propose this: let’s have one FavoriteMoviesComponent that shows all the favorite movies. This component will show 0 -> n FavoriteMovieComponents. This component will also show a search-movie section where a user would be able to search and add movies to his favorites list.

So, responsibility wise, the FavoriteMoviesComponent will know how to add a movie, delete a movie, show all movies.

Any objections? No? Good! Glad we agree.

Let’s proceed.

Generate the Dumb Components

From our scenario from above, we can identify 2 new components that are needed: The FavoriteMovie(Dumb) and the SearchMovie(let’s say Smart). To keep this post (relatively) short, we won’t care about their actual implementation. We just know they look like this:

@Component({
selector: 'kpd-favorite-movie',
templateUrl: './favorite-movie.component.html',
styleUrls: ['./favorite-movie.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FavoriteMovieComponent {

@Input() favoriteMovie: Movie;
@Output() deleteMovie = new EventEmitter<Movie>();

constructor() {}
}
@Component({
selector: 'kpd-search-movie',
templateUrl: './search-movie.component.html',
styleUrls: ['./search-movie.component.scss']
})
export class SearchMovieComponent {
@Output() addMovie = new EventEmitter<Movie>();

constructor() {}
}

Current state of the FavoriteMovies Component

Right now we already show all the favorite movies in our component template, but without the help of a reusable component. So, we want to refactor this part:

<ng-container *ngIf='(favoriteMovies$ | async); let favoriteMovies'>
<div class='movie' *ngFor='let movie of favoriteMovies'>
{{ movie.title }}
</div>
</ng-container>

We want to:

  1. Use our new FavoriteMovie component instead of just a div
  2. Use our new SearchMovie component on top of all this
  3. Listen to events from these components and act accordingly

So, let’s do this! Let’s use our new Dumb Component!

Waaaaaaaait! What’s the title of the article? TDD meaning Test Driven Development. Remember? Write tests, see them fail and THEN make them pass.

Good, let’s add a test that checks that our FavoriteMovies (Smart) Component shows the correct amount of FavoriteMovie (Dumb) Components.

it('should show all the favorite movies', () => {
const movieComponents = fixture.debugElement
.queryAll(By.directive(FavoriteMovieComponent));

expect(movieComponents.length).toEqual(favoriteMoviesToUse.length);
});
Image of the test failing

Failure! Don’t worry. It was meant to be. The error is pretty clear. It expected 3, it got 0. Now, let’s fix this.

<ng-container *ngIf='favoriteMovies$ | async; let favoriteMovies'>
<kpd-favorite-movie *ngFor='let movie of favoriteMovies'>
</kpd-favorite-movie>
</ng-container>

As you can see, we replaced the <div> with our reusable component. Now when we run the test again, we get this:

Image of the component not being recognized

This means that our TestModule does not know what the <kpd-favorite-movie> tag is. We have a couple of options.

  • We can add — schemas: [ NO_ERRORS_SCHEMA ] — to our TestBed configuration. This tells it to ignore anything it doesn’t know. I don’t really like this since it ignores part of the core responsibility of a Smart Component.
  • We can add the FavoriteMovieComponent to the declarations array in the TestBed Configuration. I don’t like this because if your Dumb component has dependencies, or if you rely on other Smart Components as well, you suddenly have to provide a ton of extra services and take care of those. This just pollutes your spec file since it’s not really related to the responsibility of the component you are currently testing.
  • We can use a library like ng-mocks to declare a MockComponent. This works for Components, Pipes, Directives etc. This will give you a fake component that has the shape of the actual component, without it’s implementation.

As you probably figured out, I usually go with the 3rd option.

So, after yarn add ng-mocks we add the mock component to our declarations:

declarations: [FavoriteMoviesComponent, MockComponent(FavoriteMovieComponent)],
Image of all the tests passing

Great! We have our first test that checks how our Smart Component uses Dumb Components. But it this enough?

Checking the details

Is this enough? Not really. Think about it. This test checks that the components are on the page, but doesn’t really check that they receive what they should. As a matter of fact, they don’t receive anything.

Let’s quickly complete the previous test.

it('should show all the favorite movies', () => {
const movieComponents = fixture.debugElement
.queryAll(By.directive(FavoriteMovieComponent));
expect(movieComponents.length).toEqual(favoriteMoviesToUse.length);

const movieComponentsInputs = movieComponents.map(
htmlComponent => (htmlComponent.componentInstance as FavoriteMovieComponent).favoriteMovie
);
expect(movieComponentsInputs).toEqual(favoriteMoviesToUse);
});

Here we check that all the inputs of the FavoriteMovie components sum up to all the favorite movies that we want to show. Of course, this is just one way of checking it. We could also just iterate one by one and check that it exists etc. Your choice on how to do this. The important thing is to check it.

Now, what do we do? We run the tests!

Image of the tests failing

No surprise here. It expected to pass the movies as inputs, but nothing was passed. The fix is pretty easy:

<ng-container *ngIf='favoriteMovies$ | async; let favoriteMovies'>
<kpd-favorite-movie
*ngFor='let movie of favoriteMovies'
[favoriteMovie]='movie'>
</kpd-favorite-movie>
</ng-container>

Running the test again shows us this:

Image of the tests passing

That GREEN man! Good stuff.

What about the events?

You’re right! We need to check the events as well. So, our FavoriteMovie component will emit an event when a movie is deleted. Our Smart component needs to listen to that and tell the FavoriteMoviesService to delete the specified movie. Again, we covered the testing/implementation of the HTTP Service here, so we won’t go through that again.

As usual, we start with the test:

it('should delete a favorite movie when the user wants to', () => {
jest.spyOn(favoriteMovieService, 'deleteMovie');
const favoriteMovieToDelete = favoriteMoviesToUse[0];

const componentToDelete = fixture.debugElement.queryAll(
By.directive(FavoriteMovieComponent)
)[0].componentInstance as FavoriteMovieComponent;

componentToDelete.deleteMovie.emit(favoriteMovieToDelete);
expect(favoriteMovieService.deleteMovie)
.toHaveBeenCalledWith(favoriteMovieToDelete);
});

A couple of things are happening here:

  • We spy on the delete method of the service
  • We get the component that we want to emit a delete event
  • We emit the delete event
  • We check that it was properly handled to the service

Now, running the test is of course: RED

Image of the test failing

Next station: GREEN!

How do we get there? We add the implementation. This consists of 2 steps: catching the event in the template and handling it in the component code.

<ng-container *ngIf='favoriteMovies$ | async; let favoriteMovies'>
<kpd-favorite-movie
*ngFor='let movie of favoriteMovies'
[favoriteMovie]='movie'
(deleteMovie)='deleteMovie($event)'
>
</kpd-favorite-movie>
</ng-container>

☝️ Here we listen to the event on the component.

deleteMovie(movie: Movie): void {
this.favoriteMovieService.deleteMovie(movie);
}

☝️ And here we tell the service that we want to delete that movie.

Now, if we run our tests again:

Image of the tests passing

Ok, now faster

So, we now have a fully tested flow with a Smart Component using a Dumb component. Let’s do the same for the SearchMovie Component.

We start with the tests, OF COURSE:

describe('SearchComponent', () => {
beforeEach(() => {
fixture.detectChanges();
});

it('should show the search component', () => {
const searchComponents = fixture.debugElement
.queryAll(By.directive(SearchMovieComponent));

expect(searchComponents.length).toEqual(1);
});

it('should add a movie when the user wants to', () => {
jest.spyOn(favoriteMovieService, 'addMovie');
const movieToAdd: Movie = { title: 'Joker' } as Movie;
const searchComponent = fixture.debugElement
.query(By.directive(SearchMovieComponent))
.componentInstance as SearchMovieComponent;

searchComponent.addMovie.emit(movieToAdd);

expect(favoriteMovieService.addMovie).toHaveBeenCalledWith(movieToAdd);
});
});

So, we added the MockComponent(SearchMovieComponent) to the declarations of the TestBed config and then we wrote the tests for that feature.

The template looks like this now:

<h1>Favorite movies</h1>

<kpd-search-movie (addMovie)='addMovie($event)'></kpd-search-movie>

<ng-container *ngIf='favoriteMovies$ | async; let favoriteMovies'>
<kpd-favorite-movie
*ngFor='let movie of favoriteMovies'
[favoriteMovie]='movie'
(deleteMovie)='deleteMovie($event)'
>
</kpd-favorite-movie>
</ng-container>

<div class='error' *ngIf='error'>
{{ error }}
</div>

And the last piece of the puzzle, the implementation for the addMovie method:

addMovie(movie: Movie): void {
this.favoriteMovieService.addMovie(movie);
}

And the test results:

Image of the tests passing

WOW! Aren’t tests beautiful?

And now we have a fully tested search feature(from the Smart Component point of view).

So, about those Reusable Components

Why go through all this trouble to use the reusable components when we can just as well put everything in one template. Well, a couple of reasons come to mind:

  1. They are super easy to test
  2. They are super easy to use in multiple pages
  3. They can/should use ChangeDetection.OnPush(big boost performance wise). Google it.
  4. They make the Smart Component easier to test and wire everything together.

Summary

So, if you just skipped through all this wall of text( I usually do that) at least keep this in mind: Reusable components are good for you, are easy to test and help a lot with performance 🏍 and maintenance.

Be kind to each other! 🧡

Over and out

My Twitter avatar
Catalin Ciubotaru 🚀

Test Driven Development in an Angular World - Part 3

Dec 10, 2019
314 people are talking about this

Wanna read more?

Updates delivered to your inbox!

A periodic update about my life, recent blog posts, how-tos, and discoveries.

No spam - unsubscribe at any time!