私たちの目標

このチュートリアルでは、架空の会社の従業員ディレクトリを作成してテストします。 このディレクトリには、すべてのユーザーを表示するビューと、個々のユーザーのプロファイルページとして機能する別のビューがあります。 チュートリアルのこの部分では、これらのユーザーに使用されるサービスとそのテストの構築に焦点を当てます。

次のチュートリアルでは、 Pokeapi を使用して、ユーザーのお気に入りのポケモンの画像をユーザープロファイルページに入力し、HTTPリクエストを行うサービスをテストする方法を学習します。

知っておくべきこと

このチュートリアルの主な焦点はテストであるため、TypeScriptおよびAngularアプリケーションでの作業に慣れていることを前提としています。 この結果、私はサービスとは何か、そしてそれがどのように使用されるかを説明するために時間を割くことはありません。 代わりに、テストを実行するときにコードを提供します。

なぜテストするのですか?

個人的な経験から、テストはソフトウェアの欠陥を防ぐための最良の方法です。 私は過去に多くのチームに参加していて、小さなコードが更新され、開発者が手動でブラウザまたはPostmanを開いて、それがまだ機能することを確認しています。 このアプローチ(手動QA)は災害を懇願しています。

テストは、ソフトウェアの欠陥を防ぐための最良の方法です。

機能とコードベースが大きくなるにつれて、手動QAはより高価で、時間がかかり、エラーが発生しやすくなります。 機能または機能が削除された場合、すべての開発者はその潜在的な副作用をすべて覚えていますか? すべての開発者が同じ方法で手動でテストしていますか? おそらくそうではありません。

コードをテストする理由は、コードが期待どおりに動作することを確認するためです。 このプロセスの結果として、自分自身や他の開発者向けのより優れた機能ドキュメントと、APIの設計支援が得られることがわかります。

なぜカルマ?

Karmaは、既存のツールを使用して独自のフレームワーク機能をテストするのに苦労しているAngularJSチームの直接の製品です。 この結果、彼らはKarmaを作成し、AngularCLIで作成されたアプリケーションのデフォルトのテストランナーとしてAngularに移行しました。

Angularでうまく遊ぶことに加えて、ワークフローに合わせてKarmaを調整するための柔軟性も提供します。 これには、電話、タブレット、さらにはYouTubeチームのようなPS3などのさまざまなブラウザやデバイスでコードをテストするオプションが含まれます。

Karmaは、JasmineをMochaQUnitなどの他のテストフレームワークに置き換えるオプションや、 Jenkins TravisCI[などのさまざまな継続的インテグレーションサービスと統合するオプションも提供します。 X216X]、またはCircleCI

追加の構成を追加しない限り、Karmaとの通常の対話は、ターミナルウィンドウでng testを実行することです。

なぜジャスミン?

Jasmineは、Karmaで非常にうまく機能するJavaScriptコードをテストするためのビヘイビア駆動開発フレームワークです。 Karmaと同様に、Angular CLIを使用してセットアップされるため、Angularドキュメント内で推奨されるテストフレームワークでもあります。 Jasmineも依存関係がなく、DOMを必要としません。

機能に関する限り、Jasmineにはテストに必要なほとんどすべてのものが組み込まれているのが大好きです。 最も顕著な例はスパイです。 スパイを使用すると、関数を「スパイ」して、呼び出されたかどうか、呼び出された回数、呼び出された引数など、関数に関する属性を追跡できます。 Mochaのようなフレームワークでは、スパイは組み込まれておらず、Sinon.jsのような別のライブラリとペアリングする必要があります。

幸いなことに、テストフレームワーク間の切り替えコストは比較的低く、構文の違いはJasmineのtoEqual()とMochaのto.equal()と同じくらい小さいです。

簡単なテスト例

どこへ行ってもあなたをフォローしているAdderという名前のエイリアンの使用人がいたと想像してみてください。 かわいいエイリアンの仲間である以外に、加算器は実際には1つのことしかできず、2つの数字を足し合わせます。

