DEV Community

Catalin Ciubotaru
Catalin Ciubotaru

Posted on

Test Driven Development in an Angular World

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) {}
Enter fullscreen mode Exit fullscreen mode

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

Test fails

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;
}
Enter fullscreen mode Exit fullscreen mode

And now, the test passes:
Test passes

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? Feels something like this:

Bus reporter

  • 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 his/her favorite movies. So, without further ado, let’s build it!

First stop, let’s generate the component:

ng g c favorite-movies
Enter fullscreen mode Exit fullscreen mode

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');
    });
  });
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

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:

<div class="movie" *ngFor="let movie of favoriteMovies">
  {{ movie.title }}
</div>
Enter fullscreen mode Exit fullscreen mode

This is 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();
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
    });
Enter fullscreen mode Exit fullscreen mode

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([]);
      })
    );
  }
Enter fullscreen mode Exit fullscreen mode

And to show the error in the template:

<div class="error" *ngIf="error">
  {{ error }}
</div>
Enter fullscreen mode Exit fullscreen mode

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:

All tests pass

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.

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 Dev.to 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!

https://twitter.com/_utukku

Top comments (11)

Collapse
 
didirare profile image
didi-rare

Hi Catalin,

Great article you put up here, i am quite new to TDD and your article helped clarify somethings.

However in the farouriteMovieService test section there is the "jest" variable you used, and i was wondering how you got it. Is it from a library you added to the project?

Collapse
 
cthulhu profile image
Catalin Ciubotaru

Hello didi-rare!

Glad to hear that it’s helpful!
So, jest is an alternative to karma. It’s a test suite.
The variable comes from the “jest” package.
Let me knowing you need more info.

Collapse
 
didirare profile image
didi-rare

Hi,

Thanks for your reply, I figured Jest is a test suite when i did some research, i had installed and configured it following their guide, however the test fails when i run it. I get this error message "error TS2304: Cannot find name 'jest'."

I had installed the jest package, @types/jest, jest-preset-angular and added jest in types [] in all tsconfig.*.json files and i still get the same error.

Do you have any pointers?

I use Webstorm IDE.

Thread Thread
 
cthulhu profile image
Catalin Ciubotaru

So, all I have it this in my tsconfig.spec.json:

"types": ["jest", "node"]

Keep in mind they are not needed in all the others tsconfig.*.json files.

Also make sure you also install jest, not just jest-preset-angular.
So, you need jest, @types/jest and jest-preset-angular; then add the "jest" types to tsconfig.spec.json and that should be it.

Sorry for the late answer. Holiday more :)

Collapse
 
siddrc profile image
Siddharth Roychoudhury

You have not spoken about e2e, which actually is the place where TDD originates from in-case of front-end, where e2e tests the entire page , where page is filled with components.
So we start by writing the spec for the entire page, and then slice out the spec into much smaller pieces and draw out the unit tests/spec for each component.
After writing one e2e spec, we see it fail, then move on to individual component unit tests one by one in the same flow as you mentioned.Once the unit tests pass, we then check e2e is passing or not. Unit tests check the component in isolation, e2e check entire pages or user flows from page to page and test entire frontend application.May be I will add a link here for E2E Testing in Angular using TDD approach.

Collapse
 
muhammedmoussa profile image
Moussa

I think e2e doesn't mean exactly fronted only, this case suits should cover case from the user interaction in the UI to server request till server response also.
Another point discussed tests can be considered as an integration test not only unit test which is one of its goals handle integration between components logic and template.

Collapse
 
cthulhu profile image
Catalin Ciubotaru

The concept is the same. It applies to unit tests, e2e tests etc.
This was mainly to show a simple approach to TDD and to demystify the difficulty of it.

Collapse
 
muhammedmoussa profile image
Moussa

Cool, thank you!

Collapse
 
ruslangonzalez profile image
Ruslan Gonzalez

Great article. Thanks!!

Collapse
 
spock123 profile image
Lars Rye Jeppesen

Thank you

Collapse
 
ciglesiasweb profile image
Carlos Iglesias

Uaoooo!