Что я узнал о насмешках с помощью тестов Spectator

Я упомянул в своем предыдущем сообщении в блоге, что Spectator в настоящее время является моим основным инструментом для тестирования моих приложений Angular. За последние пару недель я узнал кое-что о насмешках с помощью Spectator, и надеюсь, вы найдете их полезными.

Обратите внимание, что я использую Spectator с Jest.

Удалить неиспользуемых провайдеров

В моем createServiceFactory или createComponentFactory я объявил, что мои зависимости будут автоматически имитироваться, но я все еще сохранил провайдеров с useValue: {}. В приведенном ниже примере мой UserApiService уже автоматически имитируется. В этом случае уже не нужно объявлять UserApiService в провайдерах с useValue: {}:

let spectator: SpectatorService<UserService>;
  const createService = createServiceFactory({
    service: UserService,
    providers: [
      { provide: UserApiService, useValue: {}}, // TODO: Remove, we don't need it.
    ],
    mocks: [UserApiService], // Automatically mock
  });

  beforeEach(() => (spectator = createService()));

Нам не нужны UserApiService в провайдерах:

let spectator: SpectatorService<UserService>;
  const createService = createServiceFactory({
    service: UserService,
    mocks: [UserApiService],
  });

  beforeEach(() => (spectator = createService()));

Хранение UserApiService в провайдерах не имеет побочных эффектов. Однако лучше удалить его, если он не используется.

andReturn() против mockReturnValue()

Я использовал andReturn() исключительно для имитации методов в тестовом примере с тех пор, как начал использовать Spectator. Я только недавно узнал о Jest’s mockReturnValue(). Он работает так же, как andReturn(). Основное отличие, которое я заметил, заключается в том, что andReturn() не является строгим с типом возвращаемого значения.

Учитывая метод, возвращающий тип Observable<User>.

class UserService {
  getUser(): Observable<User> {
    // code here..    
  }
}

Где User:

interface User {
  firstName: string;
  lastName: string;
}

Используя mockReturnValue, я получу ошибку, если не смоюсь, используя ожидаемый тип возвращаемого значения:

const userUservice = spectator.inject(UserService);
userService.getUser.mockReturnValue(of('user mock'));

Ошибка:

error TS2345: Argument of type 'Observable<string>' 
is not assignable to parameter of type 'Observable<User>'.

mockReturnValue() принимает только объявленный тип возвращаемого значения метода:

const userUservice = spectator.inject(UserService);
userService.getUser.mockReturnValue(of({firstName: 'First', lastName: 'Last'} as User));

Используя andReturn(), я могу использовать другой тип:

const userUservice = spectator.inject(UserService);
userService.getUser.andReturn(of('user mock')); // I can mock with a string type!

Насмешка по прямому назначению

Я использовал andReturn() (и вскоре mockReturnValue()) в тестовых примерах, если меня волнует возвращаемое значение макета. Мне легче заметить, когда я ищу моки, которые происходят в одном тесте.

Тем не менее, есть тестовые случаи, когда меня интересует только утверждение toHaveBeenCalled() для фиктивного метода. В этом случае я обычно присваивал бы jest.fn() методу, который я хочу утвердить. Это работает, только если свойство или метод не доступны только для чтения.

Если я использую UserService в своем компоненте.

constructor(private userService: UserService) {}

Я могу имитировать вызов его метода getUser() в тесте, напрямую назначив фиктивную функцию. Затем подтвердите с помощью toHaveBeenCalled():

spectator.component['userService'].getUser = jest.fn();
//.. some code here
expect(spectator.component['userService'].getUser).toHaveBeenCalled();

Имитация свойств только для чтения

Что, если метод, который я хочу имитировать, доступен только для чтения?

У меня есть геттер в моей службе:

export class UserService {
  //.. some code
  get canAccess$(): Observable<boolean> {
    // .. implementaion
  }
}

У меня есть компонент, который использует указанный выше геттер UserService.canAccess$. Когда я пытаюсь издеваться над этим геттером, используя andReturn():

const userService = spectator.inject(UserService);
userService.canAccess$.andReturnvalue(of(false));

Я получаю следующую ошибку:

TS2339: Property 'andReturn' does not exist on type 'Observable '.

Я также не могу назначить макет напрямую, потому что здесь canAccess$ доступен только для чтения:

spectator.component['userService'].canAccess$ = of(false);
Cannot assign to 'canAccess$' because it is a read-only property.

Есть несколько способов справиться с этим.

Использование Object.defineProperty()

Ранее я использовал Object.defineProperty для изменения свойства объекта службы:

Object.defineProperty(spectator.component['userService'], 'canAccess$', { value: of(true) });

Это сработало. Я подумал, что должен быть способ добиться этого, используя Spectator, а не напрямую изменяя объект службы, см. следующий раздел.

Установка useValue в тестовом примере

Я узнал об этом подходе, когда просматривал примеры в README Spectator.

Я могу объявить макет по умолчанию для canAccess$ в моем вызове createComponentFactory, установив useValue.

const createComponent = createComponentFactory({
  component: MyComponent,
  //...typeOrOptions here
  providers: [
    { provide: UserService, useValue: { canAccess$: of(true) } }
  ],
});

Все тесты в наборе будут использовать это значение по умолчанию canAccess$, если я не переопределю его в тесте или другом наборе тестов. Чтобы переопределить макет по умолчанию, я могу указать провайдера в тестовом примере с помощью useValue. В этом примере я изменяю возвращаемое значение на of(false).

const provider = {
  provide: UserService,
  useValue: { canAccess: of(false) },
};

Затем вызовите createComponent(), используя провайдера с фиктивным переопределением, которое я только что объявил.

spectator = createComponent({
  providers: [provider],
});

Я могу переопределить макет по умолчанию в отдельных тестовых примерах.

it('should prevent access...', () => {
  const provider = {
    provide: UserService,
    useValue: { canAccess$: of(false) },
  };
  spectator = createComponent({
    providers: [provider],
  });
  //.. code here
});

Или я могу объявить переопределение в beforeEach(), если я хочу использовать его в наборе тестов:

describe('Prevent access', () => {
  beforeEach(() => {
    const provider = {
      provide: UserService,
      useValue: { canAccess$: of(false) },
    };
    spectator = createComponent({
      providers: [provider],
    });
    //.. code here
  });
  it('should prevent access...', () => {
    // .. code here
  });
  // .. more tests
});

Если вам нравится эта история, вам могут понравиться и другие мои истории о Spectator Test и Angular: