Trying out mocked service with promises and error handling in Angular using Karma

Having trouble with coverage in my Angular/Karma tests.

Recently, I developed a component containing a signUp() function

angularFireAuthSignOutSpyObj acts as a spy for this.auth within the component (Firebase Auth)

  signUp() {
    if (this.registrationForm.valid) {
      this.auth.createUserWithEmailAndPassword
      (
        this.registrationForm.get('email')?.value,
        this.registrationForm.get('password')?.value
      )
        .then(() => {
          this.appMessage = "Account created !";
        })
        .catch((error) => {
          this.appMessage = error.message;
        });
    } else {
      this.appMessage = 'Submit logic bypassed, form invalid !'
    }
  }

I'm currently conducting Karma tests on this component function as is

  it('should submit registration with form values', () => {
    spyOn(component, 'signUp').and.callThrough();
    angularFireAuthSignOutSpyObj.createUserWithEmailAndPassword.and.returnValue({
      then: function () {
        return {
          catch: function () {
          }
        };
      }
    });
    component.registrationForm.controls.email.setValue('<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="5e2a3b2d2a1e3b333f3732703d3133">[email protected]</a>');
    component.registrationForm.controls.password.setValue('ValidPass123');
    component.registrationForm.controls.passwordCheck.setValue('ValidPass123');
    expect(component.registrationForm.valid).toBeTruthy();
    debugElement.query(By.css("button")).triggerEventHandler("click", null);
    expect(component.signUp).toHaveBeenCalled();
    expect(component.auth.createUserWithEmailAndPassword)
      .toHaveBeenCalledWith(
        component.registrationForm.controls.email.value,
        component.registrationForm.controls.password.value)
    // expect(component.appMessage).toEqual('Account created !');
  });

The last expect section has been commented out due to an Error: Expected undefined to equal 'Account created !'. The issue arises from the lack of control over the defined then and catch functions in the mocked service angularFireAuthSignOutSpyObj.

In essence, the functions are defined to prevent errors when accessed in the signUp() function. However, I am seeking a way to trigger the then(() => ...) and catch(() => ...) events to properly test if the app message updates correctly.

All testing components work fine until this point. It seems modifying createUserWithEmailAndPassword.and.returnValue might hold the key to triggering the expected functions.

    angularFireAuthSignOutSpyObj.createUserWithEmailAndPassword.and.returnValue({
      then: function () {
        return {
          catch: function () {
          }
        };
      }
    });

If anyone has suggestions on how to properly test the behaviour of auth.createUserWithEmailAndPassword in my component, I would greatly appreciate it!

Thank you,

David

Answer №1

The method to create the spy was not visible in the code. It's a bit unusual to use promises instead of Observables. I recommend spying on the method rather than the class and returning a promise that can be controlled:

