Tips for testing an Angular 6 service with a dependency that utilizes private methods and properties to alter the output of public methods and properties

I've encountered a challenge while attempting to write a Jasmine/Karma test for an Angular 6 app. The test is for a service in my application that relies on another service with private properties and methods, causing my tests to consistently fail.

While the code functions correctly during runtime, I'm struggling to find the right approach for testing it. I've experimented with spies for the private methods and properties, as well as changing them to public, but so far, nothing has worked. It's evident that there's something crucial that I'm missing.

At the moment, my primary focus is not on testing DependencyService. Instead, I aim to successfully test the function doSomething within MyService. Currently, this function always returns null, leading to a test output of

Failed: Cannot read property 'subscribe' of null
. Interestingly, removing the if statement in postData() resolves the issue, and the test passes as expected.

It seems like I might be overlooking something significant in terms of spying, as my test appears to be overly dependent on the dependency service or values from localStorage.

I need guidance on how to effectively mock/spy on the checkAuth and isAuth methods within my service's dependency. More specifically, how do I properly test doSomething() to ensure that the test remains isolated to the MyService service?

export class MyService {
    constructor(private depService: DependencyService) { }

    public doSomething(additionalPayload: Object) {
        const payload = { ...additionalPayload, modified: true };
        return this.depService.postData('/api/endpoint', payload);
    }

}

export class DependencyService {
    constructor(private httpClient: HttpClient) { }

    private isAuth: boolean = false;

    private checkAuth() {
        const token = localStorage.get('token');
        if (token !== null) {
            this.isAuth = true;
        } else {
            this.isAuth = false;
        }
    }

    postData(url, body): Observable<any> {
        this.checkAuth();
        if (!this.isAuth) {
            return null;
        }
        return this.httpClient.post(url, body);
    }
}

The myservice.spec.ts currently failing:

describe('MyService', () => {
  let httpTestingController: HttpTestingController;

  let myService: MyService;
  let dependencyServiceSpy: jasmine.SpyObj<DependencyService>;

  beforeEach(() => {
    const dependencyServiceSpyObj = jasmine.createSpyObj('DependencyService', ['postData']);

    TestBed.configureTestingModule({
      imports: [ HttpClientTestingModule ],
      providers: [
        MyService,
        { provide: DependencyService, useValue: dependencyServiceSpyObj },
      ]
    });

    httpTestingController = TestBed.get(HttpTestingController);

    myService = TestBed.get(MyService);
    dependencyServiceSpy = TestBed.get(DependencyService);
  });

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

  it('#doSomething should post some data', async(() => {
    const payloadData: Object = {
      name: 'Ash',
      food: 'donut'
    };

    const responseData: Object = {
      success: true,
      msg: 'Payload received'
    };

    // HELP HERE ↓
    // need to spy/mock dependencyService.isAuth so that it is `true`
    // otherwise inside `postData` the if statement will always return a `null` value
    // ...spy/mock `localStorage`?
    dependencyServiceSpy.postData.and.returnValue(/* return http observable so that .subscribe can be called */);

    myService.doSomething(payloadData).subscribe(data => {
      expect(data).toEqual(responseData);
    }, fail);

    const req = httpTestingController.expectOne('/api/endpoint');

    expect(req.request.method).toEqual('POST');
    expect(req.request.body).toEqual({ ...payloadData, modified: true });

    expect(dependencyServiceSpy.postData.calls.count()).toBe(1);
    expect(dependencyServiceSpy.postData.calls.mostRecent().returnValue).toBe(responseData);

    req.flush(responseData);
  }));
});

Answer №1

Don't stress about the dependency service. It is important to focus on whether the user is authenticated and if network calls are functioning correctly as part of the dependency service specifications.

There seems to be an issue with how you are spying on dependencyService. When using TestBed.get(DependencyService), it returns the current instance of DependencyService and not the spy. It would be best to rename the variable like this:

let dependencyService: DependencyService;

and assign it like this:

dependencyService = TestBed.get(DependencyService);

The only method that needs to be spied on is postData.

From the standpoint of MyService, there are two scenarios for DependencyService:

  1. User is not authenticated

    In this situation, all that is required is for postData to return null. There is no need to concern yourself with checkAuth. Simply spy on postData and return an Observable with a null value. The focus should be on the output of the postData method, not its generation.

    it('#doSomething should return null if user is not authenticated', () => {
        const payloadData = {
            name: 'Ash',
            food: 'donut'
        };
    
        spyOn(dependencyService, 'postData').and.returnValue(Observable.create(observer => {
            observer.next(null);
            observer.complete();
        }));
    
        myService.doSomething('/api/endpoint', payloadData).subscribe(data => {
            expect(data).toBeNull();
        }, fail);
    
    });
    

    As demonstrated above, the process by which postData results in null does not need to be specified. In this scenario, it is expected that postData will return null. How this is accomplished should be tested in the spec for DependencyService.

  2. User is authenticated

    In this case, postData returns the value from an HTTP call. Once again, the focus should be on returning the value. Whether the network call is made correctly or not should be tested within the DependencyService spec.

    it('#doSomething should post some data', () => {
        const payloadData = {
            name: 'Ash',
            food: 'donut'
        };
    
        const responseData = {
            success: true,
            msg: 'Payload received'
        };
    
        spyOn(dependencyService, 'postData').and.returnValue(Observable.create(observer => {
            observer.next(responseData);
            observer.complete();
        }));
    
        myService.doSomething('/api/endpoint', payloadData).subscribe(data => {
            expect(data).toEqual(responseData);
        }, fail);
    
    });
    

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