Adderが2つの数値を加算できることを確認するために、一連のテストケースを生成して、Adderが正しい答えを提供しているかどうかを確認できます。

Jasmine内では、これは「スイート」と呼ばれるものから始まります。これは、関数describeを呼び出すことによって関連する一連のテストをグループ化します。

// A Jasmine suite
describe('Adder', () => {

});

ここから、Adderに2つの正の数(2、4)、正の数とゼロ(3、0)、正の数と負の数(5、-2)、およびすぐ。

Jasmine内では、これらは「specs」と呼ばれ、関数itを呼び出して、テストされている機能を説明する文字列を渡すことで作成します。

describe('Adder', () => {
  // A jasmine spec
  it('should be able to add two whole numbers', () => {
    expect(Adder.add(2, 2)).toEqual(4);
  });

  it('should be able to add a whole number and a negative number', () => {
    expect(Adder.add(2, -1)).toEqual(1);
  });

  it('should be able to add a whole number and a zero', () => {
    expect(Adder.add(2, 0)).toEqual(2);
  });
});

各仕様内でexpectと呼び、「actual」と呼ばれるもの、つまりactualコードの呼び出しサイトを提供します。 期待値、つまりexpectの後には、テスト開発者がテスト対象のコードの期待値出力を提供するtoEqualなどの連鎖「マッチャー」関数があります。

toEqual以外にも、他にも多くのマッチャーを利用できます。 完全なリストは、Jasmineのドキュメントにあります。

私たちのテストは、どのようにAdderが答えに到達するかには関係していません。 Adderが提供する答えだけを気にします。 私たちが知っている限りでは、これはAdderによるaddの実装である可能性があります。

function add(first, second) {
  if (true) { // why?
    if (true) { // why??
      if (1 === 1) { // why?!?1
        return first + second;
      }
    }
  }
}

つまり、Adderが期待どおりに動作することだけを気にします。つまり、Adderの実装については心配していません。

これが、テスト駆動開発(TDD)のようなプラクティスを非常に強力にするものです。 最初に、関数とその期待される動作のテストを記述して、合格させることができます。 次に、合格したら、関数を別の実装にリファクタリングできます。テストがまだ合格している場合は、別の実装でもテスト内で指定されたとおりに関数が動作していることがわかります。 Adderのadd関数は良い例です!

角度設定

まず、AngularCLIを使用して新しいアプリケーションを作成します。

ng new angular-testing --routing

このアプリケーションには複数のビューがあるため、--routingフラグを使用して、CLIがルーティングモジュールを自動的に生成します。

ここから、新しいangular-testingディレクトリに移動してアプリケーションを実行することにより、すべてが正しく機能していることを確認できます。

cd angular-testing
ng serve -o

また、アプリケーションのテストが現在合格状態にあることを確認することもできます。

ng test

ホームページの追加

ホームページにユーザーを表示するサービスを作成する前に、まずホームページを作成します。

ng g component home

コンポーネントが作成されたので、ルーティングモジュール(app-routing.module.ts)のルートパスをHomeComponentに更新できます。

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';

const routes: Routes = [
  { path: '', component: HomeComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

アプリケーションをまだ実行していない場合は実行すると、「ホームワーク」が表示されます。 CLIによって作成されたapp.component.htmlのデフォルトテンプレートの下。

AppComponentテストの削除

AppComponentのデフォルトの内容は不要になったので、不要なコードを削除して更新しましょう。

まず、app.component.htmlのすべてを削除して、router-outletディレクティブのみが残るようにします。

<router-outlet></router-outlet>

app.component.ts内で、titleプロパティを削除することもできます。

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent { }

最後に、以前はapp.component.htmlにあったコンテンツの一部について、2つのテストを削除することにより、app.component.spec.tsのテストを更新できます。

import { async, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule
      ],
      declarations: [
        AppComponent
      ],
    }).compileComponents();
  }));
  it('should create the app', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  }));
});

