1

I am trying to add unit tests to my angular component which is dependent on a service. The component is subscribing to a flag variable in the service. I am trying to use ng-mocks to mock the service using the MockProvider. And while trying to set the default mock behaviour in the test.ts file, am getting an error, which I couldn't make sense of.

"The last overload gave the following error. Argument of type 'typeof SomeService' is not assignable to parameter of type 'AnyDeclaration<{ flags: Observable; flagChange: Observable; }>[]'."

Below is the code. Any other approach to mock the subject and unit test the component. If I am not initializing the flagChange to EMPTY in the test.ts, I am getting the "Cannot read properties of undefined (reading 'subscribe')" Error. Service.ts

import { Injectable, OnDestroy } from '@angular/core';
import * as LDClient from 'launchdarkly-js-client-sdk';
import { LDFlagValue } from 'launchdarkly-js-client-sdk';
import { Observable, Subject, of } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class SomeService implements OnDestroy {
  ldClient!: LDClient.LDClient;
  flags: LDClient.LDFlagSet;
  flagChange!: Subject<Object>;
  private readonly _destroying$ = new Subject<void>();
  ldClientId: string;

  constructor(private globalvars: GlobalVars) {
    this.ldClientId = 'client-id';
    this.flags = {
      'allow_edit': false,
    };
  }

  ngOnDestroy(): void {
    this._destroying$.next(undefined);
    this._destroying$.complete();
  }

  initializeLD() {
    
    const context = {
      kind: 'user',
      key: 'my-key',
    };
    
    this.ldClient = LDClient.initialize(this.ldClientId, context);
    this.ldClient.on('initialized', () => {
      const boolFlagValue = this.ldClient.variation(
        'allow_edit'
      ) as boolean;
      
      this.flags['allow_edit'] = boolFlagValue;
      this.flagChange.next(this.flags);
    });
    this.ldClient.on('change', (allChanges) => {
      this.flags['allow_edit'] =
        allChanges['allow_edit'].current;
      this.flagChange.next(this.flags);
    });
    this.ldClient.on('ready', () => {
      const boolFlagValue = this.ldClient.variation(
        'allow_edit'
      ) as boolean;
    });
  }
}

test.ts

ngMocks.defaultMock(SomeService , () => ({
  flags: EMPTY,
  flagChange: EMPTY //Have declared it as "flagChange: ()=> EMPTY" as well
}));

component.ts

 constructor(private featureFlag: SomeService) {
    this.allow_edit= featureFlag.flags['allow_edit'];
    this.allow_edit= featureFlag.flagChange.subscribe((flags:any) => {
      this.allow_edit= flags['allow_edit'];
    });
  }

spec.ts

fdescribe('SomeComponent', () => {
  let component: SomeComponent;
  let fixture: ComponentFixture<SomeComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [HttpClientModule],
      declarations: [SomeComponent],
      providers: [
        MockProvider(SomeService),
      ],
    }).compileComponents();


    fixture = TestBed.createComponent(SomeComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => { 
    expect(component).toBeTruthy();
  });
});
satanTime
  • 12,631
  • 1
  • 25
  • 73

2 Answers2

1

The thing here is that EMPTY doesn't satisfy Subject<Object>, because EMPTY doesn't provide .next(). That's why you get the error.

To fix that you have 2 options: cast type (suppress error) or use proper mock (satisfy types):

cast type

even TS won't show you errors, in this case flagChange.next will fail, because EMPTY doesn't have it.

ngMocks.defaultMock(SomeService , () => ({
  flags: EMPTY,
  flagChange: EMPTY as never,
}));

proper mock

https://stackblitz.com/edit/github-topg3g?file=src%2Ftest.spec.ts

ng-mocks providers MockService, which mocks all methods and applies customizations if any specified.

ngMocks.defaultMock(SomeService , () => ({
  flags: EMPTY,
  flagChange: MockService(Subject, EMPTY),
}));

Now flagChange.next() is a mock function, and .subscribe() comes from EMPTY.

satanTime
  • 12,631
  • 1
  • 25
  • 73
  • Thanks a ton. Got what I missed. Although with EMPTY I still had some overload errors, but I was able to workaround with initializing it with {} or a default value for the flag change. – seeker_1234567 Jun 12 '23 at 12:11
  • That's great, I also added detailed information to the docs: https://ng-mocks.sudo.eu/extra/mock-observables#mock-subject, it turned out that `EMPTY` and `of` didn't work well with `MockService`, but now it has been solved in `v14.11.0`. – satanTime Jun 14 '23 at 05:51
-1

According to the 2nd example here: https://ng-mocks.sudo.eu/api/MockProvider, try:

MockProvider(SomeService, { flags: EMPTY, flagChange: EMPTY }, 'useValue'),
AliF50
  • 16,947
  • 1
  • 21
  • 37
  • 1
    I have tried it previously and has the same overload error as the question, because EMPTY doesn't satisfy the Subject type as mentioned by @satanTime in the above answer. – seeker_1234567 Jun 12 '23 at 12:15