Exploring the capabilities of Angular 8 Services using 'TestHttpInterceptor'!

My Angular front-end is relatively small, but it relies heavily on data from an external server.

I'm encountering issues with my tests throwing a gyp error.

While I can mock the service to test a simple component, this approach only allows me to replace the output and doesn't constitute a real test. To conduct a proper test, I believe I need to establish a predefined return when the service calls the external API.

Here's a basic example:

The definition of the object/interface:

// alerts.ts
export interface Alert {
  id: number;
  display_message: string;
  is_enabled: boolean;
}

The service definition:

// alerts.service.ts
import { of as observableOf, Observable } from 'rxjs';
import { catchError } from 'rxjs/operators;
import { Alert } from './alerts';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';

export interface IAlertService {
  getAlerts(): Observable<Alert[] | null>;
}

@Injectable()
export class AlertService implements IAlertService {

  readonly baseUrl = '/api/alerts/';
  alerts$: Observable<Alert[] | null>;

  constructor(private _http: HttpClient) { }

  getAlerts(): Observable<Alert[] | null> {
    return this._http.get<Alert[] | null>(this.baseUrl).pipe(
      catchError(error => {
        console.log('in catch: ', error);
        return observableOf(null);
      }));
  }
}

The component code:

// alerts/alerts.component.html
<div *ngIf="alerts" >
  <div class="service-notice" *ngFor="let alert of alerts">
    <p [innerHTML]="alert.display_message"></p>
  </div>
</div>

The component code then followed up by:

// alerts/alerts.component.ts
import { Component, OnInit } from '@angular/core';

import { Alert } from '../alerts';
import { AlertService } from '../alerts.service';

@Component({
  selector: 'naas-alerts',
  templateUrl: './alerts.component.html',
  styleUrls: ['./alerts.component.scss'],
})
export class AlertComponent implements OnInit {

  alerts: Alert[] | null;

  constructor(private alertService: AlertService) { }

  ngOnInit() {
    this.alertService.getAlerts().subscribe(data => {
      this.alerts = data;
    });
  }
}

To tackle this problem, I created an httpInterceptor class that enables me to define the desired string return value within the test:

// testing_interceptors.ts
import {
    HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse, HTTP_INTERCEPTORS
  } from '@angular/common/http';
import { Injectable, Injector } from '@angular/core';
import { of as observableOf, Observable } from 'rxjs';

@Injectable()
export class TestHttpInterceptor implements HttpInterceptor {

  current_containers: string = '[]';
  current_user: string = '';
  alerts: string = '[]';

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.log('TestHttpInterceptor called');
    const url_regex = /(\/api(?:\/[\w+\/]+)+)$/;
    const url_path = url_regex.exec(request.url)[1];

    if(request.method === "GET") {
        if(url_path == '/api/alerts/') {
            return observableOf(new HttpResponse({
                status: 200,
                body: this.alerts
            }));
        }
        if(url_path == '/api/users/current/') {
            return observableOf(new HttpResponse({
                status: 200,
                body: this.current_user
            }));
        }
        if (url_path === '/api/users/current/containers/') {
            return observableOf(new HttpResponse({
                status: 200,
                body: this.current_containers
            }));
        }
    }
  }
}

...Followed by the corresponding test:

// alerts/alerts.component.spec.test
import { HttpClientModule, HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { TestBed, ComponentFixture } from '@angular/core/testing';

import { AlertComponent } from './alerts.component';
import { AlertService } from '../alerts.service';
import { AlertServiceMock } from '../alerts.service.mock';

import { TestHttpInterceptor } from '../testing_interceptors';

describe('AlertsComponent', () => {
  let fixture: ComponentFixture<AlertComponent>;
  let component: AlertComponent;
 
  let testHttpInterceptor: TestHttpInterceptor;

  beforeEach(() => {
    
    testHttpInterceptor = new TestHttpInterceptor();

    TestBed.configureTestingModule({
      declarations: [ AlertComponent ],
      providers: [
        {provide: AlertService, useClass: AlertService },
        {provide: HttpClient, useClass: HttpClientModule},
        {provide: HTTP_INTERCEPTORS, useClass: TestHttpInterceptor, multi: true }
      ],
    }).compileComponents();
    fixture = TestBed.createComponent(AlertComponent);
    component = fixture.componentInstance;
  });

  it('should be created', done => {
    fixture.detectChanges();
    expect(component).toBeTruthy();
    done();
  });

});
  

I've been trying various solutions found online without much success. Any advice would be greatly appreciated!

  1. Is this testing approach valid? (I've looked into marbles but struggled to understand it)
  2. How can I fix the current issue?

Answer №1

My successful approach...

I had a specific goal of testing the generated html code by a component based on the response received from an external API call. In other words, my focus was to thoroughly test the code within alerts.component.ts.

// alerts.component.spce.ts
import { By } from '@angular/platform-browser';
import {
  HttpClientTestingModule,
  HttpTestingController,
} from '@angular/common/http/testing';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { Type } from '@angular/core';

import { AlertComponent } from './alerts.component';
import { AlertService } from '../alerts.service';

/*

This is a series of tests for alerts.component.ts.

AlertComponent contains an ngOnInit method that utilizes AlertService.getAlerts

When AlertService.getAlerts hits `/api/alerts/`, the HttpTestingController intercepts it and returns our predefined response.

The processed response is then passed back to AlertComponent by AlertService.getAlerts, resulting in the generation of the HTML fragment defined in alerts.components.html, which we can validate.

*/

describe('AlertsComponent', () => {
  let fixture: ComponentFixture<AlertComponent>;
  let httpMock: HttpTestingController;
  let alertComponent: AlertComponent;

  beforeEach( async () => {

    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      declarations: [ AlertComponent ],
      providers: [ AlertService ],
    });

    await TestBed.compileComponents();

    fixture = TestBed.createComponent(AlertComponent);
    alertComponent = fixture.componentInstance;
    httpMock = fixture.debugElement.injector
      .get<HttpTestingController>(HttpTestingController as Type<HttpTestingController>);

    fixture.detectChanges();

  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should be created', () => {
    alertComponent.ngOnInit();
    const reqs = httpMock.match(`/api/alerts/`);
    for (const req of reqs) {
      req.flush(null);
    }
    fixture.detectChanges();
    expect(alertComponent).toBeTruthy();
  });

  it('The component should initialize, make the alert service call, and receive a response', () => {
    const dummyAlerts = [{
      id: 1,
      display_message: 'Foo',
      is_enabled: true,
    }];

    alertComponent.ngOnInit();
    const reqs = httpMock.match(`/api/alerts/`);
    for (const req of reqs) {
      req.flush(dummyAlerts);
    }
    fixture.detectChanges();
    const compiled = fixture.debugElement.queryAll(By.css('p'));
    expect(compiled.length).toBe(1);
  });

  it('The component should create 2 alerts based on the response', () => {
    const dummyAlerts = [{
      id: 1,
      display_message: 'Foo',
      is_enabled: true,
    }, {
      id: 2,
      display_message: 'Bar',
      is_enabled: true,
    }];

    alertComponent.ngOnInit();
    const reqs = httpMock.match(`/api/alerts/`);
    for (const req of reqs) {
      req.flush(dummyAlerts);
    }
    fixture.detectChanges();
    const compiled = fixture.debugElement.queryAll(By.css('p'));
    expect(compiled.length).toBe(2);
  });

  // ## This fails when looking for 'small' - needs investigated ##
  it('The component should create 2 alerts from a response', () => {
    const dummyAlerts = [{
      id: 1,
      display_message: '<small>Foo</small>',
      is_enabled: true,
    }];

    alertComponent.ngOnInit();
    const reqs = httpMock.match(`/api/alerts/`);
    for (const req of reqs) {
      req.flush(dummyAlerts);
    }
    fixture.detectChanges();
    const compiled = fixture.debugElement.queryAll(By.css('p'));
    expect(compiled.length).toBe(1);
  });
});

(I welcome suggestions on alternative methods for this testing approach)

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Guide to displaying the value of a field in an object upon clicking the inline edit button using TypeScript

Is it possible to console log a specific attribute of an object when clicking on the edit button using the code below? Please provide guidance on how to utilize the index to access the value of "name". Refer to the last line in the code with the comment. ...

Retrieve all values of a specific enum type in TypeScript

When working with Typescript, I am looking to retrieve all values of an enum type and store them in an array. In C#, a similar method would look like this: public static TEnum[] GetValues<TEnum>() where TEnum : Enum { return Enum.GetValues(typeof ...

Using curly braces as function parameters in Typescript