Steps to dynamically populate dropdown menus based on the selected options from other dropdown menus

I am working on a project that involves two dropdown menus. The options for the first dropdown menu are stored in a file called constant.ts. Depending on the selection made in the first dropdown, the options for the second dropdown will change accordingly. ...

The fuse-sidebar elements are not being properly highlighted by Introjs

I have recently developed an angular project that utilizes the fuse-sidebar component. Additionally, I am incorporating introjs into the project. While introjs is functioning properly, it does not highlight elements contained within the fuse-sidebar. The ...

Having trouble with the Angular router link suddenly "failing"?

app.routes.ts: import { environment } from './environment'; import { RouterModule } from "@angular/router"; import { ContactUsComponent } from './static/components/contact-us.component'; import { HomeComponent } ...

The entire React component is not rendering as expected when calling res.render in PugJS within Express

Seeking guidance for the following issue: I have developed a PugJS view that is rendered within an ExpressJS route. In the call to the ExpressJS function res.render, the React component is included as data inside the .render() function call.... The prob ...

Combine two elements in an array

I am faced with a challenge in binding values from an Array. My goal is to display two values in a row, then the next two values in the following row, and so on. Unfortunately, I have been unable to achieve this using *ngFor. Any assistance would be greatl ...

Combining Angular subscriptions to fetch multiple data streams

I am looking to retrieve the most recent subscription from a group of subscriptions. Whenever the value of my FormControl changes, I want to capture only the latest value after the user has finished typing. Below is the code snippet I am using - let cont ...

Having trouble utilizing the ng-Command in Angular?

Currently, I am attempting to set up Angular in a vagrant-box environment. npm install -g @angular/cli Unfortunately, I encounter an error while trying to use the client: The program 'ng' is currently not installed. You can install it by typin ...

Creating an array object in TypeScript is a straightforward process

Working on an Angular 4 project, I am attempting to declare an attribute in a component class that is an object containing multiple arrays, structured like this: history: { Movies: Array<Media>, Images: Array<Media>, Music: Array<Medi ...

retrieve Angular data across components using Input

When using fetch to make a request to the reqres api users in app.component, I then share the data with its child component (hello.component) via Input. While I am able to get the correct user names in the child template, I encounter an issue when trying t ...

Enhance your TypeScript code using decorators with inheritance

Exploring the realm of Typescript decorators has led me to discover their intriguing behavior when combined with class inheritance. Consider the following scenario: class A { @f() propA; } class B extends A { @f() propB; } class C exten ...

Method for Injecting Global Constants Provider in Angular 2

I am trying to implement a global constants setup in my Angular app, specifically with a root directory that should be accessible to every component without the need for manual imports. I came across a solution on Stack Overflow suggesting the use of a con ...

Loading the value of a Subject variable in an Angular 2 application using Typescript

I am currently developing an Angular2 application where I am loading data from a service into my component as a subject. public type1Choisi: any; constructor( public formeService: FormeService, ...) { this.formeService._type1.subscribe(type1 => ...

Discover the ultimate strategy to achieve optimal performance with the wheel

How can I dynamically obtain the changing top position when a user moves their mouse over an element? I want to perform some checks whenever the user scrolls up, so I tried this code: HostListener('window:wheel', ['$event']) onWindowS ...

Determining the generic type argument of a class can be unsuccessful due to the specific properties within that class

Why is it that Typescript sometimes fails to infer types in seemingly simple cases? I am trying to understand the behavior behind this. When Typescript's Type Inference Goes Wrong Consider the scenario where we have the following class declarations: ...

Tips for adjusting the size and positioning the ng bootstrap carousel in the center

My Angular project is using ng bootstrap, but I'm facing a styling issue. The content doesn't display in the center, rather it appears in the upper left corner: example I would like the content to be centered and wide under the blue headbar. W ...

Is it possible to verify type property names using a union type?

Given type UnionType = 'prop1' | 'prop2' | 'prop3'; type DerivedType = { prop1: string; prop2: number; prop3: boolean; }; Is there a method to define DerivedType in such a way that if I introduce a new member to UnionT ...

Getting started with testing in Angular 2

When I first started coding my app, unit tests were not on my mind. Now, I realize the importance and need to write them. However, while attempting to write tests using Jasmine only, I encountered an error when trying to import components - "system is no ...

Delete an essential attribute from an entity

I am trying to remove a required property called hash from an object, but I keep encountering TypeScript or ESLint errors. All the properties of the interface are mandatory, and I do not want to make all properties optional using Partial. Here is the inte ...

Managing server errors when utilizing Observables

i am currently developing an Angular 2 application and part of it includes a login feature that utilizes this service. import { Http, Response } from '@angular/http'; import {Injectable} from '@angular/core'; import 'rxjs/add/op ...

Encountered an issue launching the advanced web server and reverse proxy server nginx for high performance

UPDATE - Recently, I encountered the following error logs: nginx: [emerg] unknown "request_url" variable Aug 19 01:14:58 nginx[4890]: nginx: configuration file /etc/nginx/nginx.conf test failed Below is my nginx.conf file: user www-data; worker ...