Jasmine Spy 没有返回正确的值

Jasmine Spy not Returning Correct Value

在我的 Jasmine 测试规范中,有时我会监视 authState,我的模拟服务的 属性 mockAngularFireAuth 和 return 代表状态的不同值该特定测试下的应用程序。

在一次测试中,这完美地工作并且断言是正确的;看测试:

AuthService
  catastrophically fails

然而,当我在测试中以完全相同的方式监视 authState 时(例如)...

AuthService
  can’t authenticate anonymously
    AuthService.currentUid
      should return undefined

…断言expect(service.currentUid).toBeUndefined()失败。

currentUid 保留为最初设置的字符串 ("17WvU2Vj58SnTz8v7EqyYYb0WRc2")。

这是我的测试规格的精简版(这仅包括有问题的测试规格):

import { async, inject, TestBed } from '@angular/core/testing';

import { AngularFireAuth } from 'angularfire2/auth';
import 'rxjs/add/observable/of';
import { Observable } from 'rxjs/Rx';

import { AuthService } from './auth.service';
import { MockUser} from './mock-user';
import { environment } from '../environments/environment';

// An anonymous user
const authState: MockUser = {
  displayName: null,
  isAnonymous: true,
  uid: '17WvU2Vj58SnTz8v7EqyYYb0WRc2'
};

// Mock AngularFireAuth
const mockAngularFireAuth: any = {
  auth: jasmine.createSpyObj('auth', {
    'signInAnonymously': Promise.resolve(authState)
  }),
  authState: Observable.of(authState)
};

describe('AuthService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        { provide: AngularFireAuth, useValue: mockAngularFireAuth },
        { provide: AuthService, useClass: AuthService }
      ]
    });
  });

  …

  describe('can’t authenticate anonymously', () => {

    …

    describe('AuthService.currentUid', () => {
      beforeEach(() => {
        // const spy: jasmine.Spy = spyOn(mockAngularFireAuth, 'authState');
        //
        // spy.and.returnValue(Observable.of(null));
        //
        mockAngularFireAuth.authState = Observable.of(null);
      });

      it('should return undefined',
        inject([ AuthService ], (service: AuthService) => {
          expect(service.currentUid).toBeUndefined();
        }));
    });
  });

  describe('catastrophically fails', () => {
    beforeEach(() => {
      const spy: jasmine.Spy = spyOn(mockAngularFireAuth, 'authState');

      spy.and.returnValue(Observable.throw(new Error('Some catastrophe')));
    });

    describe('AngularFireAuth.authState', () => {
      it('should invoke it’s onError function', () => {
        mockAngularFireAuth.authState.subscribe(null,
          (error: Error) => {
            expect(error).toEqual(new Error('Some catastrophe'));
          });
      });
    });

    describe('AuthService.currentUid', () => {
      beforeEach(() => {
        mockAngularFireAuth.authState = Observable.of(null);
      });

      it('should return undefined',
        inject([ AuthService ], (service: AuthService) => {
          expect(service.currentUid).toBeUndefined();
        }));
    });
  });

  describe('is authenticated anonymously already', () => {
    beforeEach(() => {
      // const spy: jasmine.Spy = spyOn(mockAngularFireAuth, 'authState');
      //
      // spy.and.returnValue(Observable.of(authState));
      //
      mockAngularFireAuth.authState = Observable.of(authState);
    });

    describe('.authSate.isAnonymous', () => {
      it('should be true', async(() => {
        mockAngularFireAuth.authState.subscribe((data: MockUser) => {
          expect(data.isAnonymous).toBeTruthy();
        });
      }));
    });

    describe('AuthService.currentUid', () => {
      it('should return "17WvU2Vj58SnTz8v7EqyYYb0WRc2"',
        inject([ AuthService ], (service: AuthService) => {
          expect(service.currentUid).toBe('17WvU2Vj58SnTz8v7EqyYYb0WRc2');
        }));
    });
  });

  describe('is authenticated with Facebook already', () => {
    beforeEach(() => {
      const obj: MockUser = authState;
      // const spy: jasmine.Spy = spyOn(mockAngularFireAuth, 'authState');
      //
      // spy.and.returnValue(Observable.of(Object.assign(obj, {
      //   isAnonymous: false,
      //   uid: 'ZzVRkeduEW1bJC6pmcmb9VjyeERt'
      // })));
      //
      mockAngularFireAuth.authState = Observable.of(Object.assign(obj, {
        isAnonymous: false,
        uid: 'ZzVRkeduEW1bJC6pmcmb9VjyeERt'
      }));
    });

    describe('.authSate.isAnonymous', () => {
      it('should be false', () => {
        mockAngularFireAuth.authState.subscribe((data: MockUser) => {
          expect(data.isAnonymous).toBe(false);
        });
      });
    });

    describe('AuthService.currentUid', () => {
      it('should return "ZzVRkeduEW1bJC6pmcmb9VjyeERt"',
        inject([ AuthService ], (service: AuthService) => {
          expect(service.currentUid).toBe('ZzVRkeduEW1bJC6pmcmb9VjyeERt');
        }));
    });
  });
});

