Home > OS >  How to test service with observable subscription without emitting a value
How to test service with observable subscription without emitting a value

Time:11-16

I have a service that subscribes to an observable field of a dependency in the constructor. I am trying to write unit tests for this service, and in doing so, am trying to ignore the subscription to that observable. In other words, for some of these tests, I never want to emit a value, as I'm testing an unrelated function. How can I create a mock/spy of my service that simply does nothing with that observable?

Service

export class MyService {
  constructor(private authService: AuthService) {
    this.authService.configure();
    this.authService.events.subscribe(async (event) => {
      await this.authService.refreshToken();
  });
}

AuthService dependency

This is a snippet of the 3rd party service that my service depends on.

export declare class AuthService implements OnDestroy {
    events: Observable<Event>;
    // ... other fields/functions
}

Spec

describe('MyService', () => {
  let authServiceSpy: jasmine.SpyObj<AuthService>;

  beforeEach(() => {
    authServiceSpy = jasmine.createSpyObj<AuthService>(
      ['configure', 'refreshToken'],
      {
        get events() { return defer(() => Promise.resolve(new Event('some_event'))); }
      }
    );

    
    TestBed.configureTestingModule({});
  });

  it('should create service and configure authService', () => {
    const myService = new myService(authServiceSpy);
    expect(myService).toBeTruthy();
    expect(authServiceSpy.configure).toHaveBeenCalledOnce();
  });
});

Whenever I run this test, the expectation is met but it also runs the callback in the observable subscription, and I haven't mocked the refreshToken call (and don't want to have to).

CodePudding user response:

You need to inject your mock service, like this:

let authService: AuthService;
...
TestBed.configureTestingModule({
  providers: [
    { provide: AuthService, useValue: authServiceSpy}
  ]
});
authService = TestBed.inject(AuthService);

Then you can also spy on the event subscribe function, so that your spy will be called, not the original function.

spyOn(authService.events, 'subscribe');

You can also just use of() for the mock value of events, instead of the defer call you have there.

CodePudding user response:

Ok, after much head-banging, I figured out what I needed to do. If I create a Subject and return it from my mocked events field, I can control the value emits. Without calling next() on my Subject, no value is emitted. It also allows me to test the callback of the subscribe function in a separate test.

describe('MyService', () => {
  let authServiceEventsSubject: Subject<Event>;
  let authServiceSpy: jasmine.SpyObj<AuthService>;

  beforeEach(() => {
    authServiceSpy = jasmine.createSpyObj<AuthService>(
      ['configure', 'refreshToken'],
      {
        get events() { return authServiceEventsSubject; }
      }
    );
    
    TestBed.configureTestingModule({});
  });

  // TEST PASSES
  it('should create service and configure authService', () => {
    const myService = new myService(authServiceSpy);
    expect(myService).toBeTruthy();
    expect(authServiceSpy.configure).toHaveBeenCalledOnce();
  });

  // TEST PASSES
  it('should refresh the access token on "token_expires" event', () => {
    const myService = new MyService(environmentConfigSpy, oAuthServiceSpy);
    // Emit a 'token_expires' event
    authServiceEventsSubject.next(new Event('token_expires'));
    expect(authServiceSpy.refreshToken).toHaveBeenCalledTimes(1);
  })
});
  • Related