Angular unit test mock service

Viewed 7488

I have a service that I’m trying to test and that refers to 2 other services

export class UserService {
  private env: EnvConfiguration;

  constructor(private userApiService: UserApiService, private envService: EnvService) {
    this.envService.load().subscribe(env => {
      this.env = env;
    });

    this.userApiService.rootUrl = this.env.apiUrl;
  }

  getUserList(): Observable<User[]> {
    return this.userApiService.getUsers().pipe(
      map(result => result),
      catchError(err => { return throwError(err);
      })
    );
  }
}

And this is my test class :


describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;
  let envServiceSpy = jasmine.createSpyObj('EnvService', ['load']);

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [UserService, {
         provide: EnvService,
         useValue: envServiceSpy
       }],
    });
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
    envServiceSpy = TestBed.inject(EnvService) as jasmine.SpyObj<EnvService>;;
  });

  afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {

    httpMock.verify();

 }));

  it('should be created', () => {
    const stubValue = "apiUrl: 'http://'";
    envServiceSpy.load.and.returnValue(of(stubValue));
    expect(service).toBeTruthy();
    expect(envServiceSpy.load.calls.mostRecent().returnValue)
    .toBe(stubValue);
  });

  it('should return value from observable', () => {
    expect(this.service.getUserList()).toBeTruthy();
  });
});

My problem is that my tests do not pass at all. I have the impression that the problem comes from my mocks I can’t mock my two services

This is my error :

UserService > should be created
TypeError: Cannot read property 'subscribe' of undefined
    at <Jasmine>
    at new UserService (http://localhost:9876/_karma_webpack_/src/app/services/user/user.service.ts:15:27)
    at Object.UserService_Factory [as factory] (ng:///UserService/ɵfac.js:5:10)
    at R3Injector.hydrate (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:17193:42)
    at R3Injector.get (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:16943:1)
    at NgModuleRef$1.get (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:36329:1)
    at TestBedRender3.inject (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/testing.js:3227:1)
    at Function.inject (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/testing.js:3110:1)
    at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/src/app/services/user/user.service.spec.ts:22:23)
    at ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:364:1)
    at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:292:1)
Expected undefined to be truthy.
Error: Expected undefined to be truthy.
    at <Jasmine>
    at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/src/app/services/user/user.service.spec.ts:36:21)
    at ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:364:1)
    at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:292:1)
Expected undefined to be 'apiUrl: 'gttp://''.
Error: Expected undefined to be 'apiUrl: 'gttp://''.
    at <Jasmine>
    at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/src/app/services/user/user.service.spec.ts:38:6)
    at ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:364:1)
    at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:292:1)

My EnvService load configuration and my UserApiService contains metbod how call api with httpClient I'm user Angular 9


I'm update my test :

it('should be created', () => {
  let envConfig: EnvConfiguration;
  envServiceSpy.load.and.returnValue(of(envConfig));
  expect(service).toBeTruthy();
  expect(envServiceSpy.load.calls.mostRecent().returnValue)
    .toBe(envConfig);
});

But I have this error :

Type 'EnvConfiguration' is missing the following properties from type '{ _isScalar: ExpectedRecursive<boolean>; source: ExpectedRecursive<Observable<any>>; operator: ExpectedRecursive<Operator<any, EnvConfiguration>>; ... 6 more ...; toPromise: ExpectedRecursive<...>; }': _isScalar, source, operator, lift, and 6 more.
2 Answers

Your test does not inject the UserApiService properly, thus when you call getUserList() it attempts to start UserApiService.getUsers() which is not defined.

import createSpyObj = jasmine.createSpyObj;
import SpyObj = jasmine.SpyObj;
import {of} from 'rxjs';
// .. Other imports

describe('UserService', () => {
  let service: UserService;
  let envServiceSpy: SpyObj<EnvService>;
  let userApiService: SpyObj<UserApiService>;
  let usersMock = [
    {id: 1, name: 'Walter White', bestQuote: 'I am the one who knocks.'},
    {id: 2, name: 'Jesse Pinkman', bestQuote: 'Yeah, bitch! MAGNETS!'},
  ];
  let envMock = {
    apiUrl: 'http://example.com',
  };

  beforeEach(() => {
    // It is a good idea to re-initiate the spy instance after each run so you do not face any weird side-effects.
    // That way you also do not need to call `mySpy = TestBed.inject(MyService);`
    envServiceSpy = createSpyObj('EnvService', ['load']);
    envServiceSpy.load.and.returnValue(of(envMock))

    userApiService = createSpyObj('UserApiService', ['getUsers'], ['rootUrl']);
    userApiService.getUsers.and.returnValue(of(usersMock));

    TestBed.configureTestingModule({
      providers: [
        UserService,
        {provide: EnvService, useValue: envServiceSpy},
        {provide: UserApiService, useValue: userApiService},
      ],
    });

    service = TestBed.inject(UserService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
  
  it('should set rootUrl for userApiService on init', () => {
    // Considering the `constructor()` did run already due to our initialization in `beforeEach()`
    // we can just assert on our expectations
    expect(envServiceSpy.load).toHaveBeenCalled();
    expect(userApiService.rootUrl).toEqual('http://example.com');
  });

  // Here we test, that the `getUserList()` method in fact mapped
  // the (mocked) response from `getUsers()` properly
  it('should retrieve user list ', (done) => {
    service.getUserList().subscribe((userList) => {
      expect(userList).toEqual(usersMock);
      expect(userApiService.getUsers).toHaveBeenCalled();
      done();
    }, done.fail);
  });
  
  xit('TODO: Write a test that performs the call to `getUsers()` which returns an *error*', () => {
  });
});

I think you're doing a few things wrong, you're missing a service to begin with, the UserApiService, second you don't need TestBed to test a service you can just create a new instance of it and inject the mocks into it. I also don't know why you need this HttpTestingController? is nowhere in your service and lastly you shouldn't be checking what load has returned but that it has been called once the service is initialised/created whatever..., in essence I've taken your code, restructure it a little and sort of guessed what you were trying to do, there is more advice to give you as to your example but this should give you a head start.

describe('UserService', () => {
    let service: UserService;
    let mockUserApiService: jasmine.SpyObj<UserApiService>;
    let mockEnvService: jasmine.SpyObj<EnvService>;
    const stubValue = "apiUrl: 'http://'";
  
    beforeEach(() => {
        mockUserApiService = jasmine.createSpyObj('UserApiService', ['getUsers', 'rootUrl']);
        mockEnvService = jasmine.createSpyObj('EnvService', ['load', 'apiUrl']);
    });

    describe('when the service is created', () => {

        beforeEach(() => {
            mockUserApiService.load.and.returnValue(of(stubValue));
            mockEnvService.rootUrl = 'https://some-site.com';

            service = new UserService(mockUserApiService, mockEnvService);
        });

        it('should call the environment service', () => {
            expect(mockEnvService.load).toHaveBeenCalledTimes(1);
        });

        describe('getUserList', (done) => {
            const users = [] as Users[];

            beforeEach(() => {
                mockUserApiService.getUsers.and.returnValue(of(users));
                service.getUserList();
            });

            it('should call the users api service', () => {
                expect(mockUserApiService.getUsers).toHaveBeenCalled();
            });
        })
    })
});
Related