const resolveFunction;
const rejectFunction;    
beforeEach(() => {
 spyOn(component.auth, 'createUserWithEmailAndPassword').and.returnValue(new Promise((resolve, reject) => {
   resolveFunction = resolve;
   rejectFunction = reject;
 })
}

In your tests, you can now manipulate when the promise is resolved or rejected by simply calling those functions:

it('testing catch block', () => {
   // some code
   rejectFunction('an error object');
})
it('testing then block', () => {
   // some code
   resolveFunction('an error object');
})

For more information on manually creating promises, visit this link.

Answer №2

Just wanted to give an update on my progress. Special thanks to @JeffryHouser for the valuable insights.

Essentially, I was able to achieve what I needed by adjusting my component to handle a Promise from the query. If the results are successful (UserCredentials), the appMessage string is updated with a success message. In case of an error (catch), we display the error message.

Here are the modifications I implemented on the testing side to simulate both resolve and catch scenarios:

  • Changed the test to async using fakeAsync()
  • Used spies to monitor every function called from the user click()
  • Configured a Promise return for the angularFireAuthSignOutSpyObj.createUserWithEmailAndPassword function
  • Simulated the asynchronous behavior with tick()
  • Ensured changes were detected after the promise flow ended with fixture.detectChanges()

The appMessage item now reflects the expected updates following the process

Check out the code below!

Spy declaration

let angularFireAuthSignOutSpyObj: jasmine.SpyObj<any>;
...
 beforeEach(async () => {
    angularFireAuthSignOutSpyObj = jasmine.createSpyObj('AngularFireAuth',
      ['createUserWithEmailAndPassword']);
    ...
      });

User credentials details

//Only required fields are set
export const testUserCredentials: UserCredential = {
  user: {
    providerData: [
      {
        email: '<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="304455434470555d51595c1e535f5d">[email protected]</a>',
      }
    ]
  }
}

Unit Test

  it('should submit registration with form values', fakeAsync(() => {
    spyOn(component, 'signUp').and.callThrough();
    angularFireAuthSignOutSpyObj.createUserWithEmailAndPassword.and.callFake(() => new Promise(
      resolve => {
        resolve(testUserCredentials);
      })
    );

    component.registrationForm.controls.email.setValue('<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="9eeafbedeadefbf3fff7f2b0fdf1f3">[email protected]</a>');
    component.registrationForm.controls.password.setValue('ValidPass123');
    component.registrationForm.controls.passwordCheck.setValue('ValidPass123');
    expect(component.registrationForm.valid).toBeTruthy();
    debugElement.query(By.css("button")).triggerEventHandler("click", null);
    expect(component.signUp).toHaveBeenCalled();
    expect(component.auth.createUserWithEmailAndPassword)
      .toHaveBeenCalledWith(
        component.registrationForm.controls.email.value,
        component.registrationForm.controls.password.value)
    tick();
    fixture.detectChanges();
    expect(component.appMessage).toEqual('Account created : <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="d4a0b1a7a094b1b9b5bdb8fab7bbb9">[email protected]</a>');
  }));

Triggering an Error

    angularFireAuthSignOutSpyObj.createUserWithEmailAndPassword.and.callFake(() => new Promise(() => {
      throw {message: 'test purpose failure'};
    }));

Updated register.component.ts

  signUp() {
    if (this.registrationForm.valid) {
      let createdEmail: string | null | undefined;
      this.auth.createUserWithEmailAndPassword
      (
        this.registrationForm.get('email')?.value,
        this.registrationForm.get('password')?.value
      )
        .then((userCredential: UserCredential) => {
          userCredential?.user?.providerData?.forEach(userInfo => {
            createdEmail = userInfo?.email;
          })
          this.appMessage = "Account created : " + createdEmail;
        })
        .catch((error) => {
          this.appMessage = "Account creation failed : " + error.message;
        });
    } else {
      this.appMessage = 'Submit logic bypassed, form invalid !'
    }
  }

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

Struggling to grasp how to implement Redux and React-router together in one component

I have recently embarked on learning TypeScript and encountered a confusing behavior. Upon encountering this error: Type 'ComponentClass<{}>' is not assignable to type 'StatelessComponent<void | RouteComponentProps<any>> ...

What is the best way to showcase the chosen information?

Typically, I maintain a list of specific entries within cards. When clicking on one of the records in the card, the corresponding data table is supposed to be displayed below that record. An issue arises where after selecting a record and displaying its d ...

Issue with ng-bootstrap carousel: The property 'toArray' cannot be read as it is undefined in the NgbCarousel

Having trouble sliding to the next slide and encountering this error: ERROR TypeError: Cannot read property 'toArray' of undefined at NgbCarousel._getNextSlide (carousel.js:118) Does anyone have any insights on this? import { Component, In ...

What steps should I take to turn off ES Module Error notifications in VSCode?

After switching to the Bun JS Runtime, the distinction between ES Modules and CommonJS became irrelevant as Bun seamlessly handles both. However, VSCode seems to not be on the same page, throwing errors whenever actions that would work in Bun but not in No ...

Automatic type deduction with TypeScript types/interfaces

Attempting to create a function with a return type called UnpackedValuesOnly, which can dynamically ascertain the type of a "packed" value K without requiring explicit user input for defining what K is. Here's my closest attempt so far: //assume this ...

The state array is rejecting the value from the other array, resulting in null data being returned

I am currently attempting to extract image URLs from an HTML file input in order to send them to the backend and upload them to Cloudinary. However, I am facing an issue where despite having the imagesArr populated with images, setting the images state is ...

Angular: DatePipe's output for month is unexpectedly returning as 0

I am currently utilizing DatePipe in order to convert a date string from the format '25-Oct-2017' to '2017-10-25'. Here is the code snippet I am using: this.datePipe.transform('25-Oct-2017', 'yyyy-mm-dd') However, ...

Coloring input fields in Angular Material 2

Changing Angular Material 2 Input Color <md-input-container > <input type="password" md-input placeholder="password"> </md-input-container> I am looking to change the color of the input field when it is clicked. Can anyone provide gu ...

Tips for retrieving items from <ng-template>:

When the loader is set to false, I am trying to access an element by ID that is located inside the <ng-template>. In the subscribe function, after the loader changes to false and my content is rendered, I attempt to access the 'gif-html' el ...

Error: In Angular 4, there is an issue where trying to access the length property of an undefined property results in a

How to Fix TypeError: Cannot Read Property 'length' of Undefined in Angular 4 This is the code snippet that is causing the error: export class UserComponent implements OnInit{ roles:IUserRole[]; sourseRoles: SelectedItem[]; selectedRole:a ...

What is the best way to retrieve response data from an http request in Angular?

I am looking to retrieve a response from a GET HTTP request, and my server is written in JavaScript. The specific part where I send a response is as follows: app.get('/getReport',function(req,res) { try { const data=fs.readFileSync('./ ...

What is the best way to update the body property within a component?

Within my style.sass file, I've set up the common styles for SAP. This file includes the styling for body {}. Is there a way to override this CSS property from a specific component? For instance, I want to adjust the background color of the body in ...

Function as getter/setter

Can property getter/setter be implemented as a function? Traditional getter/setters work like this: class Test { something: any; get prop() { return something; } set prop(value) { something = value; } } let instance = new Test(); inst ...

Encountered an attribute error stating that the 'FirefoxBinary' object does not possess the attribute '_get_firefox_output'

Today, I encountered an issue with my code that was previously working fine on Wednesday. No one else has tampered with it, and yet I am now seeing the error message: 'FirefoxBinary' object has no attribute '_get_firefox_output' I am ...

Unable to locate 'http' in error handling service for Angular 6

My current project involves creating an Error Handling Service for an Angular 6 Application using the HTTP Interceptor. The main goal of this service is to capture any HTTP errors and provide corresponding error codes. However, my lack of familiarity with ...

What could be causing my *ngIf to recognize a non-empty object in Angular 2?

Within my.component.ts, there is a portion in the template that looks like this: <li><a *ngIf="m_userO=={}" [routerLink]="['LoginPage']">Login</a></li> <li><a *ngIf="m_userO!={}" (click)="logOut(m_userO)">{{m_ ...

Bringing in specific functionalities from rxjs in an Angular 2 component

I am currently working on an Angular 2 project that was initialized using the Angular CLI. My main goal is to ensure that the initial load of the project is as fast as possible. To achieve this, I have been focusing on reducing the sizes of all the bundles ...

What is the best way to determine which component/service is triggering a method within an Angular 2 or 4 service?

Is there a way to determine which component or service is triggering the method of a specific service, without the need to pass additional parameters? This information must be identified directly within the service. If you have any insights on how this c ...

Discover the process of implementing Firebase Authentication in Deno Fresh!

I've been trying to set up authentication in Fresh using the official Firebase documentation, https://firebase.google.com/docs/auth but I can't seem to get it to work. Does anyone know of any other resources like docs, articles, or blogs that co ...

Setting a timeout from the frontend in the AWS apigClient can be accomplished by adjusting the

I am currently integrating the Amazon API Client Gateway into my project and I have successfully set up all the necessary requests and responses. Now, I am trying to implement a timeout feature by adding the following code snippet: apigClient.me ...