ウェブサイト検索

javascript - Angularでボタンクリックのテストをどうカバーするか?(Stackblitz添付)


私は、1つの入力ボックスとボタンがある非常にシンプルなアプリケーションを作っています。

  1. 入力はメールを入力するためのものです

  2. イベントハンドラー付きのボタンを購読

メールを入力してボタンをクリックすると、API呼び出しが行われます(この方法は動作します)

  subscribeEmail() {
    this.error = '';


        if (this.userForm.controls.email.status === 'VALID') {

      const params = new HttpParams()
                .set('EMAIL', this.userForm.controls.email.value)
        .set('subscribe','Subscribe')
        .set('b_aaa7182511d7bd278fb9d510d_01681f1b55','')
      console.log(params);
            const mailChimpUrl = this.mailChimpEndpoint + params.toString();

            this.http.jsonp<MailChimpResponse>(mailChimpUrl, 'c').subscribe(response => {
        console.log('response ', response)
                if (response.result && response.result !== 'error') {
                    this.submitted = true;
                }
                else {
                    this.error = response.msg;
                }
            }, error => {
                console.error(error);
                this.error = 'Sorry, an error occurred.';
            });
        }
  }

ここに完全な動作例があります

それに問題はなく、すべてが正常に動作するかどうかも確認できます。

要件: このボタンのクリックとパラメータ付きのHTTP呼び出しのテストケースをカバーする必要があります。

テストケースを書くことができず、パラメータ付きのHTTPコールのテストがカバーされていないと表示されています。

これまでに私が書いたテストは、ボタンクリックのシナリオを達成するためのものです、例えば、

describe('HelloComponent', () => {

  let component: HelloComponent;
  let fixture: ComponentFixture<HelloComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
       imports: [
        HttpClientTestingModule,
        ReactiveFormsModule
      ],
      declarations: [HelloComponent]
    }).compileComponents();
  }));

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

  it('check subscribeEmail function has value', (done: DoneFn) => {

    const subscribeButton = fixture.debugElement.query(By.css('.subscribe'));
    subscribeButton.triggerEventHandler('click', {});

    done();
  });
});

そして、こちらでテストケース付きの動作中のstackblitz

(hello.component.spec.ts を確認してください)

ボタンのクリックについては、対応済みで動作します

    const subscribeButton = fixture.debugElement.query(By.css('.subscribe'));
    subscribeButton.triggerEventHandler('click', {});

ボタンのクリックでパラメータ付きのHTTP呼び出しがあるテストケースをカバーする結果を達成するのを手伝っていただけますか?

更新: いくつか調べた結果、パラメータ付きでURLを呼び出すためにhttpmockを使えることがわかりましたが、次のようなエラーが発生しています、

エラー: 条件「URLに一致: https://gmail.us10.list-manage.com/subscribe/post-json?u=aaa7182511d7bd278fb9d510d&id=01681f1b55&amp」に一致するリクエストが1件必要ですが、見つかりませんでした。

あなたはまた、httpMock付きの変更されたstackblitzをこちらで見ることができます...

結局のところ、この投稿を作っているのは私だけなので、この質問を考慮して正しい解決策を提供してください。

あらかじめ大きな感謝を。

回答 #1

それは非常に長い質問でしたね。あなたがそれを作成するのに多くの時間をかけたことは理解しています。ですので、私もコードをベストプラクティスに沿ってリファクタリングするために時間をかけました。これには、コードをサービスとして分割し、ユニットテストがしやすいようにコードを改善する必要があります。(コンポーネントおよびサービスのユニットテスト用)

  1. MyService のようなサービスを作成し、http のコードをそこに移動します。これにより、component と service を別々にユニットテストするのに役立ちます。以下のように作成しました:
export class MyService {
  mailChimpEndpoint = 'https://gmail.us10.list-manage.com/subscribe/post-json?u=aaa7182511d7bd278fb9d510d&amp;id=01681f1b55&amp';
  constructor(private _http: HttpClient) {}

