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

How to configure dynamic columns in material-table using React and TypeScript

My task involves handling a table with a variable number of columns that are generated dynamically based on the data received. To manage these columns, I have designed an interface that defines various properties for each column, such as whether it can be ...

What is the best way to implement promise function in a JavaScript functional method such as forEach or reduce?

I have implemented a promise function in the following way: // WORK let res = {approveList: [], rejectList: [], errorId: rv.errorId, errorDesc: rv.errorDesc}; for (let i = 0; i < rv.copyDetailList.length; i ++) { const item = rv.copyDetailList[i]; ...

Tips for handling Firebase JS SDK errors within try-catch blocks

Attempting to type the err object in a function that saves documents to Firestore has been challenging. async function saveToFirestore(obj: SOME_OBJECT, collection: string, docId: string) { try { await firebase.firestore().collection(collection).doc( ...

Encountering an error with the Angular 2 SimpleChanges Object during the initial npm start process

Within my Angular 2 application, there exists a component that holds an array of objects and passes the selected object to its immediate child component for displaying more detailed data. Utilizing the "SimpleChanges" functionality in the child component a ...

Angular2, multi-functional overlay element that can be integrated with all components throughout the application

These are the two components I have: overlay @Component({ selector: 'overlay', template: '<div class="check"><ng-content></ng-content></div>' }) export class Overlay { save(params) { //bunch ...

Using import statement is mandatory when loading ES Module in TS Node: server/src/index.ts

Attempting to kickstart a TypeScript Node project, I've gone ahead and added some essential dependencies (TypeScript, ESLint, Mongoose, and GraphQL). However, upon executing the command: ts-node-dev --respawn --transpile-only src/index.ts An error me ...

Exploring Angular: Embracing the Power of Query String Parameters

I've been struggling with subscribing to query string parameters in Angular 2+. Despite looking at various examples, I can't seem to make it work. For instance, on this Stack Overflow thread, the question is about obtaining query parameters from ...

Harness the power of the Node.js Path module in conjunction with Angular 6

I'm currently facing an issue with utilizing the Path module in my Angular 6 project. After some research, I came across a helpful post detailing a potential solution: https://gist.github.com/niespodd/1fa82da6f8c901d1c33d2fcbb762947d The remedy inv ...

testing exceptions with Jest: a step-by-step guide

As a beginner with Jest, I am currently working on a program to delve deeper into JavaScript. While my tests are functioning properly, I'm wondering if it would be better to replace the try/catch blocks with exceptions. I feel like there might be a mo ...

deleting the existing marker before placing a new marker on the Mapbox

Upon the map loading with GeoJson data, I have implemented code to display markers at specified locations. It works flawlessly, but I am seeking a way to remove previous markers when new ones are added. What adjustments should be made for this desired func ...

The Angular Material side navigation module is not being acknowledged

Currently, I am utilizing Angular version 9.1.11 in conjunction with Angular Material version 9.2.4. The issue arises when attempting to import the MaterialSidenavModule, which is required for utilizing components like mat-sidenav-container. Below is a sn ...

Ionic 4 and RxJS 6: Issue with Ionic-native HTTP get method not fetching data as expected

Currently, I am in the process of transitioning from using the angular HttpClient Module to the ionic-native HTTP Module. This switch is necessary because I have encountered difficulties accessing a third-party API using the standard HttpClient Module. To ...

Template URI parameters are being used in a router call

Utilizing the useRouter hook in my current project. Incorporating templated pages throughout the application. Implementing a useEffect hook that responds to changes in the router and makes an API call. Attempting to forward the entire URL to /api/${path ...

Can dynamic forms in Angular 2 support nested forms without relying on formBuilder?

I'm familiar with implementing nested forms in reactive form, but I'm unsure about how to do it in dynamic form within Angular 2. Can nested forms be implemented in dynamic forms in Angular 2? ...

Tips for retrieving the most recent UI updates after the container has been modified without the need to refresh the browser

Currently, I have developed a micro frontend application in Angular using module federation. This application is hosted in production with Docker containers. My main concern revolves around how to update the UI changes for the user without them needing to ...

Creating interactive features for a TypeScript interface

I was looking to create a dynamic interface with custom properties, like so: data: dataInterface []; this.data = [ { label: { text: 'something', additionalInfo: 'something' } }, { bar: { text: ' ...

What is the best way to retrieve Express app configuration asynchronously?

I am utilizing an Express app developed with the Serverless Framework, which will be hosted via AWS API Gateway and AWS Lambda. The authentication process is handled by Okta, and I am considering storing the necessary secrets in SSM. Currently, I have to f ...

The Console.Log function will not run if it is placed within the RXJS Tap operator

In my current setup, I have the following observables: this.authenticationService.isSignedIn() -> Observable<Boolean> this.user$ -> Observable<UserModel> I am in need of checking a condition based on both these observables, so I attempt ...

When you use Array.push, it creates a copy that duplicates all nested elements,

Situation Currently, I am developing a web application using Typescript/Angular2 RC1. In my project, I have two classes - Class1 and Class2. Class1 is an Angular2 service with a variable myVar = [obj1, obj2, obj3]. On the other hand, Class2 is an Angular2 ...

The React Fabric TextField feature switches to a read-only mode once the value property is included

I've been grappling with how to manage value changes in React Fabric TextFields. Each time I set the value property, the component goes into read-only mode. When utilizing the defaultValue property, everything functions correctly, but I require this i ...