Тестирование реактивных форм Angular с использованием RxJS Marbles

Угловой компонент

public setupObservables() {
  this.formFieldChanged$ = this.formField
    .valueChanges
    .pipe(
        debounceTime(100),
        distinctUntilChanged((a, b) => a === b),
    )
}

Жасмин Тест

import { of } from 'rxjs';
import { marbles } from 'rxjs-marbles/jasmine';  
...

it('should update value on debounced formField change', marbles(m => {
  const values = { a: "1", b: "2", c: "3" };

  const fakeInputs = m.cold('a 200ms b 50ms c', values);
  const expected = m.cold('100ms a 250ms c', values);

  // works on stackblitz but otherwise gives TS2540 compiler error
  // cannot assign to a read-only property
  component.formField.valueChanges = fakeInputs; 
  component.setupObservables();

  m.expect(component.formFieldChanged$).toBeObservable(expected);
}));

stackblitz.com пример

Намерение состоит в том, чтобы использовать мраморные тесты для тестирования Observable кода в контексте с реактивными формами Angular.

  • Имеет ли смысл такой подход?
  • Как лучше всего высмеять valueChanges объекта FormField?
  • Есть ли лучший способ структурировать такие тесты?

person Vedran    schedule 17.04.2020    source источник
comment
Можете ли вы сделать демонстрацию на stackblitz? В общем, valueChanges - это субъект, излучающий значения. Сравнивая его с fakeInputs, вы не сравниваете отдельные выбросы. Вместо этого вам понадобится expectObservable github. com / ReactiveX / rxjs / blob / master / docs_app / content / guide /   -  person martin    schedule 17.04.2020
comment
добавлена ​​демонстрация stackblitz   -  person Vedran    schedule 21.04.2020
comment
component.formField.valueChanges = fakeInputs вместо spyOn работает.   -  person Vedran    schedule 21.04.2020
comment
да, значит, вы в основном решили это сами. Вы можете сделать это еще проще, используя всего const expected = '100ms a 500ms c';, а затем передавая values в toBeObservable.   -  person martin    schedule 21.04.2020
comment
Работает на stackblitz, но TS не скомпилирует его, поскольку valueChanges является свойством только для чтения.   -  person Vedran    schedule 24.04.2020
comment
Вы можете привести его к типу, а затем переопределить (component.formField as any).valueChanges = fakeInputs.   -  person martin    schedule 24.04.2020


Ответы (2)


вопрос в том - что вы хотите протестировать. это модульный тест или тест e2e? если это модульный тест - имитируйте реактивные формы, охватывайте только вашу логику, тогда у вас нет проблем с valueChanges, потому что он имитируется, и вы контролируете его.

если это тест e2e - не нужно переназначать valueChanges. Ничего не надо имитировать / заменять, потому что это тест e2e.

Тем не менее, если вы хотите изменить valueChanges - используйте https://github.com/krzkaczor/ts-essentials#writable

(Writable<typeof component.formField>component.formField).valueChanges = fakeInputs; 

Это сделает тип свойства доступным для записи.

Если это модульный тест, лично я бы проголосовал за имитацию реактивной формы, потому что в модульном тесте нам нужно тестировать только наш модуль, его зависимости должны быть имитированы / заглушены.

Внедрение деталей, которые мы хотим смоделировать

Как вариант, вы можете переместить форму как зависимость вашего компонента к поставщикам в объявлениях компонентов.

@Component({
    selector: 'app-component',
    templateUrl: './app-component.html',
    styleUrls: ['./app-component.scss'],
    providers: [
        {
            provide: 'form',
            useFactory: () => new FormControl(),
        },
    ],
})
export class AppComponent {
    public formFieldChanged$: Observable<unknown>;

    constructor(@Inject('form') public readonly formField: FormControl) {
    }

    public setupObservables(): void {
        this.formFieldChanged$ = this.formField
            .valueChanges
            .pipe(
                debounceTime(100),
                distinctUntilChanged((a, b) => a === b),
            );
    }
}

Затем вы можете просто ввести макет вместо него в тест.

it('should update value on debounced formField change', marbles(m => {
    const values = { a: "1", b: "2", c: "3" };

    const fakeInputs = m.cold('a 200ms b 50ms c', values);
    const expected = m.cold('100ms a 250ms c', values);

    const formInput = {
        valueChanges: fakeInputs,
    };

    const component = new AppComponent(formInput as any as FormControl);

    component.setupObservables();
    m.expect(component.formFieldChanged$).toBeObservable(expected);
}));
person satanTime    schedule 25.04.2020
comment
Мой вопрос был в основном о модульном тестировании. Чтобы имитировать реактивные формы, я должен использовать Writable или есть более простой способ сделать это? - person Vedran; 27.04.2020
comment
да, для модульного тестирования вы можете просто пометить его как доступный для записи. Если хотите, самым правильным способом было бы ввести this.formField как зависимость от компонента, тогда вы могли бы смоделировать его без Writable. Если хотите, я могу обновить свой ответ, чтобы показать пример. - person satanTime; 27.04.2020
comment
Пожалуйста, сделайте это, это было бы здорово для тех, кто ищет полное решение. - person Vedran; 27.04.2020
comment
обновил, к сожалению, я не знаю имя вашего компонента и вместо этого использовал AppComponent. - person satanTime; 27.04.2020
comment
Невозможно использовать лямбды в декораторах - new FormControl() должен быть в экспортированной функции, но в остальном это выглядит как надежный подход. - person Vedran; 27.04.2020
comment
Я думаю, что он не поддерживается в очень-очень старых TS, мы используем его в 3.7 и 3.8 без каких-либо проблем. - person satanTime; 27.04.2020

Использование свойств вместо полей - гораздо более чистое решение.

get formFieldChanged$() { return this._formFieldChanged$; }
private _formFieldChanged$: Observable<string>;

...
   
public setupObservables() {
  this._formFieldChanged$ = this.formField
    .valueChanges
    .pipe(
        debounceTime(100),
        distinctUntilChanged((a, b) => a === b),
    )
}

spyOnProperty здесь творит чудеса, setupObservables() больше не нужен:

it('should update value on debounced formField change', marbles(m => {
  const values = { a: "1", b: "2", c: "3" };

  const fakeInputs = m.cold('a 200ms b 50ms c', values);
  const expected = m.cold('100ms a 250ms c', values);

  spyOnProperty(component, 'formFieldChanged$').and.returnValue(fakeInputs);

  m.expect(component.formFieldChanged$).toBeObservable(expected);
}));

person Vedran    schedule 24.06.2021