你可以看到我在哪里注释掉了间谍,而是不得不劫持 mockAngularFireAuthauthSate 属性 到让断言成功,通过强行改变它的价值——这是我不应该做的,因为 mockAngularFireAuth 是一个常数。

为了完整性,这里是(部分)被测服务:

import { Injectable } from '@angular/core';

import { AngularFireAuth } from 'angularfire2/auth';
import * as firebase from 'firebase/app';
import 'rxjs/add/observable/of';
// import 'rxjs/add/operator/catch';
import { Observable } from 'rxjs/Rx';

@Injectable()
export class AuthService {
  private authState: firebase.User;

  constructor(private afAuth: AngularFireAuth) { this.init(); }

  private init (): void {
    this.afAuth.authState.subscribe((authState: firebase.User) => {
      if (authState === null) {
        this.afAuth.auth.signInAnonymously()
          .then((authState: firebase.User) => {
            this.authState = authState;
          })
          .catch((error: Error) => {
            console.error(error);
          });
      } else {
        this.authState = authState;
      }
    }, (error: Error) => {
      console.error(error);
    });
  }

  public get currentUid(): string {
    return this.authState ? this.authState.uid : undefined;
  }
}

是否因为在断言失败的规范中我没有订阅 authState 因此间谍没有 return 我设置的相应值?

更新:

我认为这可能是因为 Jasmine 无法监视不是函数的属性(据我所知),或者 getters/setters。

但是为什么间谍会在

AuthService
  catastrophically fails

通过?

根据我的更新;你不能监视属性——只能监视函数(或方法)和 属性 getter 和 setter。

相反,我向 mockAngularFireAuth 添加了一个 setAuthState 方法,如果 authState 属性.

可以更改值

它基本上和我做的一模一样,但没有违反 TypeScript 中常量的规则。由于它是一项模拟服务,因此我认为存在此附加方法并不重要。

但是,我不完全确定为什么成功的规范如此。我认为这可能是因为方法 throw 本身就是一个函数;因此它可以成为 Jasmine 间谍的 return 值。

以下是我更改测试的方式:

// Mock AngularFireAuth
const mockAngularFireAuth: any = {
  auth: jasmine.createSpyObj('auth', {
    'signInAnonymously': Promise.resolve(authState)
  }),
  authState: Observable.of(authState),
  setAuthState: (authState: MockUser): void => {
    mockAngularFireAuth.authState = Observable.of(authState);
  }
};

注意 setAuthState

这就是我更改规格的方式(代表性示例):

describe('AuthService.currentUid', () => {
  beforeEach(() => {
    mockAngularFireAuth.setAuthState(null);
  });

  it('should return undefined',
    inject([ AuthService ], (service: AuthService) => {
      expect(service.currentUid).toBeUndefined();
    }));
});