Testing Angular components: Mocking a service within another service or mocking an abstract class

I am looking to test a service that extends an abstract class. This abstract class includes the constructor and the method getId.

Here is the code snippet:

export abstract class ClientCacheService {
    private subscriptionId: string;

    protected getId(key :string, prefix:string=""): string {
        return `${this.subscriptionId}_${prefix}_${key}`;
    }

    constructor() {
        this.subscriptionId = new AppContextService().organizationKey();
    }

    abstract setCache(key :string, prefix:string, object: ICacheble): void;
    abstract getCache(key :string, prefix:string): ICacheble | null;
    abstract removeCache(key :string, prefix:string): void;
}

@Injectable()
export class MemoryCacheService extends ClientCacheService {
    constructor() {
        super();
    }
    setCache(key: string, prefix: string, object: ICacheble): void {
        window[this.getId(key, prefix)] = JSON.stringify(object);
    }    
    getCache(key: string, prefix: string): ICacheble | null {
        let res = window[this.getId(key, prefix)];
        return res ? JSON.parse(res) : null;
    }
    removeCache(key: string, prefix: string): void {
        delete window[this.getId(key, prefix)];
    }
}

I have two options:

  1. Mock the ClientCacheService
  2. Mock the AppContextService which is inside the constructor of ClientCacheService

My preference is the second option (mocking the AppContextService), but I am open to considering the first option as well.

In the provided code below, I attempted to mock the ClientCacheService, however, the MemoryCacheService does not have a defined subscriptionId, causing my 'should be possible set cache' test case to fail.

import { MemoryCacheService } from "./memory-cache.service";
import { ICacheble } from "interfaces/cacheble.interface";
import { TestBed, inject } from "@angular/core/testing";
import { ClientCacheService } from "./client-cache.service";

export class CacheableObject implements ICacheble {
    prop1: String;
    prop2: Boolean;
    constructor() {
        this.prop1 = "prop1 testable";
        this.prop2 = true;
    }
    equals(cacheableObject: CacheableObject): boolean {
        return this.prop1 === cacheableObject.prop1 && 
               this.prop2 === cacheableObject.prop2;
    }
}

export class MockClientCacheService {
    private subscriptionId: string;
    constructor() {
        this.subscriptionId = "Just a subscription";
    }
}

describe('MemoryCacheService Test cases', () => {
    let memoryCacheService: MemoryCacheService;
    beforeEach(() => {
        TestBed.configureTestingModule({
            providers: [
                { provide: ClientCacheService, useClass: MockClientCacheService },
                MemoryCacheService
            ]
        });
    });

    it('should be possible to instantiate it', inject([MemoryCacheService], (memoryCacheService:MemoryCacheService)=> {
        expect(memoryCacheService).toBeDefined();
    }));

    it('should be possible to set cache',()=> {
        let cacheableObject: CacheableObject = new CacheableObject();
        memoryCacheService.setCache("test_key", "test_prefix", cacheableObject);
        let storedObject: CacheableObject = memoryCacheService.getCache("test_key", "test_prefix") as CacheableObject;
        expect(storedObject.equals(cacheableObject)).toBeTruthy();
    });

});

Answer №1

The issue lies in the process of mocking the ClientCacheService within the providers array:

{ provide: ClientCacheService, useClass: MockClientCacheService }
.

Evidence: if one attempts to include a console.log statement within the constructor of MockClientCacheService, the output will not be displayed in the console. This indicates that the MemoryCacheService service ultimately inherits from the original

abstract class ClientCacheService
(you could also log messages within the ClientCacheService constructor and see them on the console).

Explanation: the inclusion of

{ provide: ClientCacheService, useClass: MockClientCacheService }
only functions when the Dependency Injector recognizes the service. However, since the
abstract class ClientCacheService
exists outside the realm of DI in your codebase, mocking it within the TestBed.configureTestingModule method becomes impossible.

Alternative solution: consider conducting tests for your classes independently. For instance, you can write specific tests for the abstract class itself (refer to this SO post for additional insights on testing abstract classes). Subsequently, proceed with unit tests for the derived class MemoryCacheService.

Potential concern: within your scenario, the constructor of

abstract class ClientCacheService
instantiates a new object of
AppContextService</code), complicating the process of mocking the <code>AppContextService
class. A feasible compromise involves refactoring the code to incorporate dependency injection:

export abstract class ClientCacheService {
  ...    
  constructor(appContextService: AppContextService) {
    this.subscriptionId = appContextService.organizationKey();
  }
  ...
}

Consequently, the appContextService instance must be passed to the super constructor as follows:

@Injectable()
export class MemoryCacheService extends ClientCacheService {
  constructor(private appContextService: AppContextService) {
    super(appContextService);
  }
  ...
}

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

The variable 'cache' is not recognized by Angular