Angularサービスのテスト

ホームページが設定されたので、このページに従業員のディレクトリを入力するサービスの作成に取り掛かることができます。

ng g service services/users/users

ここでは、usersサービスを新しいservices/usersディレクトリ内に作成して、サービスをデフォルトのappディレクトリから遠ざけています。

サービスが作成されたので、テストファイルservices/users/users.service.spec.tsにいくつかの小さな変更を加えることができます。

個人的には、it()内に依存関係を挿入することは、以下に示すように、テストファイルのデフォルトのスキャフォールディングで行われるため、少し反復的で読みにくいと感じています。

it('should be created', inject([TestService], (service: TestService) => {
  expect(service).toBeTruthy();
}));

いくつかの小さな変更を加えるだけで、これをbeforeEachに移動して、各itから重複を削除できます。

import { TestBed } from '@angular/core/testing';
import { UsersService } from './users.service';

describe('UsersService', () => {
  let usersService: UsersService; // Add this

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [UsersService]
    });

    usersService = TestBed.get(UsersService); // Add this
  });

  it('should be created', () => { // Remove inject()
    expect(usersService).toBeTruthy();
  });
});

上記のコードでは、TestBed.configureTestingModule({})は、providersに設定されたUsersServiceを使用してテストするサービスを設定します。 次に、TestBed.get()を使用して、テストするサービスを引数としてテストスイートにサービスを挿入します。 戻り値をローカルのusersService変数に設定します。これにより、コンポーネント内と同じように、テスト内でこのサービスを操作できるようになります。

テストセットアップが再構築されたので、ユーザーのコレクションを返すallメソッドのテストを追加できます。

import { of } from 'rxjs'; // Add import

describe('UsersService', () => {
  ...

  it('should be created', () => {
    expect(usersService).toBeTruthy();
  });

  // Add tests for all() method
  describe('all', () => {
    it('should return a collection of users', () => {
      const userResponse = [
        {
          id: '1',
          name: 'Jane',
          role: 'Designer',
          pokemon: 'Blastoise'
        },
        {
          id: '2',
          name: 'Bob',
          role: 'Developer',
          pokemon: 'Charizard'
        }
      ];
      let response;
      spyOn(usersService, 'all').and.returnValue(of(userResponse));

      usersService.all().subscribe(res => {
        response = res;
      });

      expect(response).toEqual(userResponse);
    });
  });
});

ここでは、allがユーザーのコレクションを返すという期待のテストを追加します。 userResponse変数セットをサービスメソッドのモック応答に設定することを宣言します。 次に、spyOn()メソッドを使用してusersService.allをスパイし、.returnValue()をチェーンして、モックされたuserResponse変数をof()でラップして返します。オブザーバブルとしてのこの値。

スパイセットを使用して、コンポーネント内で行うのと同じようにサービスメソッドを呼び出し、observableをサブスクライブし、その戻り値をresponseに設定します。

最後に、responseがサービス呼び出しの戻り値userResponseに設定されるという期待を追加します。

なぜモック?

この時点で、多くの人が「なぜ私たちは応答をあざけるのですか?」と尋ねます。 サービスから返されるものを手動で設定するために、自分で作成した戻り値userResponseをテストに提供したのはなぜですか? ハードコードされたユーザーのセットであろうとHTTPリクエストからの応答であろうと、サービス呼び出しはサービスからの real 応答を返すべきではありませんか?

これは完全に合理的な質問であり、最初にテストを開始するときに頭を包み込むのは難しい場合があります。 この概念は、実際の例で説明するのが最も簡単だと思います。

あなたがレストランを経営していて、それが開幕日の前夜だと想像してみてください。 あなたはレストランの「試運転」のために雇ったすべての人を集めます。 数人の友人を招待して、彼らが座って食事を注文する顧客のふりをします。

