0

My general question is how to setup an angular test when attempting to test a service that has other services as dependencies, with some of those services having other dependencies. Also, my services use both observables and promises. Do I need to mock every single constructor and method? That is what the error I am getting (detailed in the comments below) seems to indicate. How can I best accomplish this? I would also greatly appreciate pointers on how to setup this kind of test using marbles or pointers to any useful resources.

My code is largely inspired by this example which seems to do what I want to do: https://gist.github.com/btroncone/b1ef4df357278d0af33372302eb2141e

//Service I would like to test
export class ServiceToTest{
    constructor( 
        private dependency1: Dependency1Service,
        private dependency2: Dependency2Service
    ) {}

    .
    .
    .
    .

    getData1() : Promise<stsring>{return dependency1.getData1();} //method I want to test
    getData2(){}
    getData2(){}
}


export class Dependency1Service{
    constructor( 
        private dependency1: SubDependency1Service,
        private dependency2: SubDependency2Service,
        private dependency3: SubDependency3Service,
    ) {}

    .
    .
    .
    .

    getData1() : Observable<IPayload<IData>>{ return ...}
    getData2(){}
    getData2(){}
}

export class Dependency2Service{
    constructor( 
        private dependency1: SubDependency3Service,
        private dependency2: SubDependency4Service,
        private dependency3: SubDependency5Service,
    ) {}

    .
    .
    .
    .

    getData1(){}
    getData2(){}
    getData2(){}
}

export class SubDependency1Service{
    constructor( 
        private dependency1: AnotherDependency1Service,
        private dependency2: AnotherDependency2Service,
        private dependency3: AnotherDependency3Service,
    ) {}

    .
    .
    .
    .

    getData1(){}
    getData2(){}
    getData2(){}
}

//=======================================================================
//TEST SETUP
//=======================================================================



const payload: Observable<IPayload<IData>> = of({something1,something2})

export class MockDependency1Service{
    getData1(){ return payload };
}

const data2 = new Promis<DataType[]>((resolve,reject) => {
    setTimeout(() => {
        resolve([]);
    }, 1000);
});

export class MockDependency2Service{
    getData(){ return data };
}

describe('Service to test', () => {    
    let mockDependency1Service = new Dependency1Service();    
    let mockDependency2Service = new Dependency2Service();
    
    beforeEach(() => {        
        spyOn(MockDependency1Service, 'getData1');        
        spyOn(MockDependency2Service, 'getData1').and.returnValue(payload);    });
    
    beforeEach(() => TestBed.configureTestingModule ({        
        providers: [            
            {provide: Dependency1Service, useValue: mockDependency1Service},                    
            {provide: Dependency2Service, useValue: mockDependency2Service}
    }));

    //observable    
    it('should return result', (done) => {        
        const service = new TwilioConnectionService(mockDataService,mockLocalTrack);       //error here on mockDataService -- Argument of type 'MockDependency1Service' is not assignable to parameter of type 'Dependency1Service'. Type 'MockDependency1Service' is missing the following properties from type 'Dependency1Service': dependency1, dependency2, dependency3, getData2,getData3' --basically any injected property or any method that is not defined in export class MockDependency1Service.
        const obs = service.getAccessToken();        
            obs.subscribe(res => {            
                setTimeout(() => {                
                    expect(res.data.jwt).toBe('0234ljksdlkgoier')//data.jwt)                
                    done();            
            }, 100);        
        });
    });

1 Answer 1

1

If you only want to unit test the service the basic approach is to stub/mock only the direct dependencies and only the methods that are used by the service under test. You don't care about the transitive dependencies.

With jasmine you can use createSpy() to simplify the setup. The angular docs have a lot of information about testing in general and about testing services including dependencies.

Another solution is to let the Mock extend from the Service it mocks. But that is only usable if it has only a few methods.

Simplest solution is to cast it to any.

   const service = new TwilioConnectionService(mockDataService as any, mockLocalTrack);

Examples:

we have BarService and FooService:

export class FooService {
  constructor() { }

  getValue (): Observable<string> {
    return from(['foo', 'fooo', 'foooo']);
  }

  other () {
    // ....
  }

export class BarService {
  constructor(private fooService: FooService) { }

  callFoo (): Observable<string> {
    return this.fooService.getValue()
      .pipe(
        map(f => f + 'bar!')
      )
  }
}

BarService is the one we want to test.

using a manual stub:

  describe('manual stub', () => {
    let fooStub = {
      getValue() {
        return from(['foo'])
      }
    }

    it('adds bar to foo service result', () => {
      // without the cast: not assignable error because of other()
      let bs = new BarService(fooStub as any);

      bs.callFoo()
        .subscribe(r => {
          expect(r).toEqual('foobar!')
        })
    });
  })

using a jasmine spy:

  describe('jasmine stub', () => {
    const fooSpy = jasmine.createSpyObj('FooService', ['getValue']);

    it('adds bar to foo service result', () => {
      fooSpy.getValue.and.returnValue(from(['foo']));

      let bs = new BarService(fooSpy);

      bs.callFoo()
        .subscribe(r => {
          expect(r).toEqual('foobar!')
        })
    });
  })

using jasmine & jasmine-marble (you have to add it to package.json):

  import {cold} from 'jasmine-marbles'; 

  describe('marble', () => {
    const fooSpy = jasmine.createSpyObj('FooService', ['getValue']);

    it('adds bar to foo service result', () => {
      const source = cold('ab|', {a: 'foo', b: 'fooo'});
      const expected = cold('xy|', {x: 'foobar!', y: 'fooobar!'});

      fooSpy.getValue.and.returnValue(source);

      let bs = new BarService(fooSpy);
      expect(bs.callFoo()).toBeObservable(expected);
    });
  })
Sign up to request clarification or add additional context in comments.

6 Comments

That captures exactly what I am trying to do and I have actually already tried several of the options detailed on that link, but with no luck. For whatever reason, when I do const valueServiceSpy = jasmine.createSpyObj('ValueService', ['getValue']); I am not able to use the syntax valueServiceSpy.getValue.and.returnValue(stubValue) to chain the .and.returnValue(stubValue) to my created valueServicesSpy.Could this be because my getValue method returns an observable? If so, how do I handle this?
Also, what if I need to do this for several methods? For example jasmine.createSpyObj('ValueService', ['getValue','getValue2','getValue3']). How would I handle returning a value for each? By the way, I meant that my getValue returns an observable.
I added three simple test examples with Observable.
Thanks! got it to work with both the manual and stub options. For the stub option my issue was that I had strongly typed my variable as the actual service that I am trying to mock, so in this case const fooSpy : FooService; fooSpy = jasmine.createSpyObj('FooService', ['getValue']);' Removing the strong typing gave me access to the .and.returnValue()` option to set my return value.
So I'm assuming strong typing cannot be enforced in testing scenarios like this? Or is there a way to still accomplish the mocking with strong typing?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.