While exploring the content metadata component of Alfresco ADF, I came across this typescript function that has left me puzzled: private saveNode({ changed: nodeBody }): Observable<Node> { return this.nodesApiService.updateNode(this.node.id, nodeB ...

What is the best way to include documentation for custom components using jsDoc?

Within my Vuejs inline template components, we typically register the component in a javascript file and define its template in html. An example of such a component is shown below: Vue.component('compare-benefits', { data() { // By return ...

Docker brings up the login page, yet the node server.js for the MEAN stack isn't launching

Here's the latest configuration updates after adding a bounty: Configuration UPDATE: It seems that there is a CORS error occurring when trying to login, which is likely causing the problem. I've come across other posts, like this one, suggesting ...

Is there a simple method to eliminate devDependencies from the ultimate package using esbuild?

My current challenge involves using esbuild to package my lambda functions. However, during the build generation for deployment, I encounter an alert indicating that the package size exceeds the limit, as shown in the image below. File too large In explo ...

ion-list with borders of different colors for each ion-avatar

I have a list of ion items, each displaying a round ion-avatar image with a colored border. Currently, I can only set one fixed color for all items. However, I would like each item to have a different color based on the value of listItem.color. Here is th ...

Ensure Angular delays execution until my function provides a response

For my latest project, I'm working on a custom pipe that utilizes the Google Translation Service API. However, I've run into an issue where the returned value is always the original input before translation because the function finishes execution ...

Creating a unified observable by merging various iterations in RXJS

For a while now, I've been grappling with the challenge of combining multiple asynchronous calls. Every time I think I'm close to figuring it out, I hit a roadblock at a foreach loop that I just can't seem to crack. Currently, my approach i ...

Function in Angular that provides the elementId when hovering over it

Currently, I am working on an angular project that involves creating a wiki window. Basically, when any element is hovered over with the mouse, its definition will appear inside the wiki window. I am wondering if it would be possible to create a global fun ...

Security concern regarding XSRF in Spring and Angular 5

For my current project, I am using Spring as the backend (generated with microservices with Jhipster) and Angular5 as the frontend. On the server side, CSRF security is added (it was enabled by default when we created microservices with Jhipster). Workin ...

Troubleshooting Angular2 ngFor: Finding a Fix

I'm having trouble setting up a basic shopping list using ngFor in Angular. import { Component, View } from 'angular2/angular2'; @Component({ selector: 'my-app' }) @View({ template: '<h1>{{title}}</h1>< ...

Validation of email forms in Angular 5

I have encountered a challenge that I need help with: Using Angular 5 - template driven form In my template, there is an input field with the type email. Here's an example: <input type="email" [(ngModel)]="model.email" #email="ngModel" email /> ...

Expanding a component using the identical selector in Angular 2 / Angular-CLI

We are leveraging Angular to create a sleek component-based frontend for our primary application. Our various clients often request minor customizations to these core components. To maintain the integrity of our core code, we plan to store it in NPM packag ...

Encountered an error while attempting to use the 'setAttribute' method on the 'Element' object: ']' is not a recognized attribute name. This issue arose within an Angular 4 project

I encountered the following issue: Failed to execute 'setAttribute' on 'Element': ']' is not a valid attribute name. I defined a model as follows: export interface ModalComponentModel { username: string; password: s ...

The condition in a Typescript function that checks for strings will never evaluate to true

I encountered a strange issue with a TypeScript condition in a function. Here is my current code, where the parameters are passed from outside: getLevel(validation: string, status: string): string { let card = ""; if (validation == &qu ...

Creating dynamic CSS in Angular 2 and beyond

When working with a JavaScript variable containing style information, I encountered different approaches in AngularJS and Angular2+: menuBgColor = "red"; In AngularJS, I could use dynamically embedded styles like this: <div> <style> ...

Error: TypeScript is unable to find the property URL within the specified type. Can you help

Currently, I am honing my skills in TypeScript and encountering a problem. I am a bit confused about how to define the type for the URL when I am setting the mix here. Obviously, the URL is supposed to be a string, and this specific scenario is within the ...

Error Message: ElectronJS - Attempted to access the 'join' property of an undefined variable

I am currently working on developing a tray-based application. However, I am encountering an error that reads: Uncaught TypeError: Cannot read property 'join' of undefined Can anyone guide me on how to resolve this issue? This is the content ...

What is the method for generating an observable that includes a time delay?

Question In order to conduct testing, I am developing Observable objects that simulate the observable typically returned by an actual http call using Http. This is how my observable is set up: dummyObservable = Observable.create(obs => { obs.next([ ...