Writing Unit Tests page

Write a unit test for a service in Angular

Overview

In this part, we will:

  • Write a new getRestaurant method on our RestaurantsService
  • Write a unit test for the getRestaurant method

Problem

In the next section we're going to be creating a restaurant detail view. We'll need to have a method on our service that returns one restaurant from the list. Write this method and call it getRestaurant. It should take a string param "slug" and make a get request to the path '/restaurants/slug-here'. Then write a unit test for this method ensuring it makes the correct request and returns an object type of Restaurant.

What You Need to Know

  • How to write a method on a service that takes a param. You've learned this in previous sections.

  • How to write a unit test. Here's a codeblock to get you started:

    it('should make a get request to get a restaurant based on its slug', () => {
      
    });
    
  • Refer to the existing tests on the service to see how to use HttpMock to test HttpClient calls.

Solution

Click to see the solution ✏️ Update src/app/restaurant/restaurant.service.ts

import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
import { Restaurant } from './restaurant';

export interface ResponseData<DataType> {
  data: DataType[];
}

export interface State {
  name: string;
  short: string;
}

export interface City {
  name: string;
  state: string;
}

@Injectable({
  providedIn: 'root',
})
export class RestaurantService {
  constructor(private httpClient: HttpClient) {}

  getRestaurants(
    state: string,
    city: string
  ): Observable<ResponseData<Restaurant>> {
    const params = new HttpParams()
      .set('filter[address.state]', state)
      .set('filter[address.city]', city);
    return this.httpClient.get<ResponseData<Restaurant>>(
      environment.apiUrl + '/restaurants',
      { params }
    );
  }

  getStates(): Observable<ResponseData<State>> {
    return this.httpClient.get<ResponseData<State>>(
      environment.apiUrl + '/states'
    );
  }

  getCities(state: string): Observable<ResponseData<City>> {
    const params = new HttpParams().set('state', state);
    return this.httpClient.get<ResponseData<City>>(
      environment.apiUrl + '/cities',
      { params }
    );
  }

  getRestaurant(slug: string) {
    return this.httpClient.get<Restaurant>(
      environment.apiUrl + '/restaurants/' + slug
    );
  }
}

✏️ Update src/app/restaurant/restaurant.service.spec.ts

import {
  HttpClientTestingModule,
  HttpTestingController,
} from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { Restaurant } from './restaurant';
import {
  City,
  ResponseData,
  RestaurantService,
  State,
} from './restaurant.service';

