Search code examples
angularrxjsobservable

How to test service with observable subscription without emitting a value


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).


Solution

  • 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);
      })
    });