Search code examples
angularjasmine

Angular Jasmine - override one property of service while keeping other methods/properties


I have an Angular service which has as private member a class instance (Keycloak). The class is defined in an external npm library (keycloak-js). In the constructor of the service, I call keycloak.init() which makes some HTTP requests. When writing unit tests for the service, I want to avoid calling the real keycloak.init function. All other methods/properties of the service should remain the original ones. Here is some minimal code:

//defined in external npm library
export class Keycloak {
  init() {
    //makes HTTP calls
    console.log('Obj func');
  }
}

@Injectable({
  providedIn: 'root',
})
export class KeycloakService{
  private keycloak;
  constructor() {
    this.keycloak = new Keycloak();
    this.keycloak.init();
  }
  //other methods and props
}

describe('KeycloakService', () => {
  let service: KeycloakService;
  const objectSpy: jasmine.SpyObj<Keycloak> = jasmine.createSpyObj('Keycloak', [
    'init',
  ]);

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [KeycloakService],
      //somehow override KeycloakService.keycloak with keycloakSpy, while keeping other methods and properties untouched
    });
    service = TestBed.inject(KeycloakService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
});

How could I achieve this?

Update: I'm managed to solve the problem by wrapping the Keycloak class into an Angular service, but I still wonder if this can be avoided. Updated complete code:

//defined in external npm library
export class Keycloak {
  init() {
    //makes HTTP calls
    console.log('Obj func');
  }
}

@Injectable({
  providedIn: 'root',
})
export class KeycloakWrapper extends Keycloak {}

@Injectable({
  providedIn: 'root',
})
export class KeycloakService {
  constructor(private keycloak: KeycloakWrapper) {}
  //other methods and props
}

describe('KeycloakService', () => {
  let service: KeycloakService;
  const keycloakSpy: jasmine.SpyObj<KeycloakWrapper> = jasmine.createSpyObj(
    'KeycloakWrapper',
    ['init']
  );

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        KeycloakService,
        { provide: KeycloakWrapper, useValue: keycloakSpy },
      ],
    });
    service = TestBed.inject(KeycloakService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
});

Solution

  • I found a solution, using @Inject and @Optional:

    @Injectable({
      providedIn: 'root',
    })
    export class KeycloakService {
      constructor(@Inject(Keycloak) @Optional() private keycloak?: Keycloak) {
        this.keycloak = keycloak || new Keycloak();
        this.keycloak.init();
      }
      //other methods and props
    }
    

    describe('KeycloakService', () => {
      let service: KeycloakService;
      const keycloakSpy: jasmine.SpyObj<Keycloak> = jasmine.createSpyObj(
        'Keycloak',
        ['init']
      );
    
      beforeEach(() => {
        TestBed.configureTestingModule({
          providers: [
            { provide: Keycloak, useValue: keycloakSpy },
            KeycloakService,
          ],
        });
        service = TestBed.inject(KeycloakService);
      });
    
      it('should be created', () => {
        expect(service).toBeTruthy();
      });
    });