テストランでは実際に料理は提供されません。 あなたはすでにあなたの料理人と一緒に働いており、彼らが料理を正しく作ることができることに満足しています。 このテスト実行では、顧客が料理を注文し、ウェイターがそれをキッチンに送り、ウェイターがキッチンの顧客への応答を実行するまでの移行をテストします。 キッチンからのこの応答は、いくつかのオプションの1つである可能性があります。

  1. 食事の準備ができました。
  2. 食事が遅れます。
  3. 食事はできません。 料理の材料が足りなくなった。

食事の準備ができたら、ウェイターが顧客に食事を届けます。 ただし、食事が遅れたり、食事ができなかったりした場合、ウェイターは顧客に戻って謝罪し、2番目の料理を要求する可能性があります。

私たちのテストランでは、バックエンド(キッチン)から受け取ったリクエストを満たすフロントエンド(ウェイター)の能力をテストしたいときに、実際に食事を作成することは意味がありません。 さらに重要なことに、ウェイターが食事が遅れたり、食事ができなかったりした場合に、実際に顧客に謝罪することができます。これらのケースのテストの前に、料理人が遅すぎるか、材料がなくなるまで文字通り待っていました。確認できた。 このため、バックエンド(キッチン)を「モック」して、テストしたいシナリオが何であれ、ウェイターに提供します。

同様に、コードでは、さまざまなシナリオをテストするときに実際にAPIをヒットすることはありません。 受信が予想される応答をモックし、アプリケーションがそれに応じてその応答を処理できることを確認します。 キッチンの例と同じように、失敗したAPI呼び出しを処理するアプリケーションの機能をテストしている場合、APIがそのケースを処理できることを確認できないのを文字通り待つ必要があります。これは、それほど頻繁には発生しないことを願っています。 !!

ユーザーの追加

このテストに合格するには、users.service.tsにサービスメソッドを実装する必要があります。

まず、インポートと従業員のコレクションをサービスに追加することから始めます。

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs'; // Add imports

@Injectable({
  providedIn: 'root'
})
export class UsersService {
  users: Array<object> = [  // Add employee object
    {
      id: '1',
      name: 'Jane',
      role: 'Designer',
      pokemon: 'Blastoise'
    },
    {
      id: '2',
      name: 'Bob',
      role: 'Developer',
      pokemon: 'Charizard'
    },
    {
      id: '3',
      name: 'Jim',
      role: 'Developer',
      pokemon: 'Venusaur'
    },
    {
      id: '4',
      name: 'Adam',
      role: 'Designer',
      pokemon: 'Yoshi'
    }
  ];

  constructor() { }
}

次に、コンストラクターのすぐ下に、allを実装できます。

all(): Observable<Array<object>> {
  return of(this.users);
}

ng testを再度実行すると、サービスメソッドの新しいテストを含むテストに合格するはずです。

ホームページにユーザーを追加する

サービスメソッドを使用する準備ができたので、これらのユーザーをホームページに表示する作業を行うことができます。

まず、index.htmlBulma に更新して、スタイリングに役立てます。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>AngularTesting</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <!--Add these-->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css">
  <script defer src="https://use.fontawesome.com/releases/v5.1.0/js/all.js"></script>
</head>
<body>
  <app-root></app-root>
</body>
</html>

次に、home/home.component.ts内で、新しいサービスへの呼び出しを追加できます。

import { Component, OnInit } from '@angular/core';
import { UsersService } from '../services/users/users.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
  users;

  constructor(private usersService: UsersService) { }

  ngOnInit() {
    this.usersService.all().subscribe(res => {
      this.users = res;
    });
  }

}

まず、サービスをインポートして、コンポーネントのコンストラクターに挿入します。 次に、ngOnInit内のサービスメソッドへの呼び出しを追加し、戻り値をコンポーネントのusersプロパティに設定します。

これらのユーザーをビューに表示するには、home/home.component.htmlのテンプレートを更新します。