  submitForm(email: string){
    const params = new HttpParams()
                .set('EMAIL', email)
        .set('subscribe','Subscribe')
        .set('b_aaa7182511d7bd278fb9d510d_01681f1b55','')
    const mailChimpUrl = this.mailChimpEndpoint + params.toString();

    return this._http.jsonp<MailChimpResponse>(mailChimpUrl, 'c')
  }
}
  1. ここにある Stackblitz でのコンポーネントのユニットテストを作成してください。リファクタリングされたコードと、シナリオに応じてすべてのケースをどのようにカバーしているかをよく見てください。前回の質問で既に説明したように、MatSnackBar の呼び出しをテストすることもできます。

  2. 同様に、サービスをテストするためのユニットテストは、私の記事のひとつを参考にして書くことができます

回答 #2

確かに、コードをリファクタリングしてテストしやすくする方が良いでしょう。しかし、提供された例でもテストでカバーすることは可能です。渡されたパラメータを検証し、パラメータが正しければ予測可能な応答をオブザーバブルとして返すというアイデアです。

つまり、this.http.jsonp は、渡されたパラメータが正しい場合にのみ期待された結果を返します。

import { ComponentFixture, TestBed, async, fakeAsync, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement, Injector }  from '@angular/core';

import { HelloComponent } from './hello.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { moqInjectorProviders, resolveMock } from 'ng-auto-moq';
import { Mock, It } from 'moq.ts';
import { HttpClient, HttpParams } from '@angular/common/http';
import { cold, getTestScheduler } from 'jasmine-marbles';


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

  beforeEach(async(() => {
    TestBed.configureTestingModule({
       imports: [
        HttpClientTestingModule,
        ReactiveFormsModule
      ],
      declarations: [HelloComponent],
      // setups automatically all dependecies of the component as mock objects
      providers: moqInjectorProviders(HelloComponent)
    }).compileComponents();
  }));

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

  it('check subscribeEmail function has value', fakeAsync(() => {
    const injector = TestBed.get(Injector);

    // sets mock behaviour
    resolveMock(HttpClient, injector)
      // if jsonp is invoked with first parameter with email substring
      .setup(instance => instance.jsonp(It.Is<string>(url => url.indexOf('[email ') > 1), 'c'))
      // only in this case return a cold observable
      .returns(cold("a", {a: {result: "success"}}));

    const email = fixture.debugElement.query(By.css('.email-input')).nativeElement;
    // sets email value
    email.value = '[email ';
    // notifies angualr that element has changed
    email.dispatchEvent(new Event('input'));

    const subscribeButton = fixture.debugElement.query(By.css('.subscribe'));
    subscribeButton.triggerEventHandler('click', {});

    // flush the observable of jsonp
    getTestScheduler().flush();
    fixture.detectChanges();

    expect(component.submitted).toBe(true);
  }));

  it('sets error when server returns an error', fakeAsync(() => {
    const injector = TestBed.get(Injector);
    const msg = "erorr message";

    resolveMock(HttpClient, injector)
      .setup(instance => instance.jsonp(It.Is<string>(url => url.indexOf('[email ') > 1), 'c'))
      .returns(cold("a", {a: {result: "error", msg}}));

    const email = fixture.debugElement.query(By.css('.email-input')).nativeElement;
    email.value = '[email ';
    email.dispatchEvent(new Event('input'));

    const subscribeButton = fixture.debugElement.query(By.css('.subscribe'));
    subscribeButton.triggerEventHandler('click', {});

    getTestScheduler().flush();
    fixture.detectChanges();

    expect(component.error).toEqual(msg);
  }));
});

ここで動作する例を見つけることができます

どうぞ、Observableを偽装するためにjasmine-marblesのような追加のライブラリや、HttpClientをモックするためにmoq.tsのようなものを使ったことに注意してください