Angular 2: как имитировать ChangeDetectorRef во время модульного тестирования

Я только начал с Unit-Testing, и мне удалось смоделировать свои собственные сервисы, а также некоторые из Angular и Ionic, но независимо от того, что я делаю, ChangeDetectorRef остается прежним.

Я имею в виду, что это за колдовство?

beforeEach(async(() => 
    TestBed.configureTestingModule({
      declarations: [MyComponent],
      providers: [
        Form, DomController, ToastController, AlertController,
        PopoverController,

        {provide: Platform, useClass: PlatformMock},
        {
          provide: NavParams,
          useValue: new NavParams({data: new PageData().Data})
        },
        {provide: ChangeDetectorRef, useClass: ChangeDetectorRefMock}

      ],
      imports: [
        FormsModule,
        ReactiveFormsModule,
        IonicModule
      ],
    })
    .overrideComponent(MyComponent, {
      set: {
        providers: [
          {provide: ChangeDetectorRef, useClass: ChangeDetectorRefMock},
        ],
        viewProviders: [
          {provide: ChangeDetectorRef, useClass: ChangeDetectorRefMock},
        ]
      }
    })
    .compileComponents()
    .then(() => {
      let fixture = TestBed.createComponent(MyComponent);
      let cmp = fixture.debugElement.componentInstance;

      let cdRef = fixture.debugElement.injector.get(ChangeDetectorRef);

      console.log(cdRef); // logs ChangeDetectorRefMock
      console.log(cmp.cdRef); // logs ChangeDetectorRef , why ??
    })
  ));

 it('fails no matter what', async(() => {
    spyOn(cdRef, 'markForCheck');
    spyOn(cmp.cdRef, 'markForCheck');

    cmp.ngOnInit();

    expect(cdRef.markForCheck).toHaveBeenCalled();  // fail, why ??
    expect(cmp.cdRef.markForCheck).toHaveBeenCalled(); // success

    console.log(cdRef); // logs ChangeDetectorRefMock
    console.log(cmp.cdRef); // logs ChangeDetectorRef , why ??
  }));

@Component({
  ...
})
export class MyComponent {
 constructor(private cdRef: ChangeDetectorRef){}

 ngOnInit() {
   // do something
   this.cdRef.markForCheck();
 }
}

Я пробовал все, async, fakeAsync, injector([ChangeDetectorRef], () => {}).

Ничего не работает.


person Ankit Singh    schedule 02.01.2017    source источник
comment
Компилятор Angular 2 специально обрабатывает ChangeDetectorRef. Я думаю, что вы не можете предоставить его. Вы можете проверить тест для AsyncPipe github .com/angular/angular/blob/ Используется SpyChangeDetectorRef   -  person yurzui    schedule 02.01.2017
comment
Я столкнулся с той же проблемой - как люди работают с этим?   -  person SamF    schedule 16.01.2017


Ответы (4)


Обновление 2020:

Я написал это в мае 2017 года, это решение отлично работало в то время и работает до сих пор.

Мы не можем настроить внедрение макета changeDetectorRef через испытательный стенд, поэтому вот что я делаю в эти дни:

 it('detects changes', () => {
      // This is a unique instance here, brand new
      const changeDetectorRef = fixture.debugElement.injector.get(ChangeDetectorRef); 
     
      // So, I am spying directly on the prototype.
      const detectChangesSpy = spyOn(changeDetectorRef.constructor.prototype, 'detectChanges');

      component.someMethod(); // Which internally calls the detectChanges.

      expect(detectChangesSpy).toHaveBeenCalled();
    });

Тогда вам не нужны частные атрибуты или что-то еще.


В случае, если кто-то столкнется с этим, это один из способов, который хорошо сработал для меня:

Когда вы внедряете экземпляр ChangeDetectorRef в свой конструктор:

 constructor(private cdRef: ChangeDetectorRef) { }

У вас есть этот cdRef как один из частных атрибутов компонента, что означает, что вы можете шпионить за компонентом, заглушить этот атрибут и заставить его возвращать все, что вы хотите. Кроме того, вы можете утверждать его вызовы и параметры по мере необходимости.

В вашем файле спецификаций вызовите свой TestBed, не предоставляя ChangeDetectorRef, поскольку он не предоставит то, что вы ему даете. Установите тот же компонент перед каждым блоком, чтобы он сбрасывался между спецификациями, как это делается в документах здесь:

component = fixture.componentInstance;

Затем в тестах подсмотрите непосредственно атрибут

describe('someMethod()', () => {
  it('calls detect changes', () => {
    const spy = spyOn((component as any).cdRef, 'detectChanges');
    component.someMethod();

    expect(spy).toHaveBeenCalled();
  });
});

Со шпионом вы можете использовать .and.returnValue() и получить все, что вам нужно.

Обратите внимание, что (component as any) используется, поскольку cdRef является закрытым атрибутом. Но private не существует в самом скомпилированном javascript, поэтому он доступен.

Вам решать, хотите ли вы получить доступ к закрытым атрибутам во время выполнения для своих тестов.

