Catalin Ciubotaru avatar

Test Driven Development in an Angular World - Part 1

Wait! What is TDD?

So, you keep hearing about this TDD thing and want to start using it but have no idea why. Don't worry, the internet to the rescue! But first, what is TDD?

TDD stands for Test Driven Development and is a way of writing code. It means that you first write your test, you see it fail, and then you write the code that makes it pass. This is also called the red-green approach. Long story short, it's Development Driven by Tests.

Small example: Let's say you want to write a method that adds 2 numbers. Now, instead of implementing the method, you write the test first:

it('should add the values', () => {
const result = addValues(2, 4);

expect(result).toBe(6);
})

let addValues = (value1, value2) {}

Now, you run the test, and you see it fail:

Image of the test failing

Only now, you know what the problem is, so you can implement the method:

it('should add the values', () => {
const result = addValues(2, 4);

expect(result).toBe(6);
})

let addValues = (value1, value2) {
return value1 + value2;
}

And now, the test passes:

Image of the test passing

Now, that’s quite satisfying, right?

So, this does a few things:

  • Makes sure your code is tested, so you don’t have to spend the last 2 days of the sprint writing tests for all the code that you wrote( which never happens )
  • Makes sure you write code in a pragmatic way. There is a thing called YAGNI(You aren’t going to need it) which is about developers over-thinking and trying to solve climate change and world hunger with the next bit of code. Safe to say, most often than not, this makes the code overcomplicated without any real benefit.
  • When you finished writing a feature this way, you saw all the tests pass, that gives you confidence that if/when you refactor if the tests are still passing, then you didn’t break anything. How often were you afraid of breaking the product with your simple Pull Request?
  • Last but not least, it’s all about the colors! Seeing colors makes us happy. Seeing something failing and then passing, makes us even happier.

3 months and 500 tests later, we have a real sense of confidence over the product that we are building.

The keyword here is: CONFIDENCE

Now, back to Angular and our component

So, now you have an idea about what TDD is. Next stop, using it in our component. For this post, I am assuming you have a basic understanding of Angular, so I won’t go into details on how it works. I’ll be using Jest for my tests but the principles are the same.

For our example, let use a fictitious app that lets us create lists with movies. Now, somewhere in space and time, we decide that we also want a page where a user can see their favorite movies. So, without further ado, let’s build it!

First stop, let’s generate the component: ng g c favorite-movies

Now, this will generate a component. So, let’s see, what do we want to do first? We want to add a title like Favorite movies. Let’s do it!

WAAAAIT! What did we discuss? Write tests, then write the code that fixes it.

So, what would that look like? Something like this:

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

it('should have a title', () => {
const titleElements = fixture.debugElement.queryAll(By.css('h1'));

expect(titleElements.length).toBe(1);
expect(titleElements[0].nativeElement.innerHTML).toBe('Favorite movies');
});
});

We created a new describe block for Rendering and there we added a new test that checks that there is an h1 tag and that it has the correct content. Now, we run the test and, big surprise, it fails! Don’t fret, we can fix it:

<h1>Favorite movies</h1>

We added this to our HTML template and now our test passes!

Congratulations! You wrote your first bit of code in a TDD way. Take some time to pat yourself on the back. Good job! Now stop it. We have more work to do.

Next, we want to test that, given a list of movies, they are shown in the HTML. So, what do we do? We write a test for it!

const favoriteMoviesToUse: Movie[] = [
{ title: 'Interstellar' } as Movie,
{ title: 'The big Lebowski' } as Movie,
{ title: 'Fences' } as Movie
];

describe('FavoriteMoviesComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(FavoriteMoviesComponent);
component = fixture.componentInstance;
component.favoriteMovies = favoriteMoviesToUse;
});

describe('Render', () => {
it('show all the favorite movies', () => {
const movieElements = fixture.debugElement.queryAll(By.css('.movie'));

expect(movieElements.length).toBe(favoriteMoviesToUse.length);
});

it('should show the movie titles', () => {
const movieElements = fixture.debugElement.queryAll(By.css('.movie'));

movieElements.forEach((movieElement: DebugElement, index) => {
expect(movieElement.nativeElement.innerHTML)
.toContain(favoriteMoviesToUse[index].title);
});
});
})

Here we added a new @Input() property to the component, we created a new list of movies, and we passed those as input to the component. Then, we are testing that the rendered HTML contains the correct amount of elements with the class movie and that all the movie titles are displayed.

Of course, this test fails. So, let’s make it pass:Of course, this test fails. So, let’s make it pass:

<div class='movie' *ngFor='let movie of favoriteMovies'>
{{ movie.title }}
</div>

This is what we add to our template. Yei! Tests pass now!

Neeeeext!

What if our data actually comes from a service asynchronously? Let’s adjust our test for that:

describe('FavoriteMoviesComponent', () => {
let component: FavoriteMoviesComponent;
let fixture: ComponentFixture<FavoriteMoviesComponent>;
let favoriteMovieService: FavoriteMoviesService;

beforeEach(() => {
fixture = TestBed.createComponent(FavoriteMoviesComponent);
component = fixture.componentInstance;
favoriteMovieService = TestBed.get(FavoriteMoviesService);
jest.spyOn(favoriteMovieService, 'getFavoriteMovies')
.mockReturnValue(of(favoriteMoviesToUse));
});

describe('Getting the movies', () => {
it('should get the movies from the service', () => {
fixture.detectChanges();

expect(favoriteMovieService.getFavoriteMovies).toHaveBeenCalled();
});
});
});

Now, our tests assume a service is injected. What we do is mock the response and check that the service is called. Tests fail, let’s fix them!

export class FavoriteMoviesComponent implements OnInit {
favoriteMovies$: Observable<Movie[]>;

constructor(private favoriteMovieService: FavoriteMoviesService) {}

ngOnInit() {
this.favoriteMovies$ = this.favoriteMovieService.getFavoriteMovies();
}
}

Now our component instead of having an @Input(), uses a service. The last puzzle piece is the template:

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

So, now we have a fully working component that gets data from a service and renders it. What else do we need? Nothing! Time to go home and play some Red Dead Redemption 2

WAAAAAIT! What if there are errors? Ok, ok, let write a few tests to make sure that in case something fails, it is properly handled:

it('should show an error if getting the movies fail', () => {
const errorToThrow = 'User not found';
jest
.spyOn(favoriteMovieService, 'getFavoriteMovies')
.mockReturnValue(throwError(errorToThrow));

fixture.detectChanges();
const errorElement = fixture.debugElement.queryAll(By.css('.error'));

expect(errorElement.length).toBe(1);
expect(errorElement[0].nativeElement.innerHTML).toContain(errorToThrow);
});

it('should not show an error if getting the movies succeeds', () => {
fixture.detectChanges();

const errorElement = fixture.debugElement.queryAll(By.css('.error'));
expect(errorElement.length).toBe(0);
});

So, we want to make sure that the error is displayed if getting the favorite movies fails, and it is hidden if everything goes according to plan.

What we need is to catch the error in the component:

ngOnInit() {
this.favoriteMovies$ = this.favoriteMovieService.getFavoriteMovies()
.pipe(catchError((error: any) => {
this.error = error;
return of([]);
})
);
}

And to show the error in the template:

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

Now, after all our hard work we have a fully working component that uses an async service to get data, handles errors and renders everything that we expect it to.

Now, time for some eye candy:

Image of all tests passing

Pretty cool right?

Now, in order for this to work, you don’t need to implement the service. It just needs to exist and have a method that is called getFavoriteMovies.

export class FavoriteMoviesService {
constructor() {}
getFavoriteMovies(): Observable<Movie[]> {
return of([]);
}
}

Wrap-up

I know, I know, lots of words. I tried to be as succinct as I could without omitting any important information. Easier said than done. This is my first Medium article, so, I’m still learning. Hopefully, in the next one, we’ll talk about testing an Angular Service. TDD of course.

Hope you have enjoyed this program 📺. See you in the next one!

Be kind to each other! 🧡

Over and out

My Twitter avatar
Catalin Ciubotaru 🚀

Test Driven Development in an Angular World— Part 1

Nov 25, 2020
11 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!