Angular + Jest
Test cases specific to Local Storage & Behavior Subject with GET method

By: Sagar Panchal
TestCases Angular

At, TechCompose Solutions, we have been following a practice of writing test cases for all front-end and back-end development.

This practice of writing test cases can help in

  • Understanding the requirements much better
  • Uncovers some use cases and underlying complexities for engineers
  • Motivates developers to write refactored and optimized code

We use the Jest testing framework for Angular, NodeJS, and ReactJS.

Introduction to Jest:

Jest is currently the most popular JavaScript Testing Framework in the industry due to its simple syntax, minimum configuration & high speed, and extensive features such as easy mocking, code coverage, etc.

Click here to learn more about Jest.

Reference links to configure Jest in Angular.

Here are some reference links which can help you to configure Jest in Angular.

This blog covers topics related to

  • Pre-requisites for starting to write custom test cases.
  • Local storage test cases.
  • Test cases for GET method with Behavior subject variables.
  1. Pre-requisites that can make your process to write custom test cases hassle-free

    protip-icon

    Protip - While writing test cases for any file(components, services, etc.), make sure to write it in isolation.

    • For example - If the file, for which you are writing the test case, has any functions or variables defined in another file(services or components), you must create mocks first.
    • Why? - It is because we should not make a call to actual functions or variables(at another file) for isolation while writing the test cases. i.e. considered to be the best practice while writing test cases.
    protip-icon

    Protip - Always pass the default test case first. 

    • Why? -This will allow you to resolve all required dependencies in order to write test cases for that particular file. 
    • Results - It will help avoid any dependency-related issues in the future while writing your custom test cases. Again one of the best approaches 🤓.

    Any additional modules used will be mentioned in the imports of the configureTestingModule method of the test cases as shown below.

    beforeEach(waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [RouterTestingModule],
        declarations: [AppComponent],
      }).compileComponents();
    });

    Any additional components used will be mentioned in the declaration of the configureTestingModule method of the test cases as shown below.

    beforeEach(waitForAsync(() => {
      TestBed.configureTestingModule({
        declarations: [AppComponent],
      }).compileComponents();
    });

    Any additional services used, will be mentioned in the providers array of the configureTestingModule method of the test cases as shown below.

    describe('AppComponent', () => {
      beforeEach(waitForAsync () => {
        TestBed.configureTestingModule({
          providers: [{ provide: UserService, useValue: userServiceMock }],
          declarations: [HomeComponent],
        }).compileComponents();
      });
    });

    Note: Here we’ve used useValue while providing a mock for the user-service. You can also use useClass.

    Now I believe we have enough base so let’s dive straight into the Test cases 🚀:

    Note: we’ve attached the Repository consisting code mentioned below for reference.

  2. How to mock LocalStorage

    To write test cases related to local storage, the basic strategy would be to

    • Create a fake localstorage
    • Assign it to the window object as a replacement to the actual localstorage

    It is because, by using our mocked localstorage, we can avoid disturbing actual localstorage while executing the code during test cases.

    Let’s write a test case for the below function where

    Here, We’re signing out a user by removing user data from local storage.

    public onLogOut(): void {
      this.userService.isUserLoggedIn.next(false);
      localStorage.removeItem('user');
    }

    Now, first, we’ll mock the localstorage in the spec file as shown below

    let setUpLocalStorageMock = () => {
      localStorageMock = (function () {
        let store: any = {};
    
        return {
          getItem(key: string) {
            return store[key];
          },
          setItem(key: string, value: string) {
            store[key] = value;
          },
          clear() {
            store = {};
          },
          removeItem(key: string) {
            delete store[key];
          },
          getAll() {
            return store;
          },
        };
      })();
    
      // this is the place where we place our mocked localstorage instead of the real one.
      Object.defineProperty(window, 'localStorage', { value: localStorageMock });
    
      localStorage.setItem('user', JSON.stringify(userData));
    };
    

    Note: In the last line, We've also added a fake userData for the actual function to remove.

    Now, you can just add the setUpLocalStorageMock function to your spec file and call it in beforeEach method.

    beforeEach(async () => {
      await TestBed.configureTestingModule({
        declarations: [HomeComponent],
        providers: [{ provide: UserService, useValue: userServiceMock }],
      }).compileComponents();
    
      setUpLocalStorageMock();
    
      fixture = TestBed.createComponent(HomeComponent);
      component = fixture.componentInstance;
      fixture.detectChanges();
    });

    And finally, we’re all set to write an actual test case for the function.

    it('onLogOut function test.', () => {
      /* just for the reference that the user data exist
      initially before we remove it by onLogOut function.
      */
      expect(localStorage.getItem('user')).not.toBeFalsy();
    
      //actual test case starts here.
      component.onLogOut();
      expect(userServiceMock.isUserLoggedIn.value).toBeFalsy();
      expect(localStorage.getItem('user')).toBeFalsy();
    });
  3. Test case for GET method with Behavior subject variables

    This particular test case is related to the Behavior subject and the GET method to access the value of the Behavior subject.

    Following is the example of the service file that has

    • isUserLoggedIn as a Behavior subject with a boolean value
    • userLoginValue as a GET method of that particular behavior subject
    export class UserService {
      public usersArray: BehaviorSubject<any> = new BehaviorSubject([]);
    
      public isUserLoggedIn: BehaviorSubject<any> = new BehaviorSubject(false);
    
      public userData: BehaviorSubject<any> = new BehaviorSubject(undefined);
    
      constructor() {}
    
      public get userLoginValue(): boolean {
        return this.isUserLoggedIn.value;
      }
    }

    Here’s how we’ll write a test case for the userLoginValue with the GET method.

    describe('UserService', () => {
      let service: UserService;
    
      beforeEach(() => {
        TestBed.configureTestingModule({});
        service = TestBed.inject(UserService);
      });
    
      it('should be created', () => {
        expect(service).toBeTruthy();
      });
    
      it('Check userLoginValue', () => {
        /* here we're changing the default of 
        isUserLoggedIn behavior subject i.e false to true.
        */
        service.isUserLoggedIn.next(true);
    
        // here's where we access the value of the isUserLoggedIn behavior subject.
        let loginValue = service.userLoginValue;
    
        expect(loginValue).toBeTruthy();
      });
    });

    Now, let’s see how we can write a test case for the component, using the userLoginValue with the GET method.

    Below is the code snippet of the component accessing it,

    ngOnInit(): void {
      /* here's where value of isUserLoggedIn behavior 
      subject is accessed via userLoginValue */
      this.isUserLoggedIn = this.userService.userLoginValue;
    
      this.userService.usersArray.subscribe((usersList: any[]) => {
        this.users = usersList;
      });
    }

    We’ve to mock user service first for writing test cases for the component so that, no variables or functions of the actual service will be used & the testing for the component could be performed in isolation.

    protip-icon

    Pro-tip: - In the following code snippet, we’ve mocked only necessary variables and functions that are used in the component and not everything from the service file mentioned above. This way you can avoid spending extra effort 😎.

    userServiceMock = {
      isUserLoggedIn: new BehaviorSubject(false),
      userLoginValue: {},
      userData: new BehaviorSubject(undefined),
      usersArray: new BehaviorSubject([]),
    };
    
    // attaching GET method to the userLoginValue function.
    Object.defineProperty(userServiceMock, 'userLoginValue', {
      get: () => {
        return userServiceMock.isUserLoggedIn.value;
      },
    });

    And so here’s how we can write a test case for calling the component function.

    it('Test isUserLoggedIn behavior subject & userLoginValue get method.', () => {
      // assigning true to the value instead of it's default value false.
      userServiceMock.isUserLoggedIn.next(true);
    
      component.ngOnInit();
      // storing value in variable to test
      const userLoginValue: boolean = userServiceMock.userLoginValue;
    
      /* testing the value of variable
      Note: storing userLoginValue value in variable & write 
      expect is not needed for actual code coverage it's just for reference */
      expect(userLoginValue).toBeTruthy();
    
      // testing the actual component variable using the value of userLoginValue
      expect(component.isUserLoggedIn).toBeTruthy();
    });