person Juan    schedule 30.05.2017
comment
так почему вы создаете приватные поля, если не относитесь к ним как к приватным? смешной - person Stwosch; 04.10.2018
comment
Исходный вопрос объявлен как частный. Я только что ответил;) Существует старая дискуссия о том, должен ли я тестировать частные методы с двух точек зрения, да и нет. Оба по уважительной причине. Мой совет: выбирайте тот, который делает вас счастливым. - person Juan; 04.10.2018
comment
@Juan Почему ChangeDetectorRef нельзя указать в TestBed и зачем вместо этого обращаться к частному полю? - person Hary; 25.05.2020
comment
@mbharanidharan88 это был май 2017 года, у меня была проблема с этим в то время. Я только что добавил к своему ответу рекомендацию испытательного стенда, как показано в других ответах, чтобы не заботиться о внутренних/личных деталях. - person Juan; 26.05.2020
comment
Чтобы избавиться от проблем с линтингом, const spy = spyOn(component['cdRef'], 'detectChanges'); - person Hary; 26.05.2020
comment
@Juan, возможно ли ввести ChangeDetectorRef и проверить, вызывается ли он, а не шпионить за компонентом.ChangeDetectorRef - person Hary; 27.05.2020
comment
Да! Я не хотел использовать это таким образом. Вместо этого вы используете const cdRefMock = jasmine.createSpyObj('ChangeDetectorRef', ['detectChanges']); а затем предоставьте его на тестовом стенде с помощью useValue. Затем вы внедряете шпиона. Не шпионьте за компонентом! Уточню в редакции. - person Juan; 27.05.2020
comment
Я обновил его. Там есть хорошие ответы, показывающие последнюю часть с использованием макета, так что это настолько подробное и подробное обновление, которое будет получено на данный момент, поскольку это старый вопрос и ответ, и там есть хорошая информация. Я надеюсь, теперь это укажет вам правильное направление @mbharanidharan88 - person Juan; 27.05.2020

Не уверен, что это новая вещь или нет, но к changeDetectorRef можно получить доступ через фикстуру.

См. документы: https://angular.io/guide/testing#componentfixture-properties

Мы столкнулись с той же проблемой с насмешкой детектора изменений, и это оказалось решением

person FDIM    schedule 21.06.2018
comment
Это путь. Это намного лучше, чем шпионить за свойствами компонента, особенно приватными. Спасибо ! - person ethanfar; 06.03.2020
comment
fixture.changeDetectorRef не совпадает с предоставленным компонентом, поэтому вы не можете его использовать. - person Alex Parloti; 02.06.2020

Для модульного тестирования, если вы имитируете ChangeDetectorRef только для внедрения зависимостей для создаваемого компонента, вы можете передать любое значение.

Для моего случая я сделал так:

TestBed.configureTestingModule({
  providers: [
    FormBuilder,
    MyComponent,
    { provide: ChangeDetectorRef, useValue: {} }
  ]
}).compileComponents()
injector = getTestBed()
myComponent = injector.get(MyComponent)

Он успешно создаст myComponent. Просто убедитесь, что путь выполнения теста не нуждается в ChangeDetectorRef. Если вы это сделаете, замените useValue: {} подходящим фиктивным объектом.

В моем случае мне просто нужно было протестировать создание некоторых форм с помощью FormBuilder.

person kctang    schedule 20.06.2018
comment
ChangeDetectorRef не предоставляется через DI, поэтому вы не можете предоставить дубликат. - person Alex Parloti; 12.06.2020

Вероятно, следует отметить один момент: по сути, здесь вы хотите протестировать свой собственный код, а не модульное тестирование самого детектора изменений (который был протестирован командой Angular). На мой взгляд, это хороший показатель того, что вы должны извлечь вызов детектора изменений в локальный частный метод (частный, поскольку это то, что вы не хотите тестировать), например.

private detectChanges(): void {
    this.cdRef.detectChanges();
}

Затем в своем модульном тесте вы захотите убедиться, что ваш код действительно вызвал эту функцию и, таким образом, вызвал метод из ChangeDetectorRef. Например:

it('should call the change detector',
    () => {
        const spyCDR = spyOn((cmp as any).cdRef, 'detectChanges' as any);
        cmp.ngOnInit();
        expect(spyCDR).toHaveBeenCalled();
    }
);

У меня была точно такая же ситуация, и это было предложено мне как общая передовая практика для модульного тестирования от старшего разработчика, который сказал мне, что модульное тестирование на самом деле вынуждает вас с помощью этого шаблона лучше структурировать ваш код. С предлагаемой реструктуризацией вы убедитесь, что ваш код гибок для изменения, например. если Angular изменит способ, которым они предоставляют нам обнаружение изменений, вам нужно будет только адаптировать метод detectChanges.

person ArthurT    schedule 13.03.2018
comment
Можно ли ввести ChangeDetectorRef и проверить toHaveBeenCalled вместо того, чтобы шпионить за Component.ChangeDetectorRef ? - person Hary; 27.05.2020