Everything runs smoothly on my local Angular app, but once deployed on Heroku using a Go server, I encounter issues with aot being disabled in the Angular build on Chrome and Opera, particularly on mobile devices using Linux and OSX. However, Safari presen ...

Angular - Navigate to Login Page post registration and display a confirmation message

As a newcomer to Angular, I am currently working on an Angular and Spring Boot application. So far, I have created components for user login and registration along with validation features. Now, my goal is to redirect the user to the login page upon succes ...

"Upon the addition of a child, no response is being given

My database structure resembles the following: https://i.sstatic.net/duWdk.png /* formsById formId usersList userId */ I am trying to retrieve a list of all users (usersList) associated with a specific formId. Below is my method ...

How can I access the label of an input field?

With the for attribute, we can connect a label to a control, such as an <input>, so that clicking on the label focuses on the control. That's pretty neat. Here's a question that might sound a bit silly. How about the reverse? Is there a wa ...

What could be the reason for the malfunctioning of the basic angular routing animation

I implemented a basic routing Angular animation, but I'm encountering issues where it's not functioning as expected. The animation definition is located in app.component.ts with <router-outlet></router-outlet> and two links that shoul ...

Angular2 Error: Unresolved Reference Issue

Within my data-table.component.ts file, I have a reference to TableSorting which is also defined in the same file. Upon running my application, I encountered the following error in the console: Uncaught ReferenceError: TableSorting is not defined at ...

Implementing a fixed search bar in Angular for a dropdown selection using mat-select

I'm currently utilizing a mat-select widget from Angular-Material in my application To enhance user experience, I am adding a search bar at the top of the select dropdown for filtering options: <mat-select placeholder="Select a store" ...

Combination of two generic interfaces creates a union type

I have been diving into the world of typescript and I encountered a challenge with the syntax of union types, specifically when using a generic interface: interface ArrayElementError { kind: 'failure' reason: string } interface ArrayElementS ...

Issues with POST requests in Angular causing failures

I am currently developing a web application using Angular version 5 or higher. My goal is to set up a POST request where the user can input a company name, which will then be saved to a MongoDB database. Given that this is my first Angular project, I am c ...

Using Typescript in React to render font colors with specific styling

Attempting to utilize a variable to set the font color within a react component, encountering an error with my <span>: Type '{ style: "color:yellow"; }' is not assignable to type 'HTMLProps<HTMLSpanElement>' The use of yel ...

Restricting union types by a specific property

I'm facing an issue when attempting to narrow down a type based on a property. To explain it better, here's a simplified version in code: type User = { id: number; name: string; } type CreateUser = { name?: string; } const user: User | Cr ...

Creating an npm library using TypeScript model classes: A step-by-step guide

Currently, I am working on a large-scale web application that consists of multiple modules and repositories. Each module is being developed as an individual Angular project. These Angular projects have some shared UI components, services, and models which ...

Obtain the position and text string of the highlighted text

I am currently involved in a project using angular 5. The user will be able to select (highlight) text within a specific container, and I am attempting to retrieve the position of the selected text as well as the actual string itself. I want to display a s ...

Angular 2 does not update the variable value within a dataservice call on the page until you navigate away from the page and then come back to it

Currently, I am working with Angular2 and have encountered a strange issue. I have a variable that I display on the page, and when a button is clicked, a data service is called to update the value of this variable. Surprisingly, even after changing the val ...

Issue with Angular 4 Bootstrap Carousel

I encountered a console error that I couldn't resolve while working on my project. The technology stack involves Angular 4 and Bootstrap. Unfortunately, my frontend developer is unavailable this weekend, and I'm unsure if there are any missing d ...

Troubleshooting: The issue of importing Angular 2 service in @NgModule

In my Angular 2 application, I have created an ExchangeService class that is decorated with @Injectable. This service is included in the main module of my application: @NgModule({ imports: [ BrowserModule, HttpModule, FormsModu ...

Displaying HTML content in Angular 15

Struggling with Angular 15.2, I'm attempting to develop a component that can render valid HTML input. My initial approach involved using ElementRef and innerHTML: constructor( private readonly componentElement: ElementRef, ) {} ngOnInit(): void { ...

Signing in to a Discord.js account from a React application with Typescript

import React from 'react'; import User from './components/User'; import Discord, { Message } from 'discord.js' import background from './images/background.png'; import './App.css'; const App = () => { ...

Set up a personalized React component library with Material-UI integration by installing it as a private NPM package

I have been attempting to install the "Material-UI" library into my own private component library, which is an NPM package built with TypeScript. I have customized some of the MUI components and imported them into another application from my package. Howe ...

React TypeScript - Issue with passing props to Hooks causing type errors

I have set up a codesandbox project to demonstrate my problem 1) Initially, I created the <Input> component for styling and tracking input content. 2) While everything was functional, adding more forms prompted me to create a useInput hook for easi ...