describe('RestaurantService', () => {
  let httpMock: HttpTestingController;
  let service: RestaurantService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
    });

    httpMock = TestBed.inject(HttpTestingController);
    service = TestBed.inject(RestaurantService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should make a get request to restaurants', () => {
    const mockRestaurants = {
      data: [
        {
          name: 'Brunch Place',
          slug: 'brunch-place',
          images: {
            thumbnail:
              'node_modules/place-my-order-assets/images/4-thumbnail.jpg',
            owner: 'node_modules/place-my-order-assets/images/2-owner.jpg',
            banner: 'node_modules/place-my-order-assets/images/2-banner.jpg',
          },
          menu: {
            lunch: [
              { name: 'Ricotta Gnocchi', price: 15.99 },
              { name: 'Garlic Fries', price: 15.99 },
              { name: 'Charred Octopus', price: 25.99 },
            ],
            dinner: [
              { name: 'Steamed Mussels', price: 21.99 },
              { name: 'Roasted Salmon', price: 23.99 },
              { name: 'Crab Pancakes with Sorrel Syrup', price: 35.99 },
            ],
          },
          address: {
            street: '2451 W Washburne Ave',
            city: 'Ann Arbor',
            state: 'MI',
            zip: '53295',
          },
          _id: 'xugqxQIX5rPJTLBv',
        },
        {
          name: 'Taco Joint',
          slug: 'taco-joint',
          images: {
            thumbnail:
              'node_modules/place-my-order-assets/images/4-thumbnail.jpg',
            owner: 'node_modules/place-my-order-assets/images/2-owner.jpg',
            banner: 'node_modules/place-my-order-assets/images/2-banner.jpg',
          },
          menu: {
            lunch: [
              { name: 'Beef Tacos', price: 15.99 },
              { name: 'Chicken Tacos', price: 15.99 },
              { name: 'Guacamole', price: 25.99 },
            ],
            dinner: [
              { name: 'Shrimp Tacos', price: 21.99 },
              { name: 'Chicken Enchilada', price: 23.99 },
              { name: 'Elotes', price: 35.99 },
            ],
          },
          address: {
            street: '13 N 21st St',
            city: 'Chicago',
            state: 'IL',
            zip: '53295',
          },
          _id: 'xugqxQIX5dfgdgTLBv',
        },
      ],
    };

    service
      .getRestaurants('IL', 'Chicago')
      .subscribe((restaurants: ResponseData<Restaurant>) => {
        expect(restaurants).toEqual(mockRestaurants);
      });

    const url =
      'http://localhost:7070/restaurants?filter%5Baddress.state%5D=IL&filter%5Baddress.city%5D=Chicago';
    // url parses to 'http://localhost:7070/restaurants?filter[address.state]=IL&filter[address.city]=Chicago'

    const req = httpMock.expectOne(url);

    expect(req.request.method).toEqual('GET');
    req.flush(mockRestaurants);

    httpMock.verify();
  });

  it('can set proper properties on restaurant type', () => {
    const restaurant: Restaurant = {
      name: 'Taco Joint',
      slug: 'taco-joint',
      images: {
        thumbnail: 'node_modules/place-my-order-assets/images/4-thumbnail.jpg',
        owner: 'node_modules/place-my-order-assets/images/2-owner.jpg',
        banner: 'node_modules/place-my-order-assets/images/2-banner.jpg',
      },
      menu: {
        lunch: [
          { name: 'Beef Tacos', price: 15.99 },
          { name: 'Chicken Tacos', price: 15.99 },
          { name: 'Guacamole', price: 25.99 },
        ],
        dinner: [
          { name: 'Shrimp Tacos', price: 21.99 },
          { name: 'Chicken Enchilada', price: 23.99 },
          { name: 'Elotes', price: 35.99 },
        ],
      },
      address: {
        street: '13 N 21st St',
        city: 'Chicago',
        state: 'IL',
        zip: '53295',
      },
      _id: 'xugqxQIX5dfgdgTLBv',
    };
    // will error if interface isn't implemented correctly
    expect(true).toBe(true);
  });

  it('should make a get request to states', () => {
    const mockStates = {
      data: [{ name: 'Missouri', short: 'MO' }],
    };

    service.getStates().subscribe((states: ResponseData<State>) => {
      expect(states).toEqual(mockStates);
    });

    const url = 'http://localhost:7070/states';
    const req = httpMock.expectOne(url);

    expect(req.request.method).toEqual('GET');
    req.flush(mockStates);

    httpMock.verify();
  });

  it('should make a get request to cities', () => {
    const mockCities = {
      data: [{ name: 'Kansas City', state: 'MO' }],
    };

    service.getCities('MO').subscribe((cities: ResponseData<City>) => {
      expect(cities).toEqual(mockCities);
    });

    const url = 'http://localhost:7070/cities?state=MO';
    const req = httpMock.expectOne(url);
    expect(req.request.method).toEqual('GET');
    req.flush(mockCities);

    httpMock.verify();
  });

  it('should make a get request to get a restaurant based on its slug', () => {
    const mockRestaurant = {
      name: 'Brunch Place',
      slug: 'brunch-place',
      images: {
        thumbnail: 'node_modules/place-my-order-assets/images/4-thumbnail.jpg',
        owner: 'node_modules/place-my-order-assets/images/2-owner.jpg',
        banner: 'node_modules/place-my-order-assets/images/2-banner.jpg',
      },
      menu: {
        lunch: [
          { name: 'Ricotta Gnocchi', price: 15.99 },
          { name: 'Garlic Fries', price: 15.99 },
          { name: 'Charred Octopus', price: 25.99 },
        ],
        dinner: [
          { name: 'Steamed Mussels', price: 21.99 },
          { name: 'Roasted Salmon', price: 23.99 },
          { name: 'Crab Pancakes with Sorrel Syrup', price: 35.99 },
        ],
      },
      address: {
        street: '2451 W Washburne Ave',
        city: 'Ann Arbor',
        state: 'MI',
        zip: '53295',
      },
      _id: 'xugqxQIX5rPJTLBv',
    };

    service
      .getRestaurant('brunch-place')
      .subscribe((restaurant: Restaurant) => {
        expect(restaurant).toEqual(mockRestaurant);
      });

    const url = 'http://localhost:7070/restaurants/brunch-place';
    const req = httpMock.expectOne(url);

    expect(req.request.method).toEqual('GET');
    req.flush(mockRestaurant);

    httpMock.verify();
  });
});