<section class="section is-small">
  <div class="container">
    <div class="columns">
      <div class="column" *ngFor="let user of users">
        <div class="box">
          <div class="content">
            <p class="has-text-centered is-size-5">{% raw %}{{user.name}}{% endraw %}</p>
            <ul>
              <li><strong>Role:</strong> {% raw %}{{user.role}}{% endraw %}</li>
              <li><strong>Pokemon:</strong> {% raw %}{{user.pokemon}}{% endraw %}</li>
            </ul>
          </div>
        </div>
      </div>
    </div>
  </div>
</section>

ng serveを実行してホームページを表示すると、ブルマボックス内にユーザーが表示されているはずです。

シングルユーザーを見つける

ユーザーがホームページに入力されたので、ユーザープロファイルページに使用される単一のユーザーを検索するためのサービスメソッドをもう1つ追加します。

まず、新しいサービスメソッドのテストを追加します。

describe('all', () => {
  ...
});

describe('findOne', () => {
  it('should return a single user', () => {
    const userResponse = {
      id: '2',
      name: 'Bob',
      role: 'Developer',
      pokemon: 'Charizard'
    };
    let response;
    spyOn(usersService, 'findOne').and.returnValue(of(userResponse));

    usersService.findOne('2').subscribe(res => {
      response = res;
    });

    expect(response).toEqual(userResponse);
  });
});

ここでは、findOneが単一のユーザーを返すという期待のテストを追加します。 userResponse変数を、ユーザーのコレクションからの単一のオブジェクトであるサービスメソッドのモック応答に設定することを宣言します。

次に、usersService.findOneのスパイを作成し、モックされたuserResponse変数を返します。 スパイセットを使用して、サービスメソッドを呼び出し、その戻り値をresponseに設定します。

最後に、responseがサービス呼び出しの戻り値userResponseに設定されるというアサーションを追加します。

このテストに合格するには、users.service.tsに次の実装を追加します。

all(): Observable<Array<object>> {
  return of(this.users);
}

findOne(id: string): Observable<object> {
  const user = this.users.find((u: any) => {
    return u.id === id;
  });
  return of(user);
}

これで、ng testを実行すると、すべてのテストが合格状態で表示されるはずです。

結論

この時点で、テストの利点とそれらを作成する理由の両方がもう少し明確になり始めていることを願っています。 個人的に、私は最も長い間テストを延期しました、そして私の理由は主に私がそれらの背後にある理由を理解していなかったためであり、テストのためのリソースが限られていました。

このチュートリアルで作成したものは、視覚的に最も印象的なアプリケーションではありませんが、正しい方向への一歩です。

次のチュートリアルでは、Pokeapiを使用してポケモン画像を取得するためのユーザープロファイルページとサービスを作成します。 HTTPリクエストを行うサービスメソッドをテストする方法とコンポーネントをテストする方法を学びます。

追加

ターミナル内でテストをより読みやすい形式で表示したい場合は、このためのnpmパッケージがあります。

まず、パッケージをインストールします。

npm install karma-spec-reporter --save-dev

それが終わったら、src/karma.conf.jsを開き、新しいパッケージをpluginsに追加し、reporters内のprogress値をspecに更新します。

module.exports = function (config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine', '@angular-devkit/build-angular'],
    plugins: [
      require('karma-jasmine'),
      require('karma-chrome-launcher'),
      require('karma-jasmine-html-reporter'),
      require('karma-coverage-istanbul-reporter'),
      require('@angular-devkit/build-angular/plugins/karma'),
      require('karma-spec-reporter') // Add this
    ],
    client: {
      clearContext: false // leave Jasmine Spec Runner output visible in browser
    },
    coverageIstanbulReporter: {
      dir: require('path').join(__dirname, '../coverage'),
      reports: ['html', 'lcovonly'],
      fixWebpackSourcePaths: true
    },
    reporters: ['spec', 'kjhtml'], // Update progress to spec
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome'],
    singleRun: false
  });
};

これで、ng testを実行すると、テストスイートに対してより視覚的に魅力的な出力が得られるはずです。