Зачем мне дважды вызывать detectChanges / whenStable? - PullRequest
15 голосов
/ 16 марта 2019

Первый пример

У меня есть следующий тест:

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

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

@Component({
    template: '<ul><li *ngFor="let state of values | async">{{state}}</li></ul>'
})
export class TestComponent {
    values: Promise<string[]>;
}

describe('TestComponent', () => {
    let component: TestComponent;
    let fixture: ComponentFixture<TestComponent>;
    let element: HTMLElement;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [TestComponent]
        })
            .compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(TestComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
        element = (<HTMLElement>fixture.nativeElement);
    });

    it('this test fails', async() => {
        // execution
        component.values = Promise.resolve(['A', 'B']);
        fixture.detectChanges();
        await fixture.whenStable();

        // evaluation
        expect(Array.from(element.querySelectorAll('li')).map(elem => elem.textContent)).toEqual(['A', 'B']);
    });

    it('this test works', async() => {
        // execution
        component.values = Promise.resolve(['A', 'B']);
        fixture.detectChanges();
        await fixture.whenStable();
        fixture.detectChanges();
        await fixture.whenStable();

        // evaluation
        expect(Array.from(element.querySelectorAll('li')).map(elem => elem.textContent)).toEqual(['A', 'B']);
    });
});

Как вы можете видеть, есть супер простой компонент, который просто отображает список элементов, предоставленныхPromise.Есть два теста, один из которых не пройден, а другой пройденЕдинственное различие между этими тестами заключается в том, что тест, который дважды вызывает fixture.detectChanges(); await fixture.whenStable();.

ОБНОВЛЕНИЕ: Второй пример (обновлено снова в 2019/03/21)

В этом примере предпринимается попытка исследоватьвозможные отношения с ngZone:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { Component, NgZone } from '@angular/core';

@Component({
    template: '{{value}}'
})
export class TestComponent {
    valuePromise: Promise<ReadonlyArray<string>>;
    value: string = '-';

    set valueIndex(id: number) {
        this.valuePromise.then(x => x).then(x => x).then(states => {
            this.value = states[id];
            console.log(`value set ${this.value}. In angular zone? ${NgZone.isInAngularZone()}`);
        });
    }
}

describe('TestComponent', () => {
    let component: TestComponent;
    let fixture: ComponentFixture<TestComponent>;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            imports: [FormsModule],
            declarations: [TestComponent],
            providers: [
            ]
        })
            .compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(TestComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
    });

    function diagnoseState(msg) {
        console.log(`Content: ${(fixture.nativeElement as HTMLElement).textContent}, value: ${component.value}, isStable: ${fixture.isStable()} # ${msg}`);
    }

    it('using ngZone', async() => {
        // setup
        diagnoseState('Before test');
        fixture.ngZone.run(() => {
            component.valuePromise = Promise.resolve(['a', 'b']);

            // execution
            component.valueIndex = 1;
        });
        diagnoseState('After ngZone.run()');
        await fixture.whenStable();
        diagnoseState('After first whenStable()');
        fixture.detectChanges();
        diagnoseState('After first detectChanges()');
    });

    it('not using ngZone', async(async() => {
        // setup
        diagnoseState('Before setup');
        component.valuePromise = Promise.resolve(['a', 'b']);

        // execution
        component.valueIndex = 1;

        await fixture.whenStable();
        diagnoseState('After first whenStable()');
        fixture.detectChanges();
        diagnoseState('After first detectChanges()');

        await fixture.whenStable();
        diagnoseState('After second whenStable()');
        fixture.detectChanges();
        diagnoseState('After second detectChanges()');

        await fixture.whenStable();
        diagnoseState('After third whenStable()');
        fixture.detectChanges();
        diagnoseState('After third detectChanges()');
    }));
});

Этот первый из этих тестов (явно использующий ngZone) приводит к:

Content: -, value: -, isStable: true # Before test
Content: -, value: -, isStable: false # After ngZone.run()
value set b. In angular zone? true
Content: -, value: b, isStable: true # After first whenStable()
Content: b, value: b, isStable: true # After first detectChanges()

Журналы второго теста:

Content: -, value: -, isStable: true # Before setup
Content: -, value: -, isStable: true # After first whenStable()
Content: -, value: -, isStable: true # After first detectChanges()
Content: -, value: -, isStable: true # After second whenStable()
Content: -, value: -, isStable: true # After second detectChanges()
value set b. In angular zone? false
Content: -, value: b, isStable: true # After third whenStable()
Content: b, value: b, isStable: true # After third detectChanges()

Я как бы ожидал, что тест проходит в угловой зоне, но это не так.Кажется, проблема в том, что

Во избежание неожиданностей функции, передаваемые в then (), никогда не будут вызываться синхронно, даже с уже разрешенным обещанием.( Source )

Во втором примере я спровоцировал проблему, вызвав .then(x => x) несколько раз, что не более чем снова поместит прогресс в цикл событий браузера итаким образом задерживая результат.Насколько я понимаю, вызов await fixture.whenStable() должен в основном сказать «подождите, пока эта очередь не станет пустой».Как мы видим, это действительно работает, если я выполняю код в ngZone явно.Однако это не значение по умолчанию, и я нигде не могу найти в руководстве, что предполагается, что я пишу свои тесты таким образом, так что это неудобно.

Что на самом деле делает await fixture.whenStable() во втором тесте ?.Исходный код показывает, что в этом случае fixture.whenStable() будет просто return Promise.resolve(false);.Поэтому я попытался заменить await fixture.whenStable() на await Promise.resolve(), и это действительно имеет тот же эффект: это приводит к приостановке теста и его запуску из очереди событий, и, таким образом, обратный вызов, переданный в valuePromise.then(...), фактически выполняется, еслиЯ просто достаточно часто звоню await по любому обещанию.

Почему мне нужно звонить await fixture.whenStable(); несколько раз?Я использую это неправильно?Это намеренное поведение?Есть ли "официальная" документация о том, как она предназначена для работы / как с этим бороться?

Ответы [ 2 ]

10 голосов
/ 19 марта 2019

Я считаю, что вы испытываете Delayed change detection.

Обнаружение отложенных изменений является намеренным и полезным.Он дает тестеру возможность проверять и изменять состояние компонента до того, как Angular инициирует привязку данных и вызывает ловушки жизненного цикла.

detectChanges ()


Реализация Automatic Change Detection позволяет вам вызывать fixture.detectChanges() только один раз в обоих тестах.

 beforeEach(async(() => {
            TestBed.configureTestingModule({
                declarations: [TestComponent],
                providers:[{ provide: ComponentFixtureAutoDetect, useValue: true }] //<= SET AUTO HERE
            })
                .compileComponents();
        }));

Stackblitz

https://stackblitz.com/edit/directive-testing-fnjjqj?embed=1&file=app/app.component.spec.ts

Этот комментарий в примере Automatic Change Detection важен, и поэтому ваши тесты все еще должны вызывать fixture.detectChanges(), даже с AutoDetect.

Второй и третий тест выявляют важное ограничение.Среда тестирования Angular не знает, что тест изменил название компонента.Служба ComponentFixtureAutoDetect реагирует на асинхронные действия, такие как разрешение обещаний, таймеры и события DOM.Но прямое синхронное обновление свойства компонента невидимо.Тест должен вызвать fixture.detectChanges () вручную, чтобы запустить другой цикл обнаружения изменений.

Из-за того, как вы решаете Обещание во время его настройки, я подозреваю, что оно рассматривается каксинхронное обновление, и Auto Detection Service не будет на него реагировать.

component.values = Promise.resolve(['A', 'B']);

Автоматическое обнаружение изменений


Проверка различных приведенных примеров дает представление о том, какпочему вам нужно звонить fixture.detectChanges() дважды без AutoDetect.Первый запуск ngOnInit в модели Delayed change detection ... вызов его во второй раз обновляет представление.

Это можно увидеть по комментариям справа от fixture.detectChanges() в приведенном ниже примере кода

it('should show quote after getQuote (fakeAsync)', fakeAsync(() => {
  fixture.detectChanges(); // ngOnInit()
  expect(quoteEl.textContent).toBe('...', 'should show placeholder');

  tick(); // flush the observable to get the quote
  fixture.detectChanges(); // update view

  expect(quoteEl.textContent).toBe(testQuote, 'should show quote');
  expect(errorMessage()).toBeNull('should not show error');
}));

Дополнительные асинхронные тесты Пример


В заключение: Если не использовать Automatic change detection, вызов fixture.detectChanges() будет "шагать" по модели Delayed Change Detection ... что даст вам возможность проверить иизмените состояние компонента до того, как Angular инициирует привязку данных и вызывает ловушки жизненного цикла.

Также обратите внимание на следующий комментарий по предоставленным ссылкам:

Вместо того, чтобы удивляться, когда тестовое устройство будетили не будет выполнять обнаружение изменений, примеры в этом руководстве всегда вызывают функцию receiveChanges () явно.Нет никакого вреда в вызове detectChanges () чаще, чем это строго необходимо.


Второй пример Stackblitz

Второй пример stackblitz, показывающий, что комментированиестрока 53 detectChanges() приводит к тому же выводу console.log.Вызов detectChanges() дважды до whenStable() не требуется.Вы звоните detectChanges() три раза, но второй звонок до whenStable() не оказывает никакого влияния.В вашем новом примере вы действительно получаете что-то только от двух из detectChanges().

Вызывать функцию receiveChanges () чаще, чем строго необходимо, не вредно.

https://stackblitz.com/edit/directive-testing-cwyzrq?embed=1&file=app/app.component.spec.ts


ОБНОВЛЕНИЕ: Второй пример (обновлено снова в 2019/03/21)

Предоставление стекаблиц для демонстрации отличного вывода из следующихварианты для вашего обзора.

  • await fixture.whenStable ();
  • fixture.whenStable (). Then (() => {})
  • ожидают fixture.whenStable (). Then (() => {})

Stackblitz

https://stackblitz.com/edit/directive-testing-b3p5kg?embed=1&file=app/app.component.spec.ts

0 голосов
/ 18 марта 2019

На мой взгляд, второй тест кажется неправильным, его следует записать по следующей схеме:

component.values = Promise.resolve(['A', 'B']);
fixture.whenStable().then(() => {
  fixture.detectChanges();       
  expect(Array.from(element.querySelectorAll('li')).map(elem => elem.textContent)).toEqual(['A', 'B']);
});

Пожалуйста, см .: При стабильном использовании

Вам следует позвонитьdetectChanges в whenStable() как

fixture.whenStable () возвращает обещание, которое разрешается, когда очередь задач движка JavaScript становится пустой.

...