アルパカログ

プログラミングとマネジメントがメインです。時々エモいのも書きます。

【AngularFire2】テストでsnapshotChangesをスタブする

Angular.js + Firestoreという構成ではAngularFire2を使うケースが多いと思います。

AngularFire2ではFirestoreからデータ(コレクション)を取得するとき、valueChanges()snapshotChanges()の2つの方法があります。

このうちvalueChanges()はドキュメントIDなどのメタデータを含まないデータそのものだけを返しますが、更新系の処理を行うにはsnapshotChanges()を使ってメタデータを含めてデータを取得する必要があります。

テストの際、メタデータを含まないデータのみを返すvalueChanges()をスタブするのは簡単です。しかし、メタデータを含めたデータを返すsnapshotChanges()をスタブするのは少し面倒です。さらに悪いことに、調べてもあまり情報が出てきません。

そこで今日はsnapshotChanges()のスタブの例を紹介したいと思います。

UserServiceクラスの例

よくあるUserServiceクラスの例を下記に挙げます。公式サンプル通りなので特に説明は不要でしょう。

src/app/user.service.ts

import { Injectable } from '@angular/core';
import { User } from './user';
import { AngularFirestore } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class UserService {

  constructor(private afs: AngularFirestore) { }

  /**
   * 全てのUserを取得する
   */
  getUsers(): Observable<User[]> {
    return this.afs.collection('users').snapshotChanges().pipe(
      map(actions => actions.map(action => {
        const doc = action.payload.doc;
        const user = doc.data() as User;
        return { id: doc.id, ...user };
      }))
    );
  }
}

テストの例

次にsnapshotChanges()をスタブします。

結論から言うと、snapshotChanges()DocumentChangeAction<T>[]を型引数に持つObservableを返します。下記のように定義されています。

snapshotChanges(events?: DocumentChangeType[]): Observable<DocumentChangeAction<T>[]>

https://github.com/angular/angularfire2/blob/23464c29088602f05bc869226d2120920146bb6a/src/firestore/collection/collection.ts#L97

実際のスタブの例を見て見ましょう。DocumentChangeAction[]を作成しJasmine.createSpysnapshotChanges()の返り値に設定しているところに注目してください。

他の注意点としてはDocumentChangeActionDocumentChangeを、DocumentChangeDocumentSnapshotをインターフェースとして持つところです。興味のある人はソースを読んでみてくださいね。

src/app/user.service.spec.ts

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

import { AngularFirestore } from '@angular/fire/firestore';
import { DocumentChangeAction } from '@angular/fire/firestore/interfaces';
import { User } from './user';
import { UserService } from './user.service';
import { Observable, of } from 'rxjs';

const fixtureData = [
  {
    id: 'foo',
    email: 'foo@example.com',
    name: 'fooName',
  },
  {
    id: 'bar',
    email: 'bar@example.com',
    name: 'barName',
  },
  {
    id: 'baz',
    email: 'baz@example.com',
    name: 'bazName',
  },
];

const documentChangeActions: DocumentChangeAction<User>[] = fixtureData.map(data => {
  const user: User = data;
  const documentSnapshot = { id: user.id, data: (() => user) };
  const documentChange = { doc: documentSnapshot };
  return { payload: documentChange } as DocumentChangeAction<User>;
});

const collectionStub = {
  snapshotChanges: jasmine.createSpy('snapshotChanges').and.returnValue(of(documentChangeActions))
};
const angularFirestoreStub = {
  collection: jasmine.createSpy('collection').and.returnValue(collectionStub)
};

describe('UserService', () => {
  beforeEach(() => TestBed.configureTestingModule({
    providers: [
      UserService,
      { provide: AngularFirestore, useValue: angularFirestoreStub }
    ]
  }));

  describe('getUsers()', () => {
    it('returns users as Observable', () => {
      const service: UserService = TestBed.get(UserService);
      service.getUsers().subscribe(users => expect(users).toEqual(fixtureData as User[]));
    });
  });
});

Angular.js + Firestoreの構成では、Angular.jsフレームワーク、TypeScriptやRxJSなど必須で学ぶことが多く、さらにAngularFire2もとなると、プログラミングに慣れている人にとってもかなり学習コストが高くしんどいと思います。

特に私は、普段サーバーアプリケーションばかり書いているのでRxJSに苦戦しました。ひとつひとつ確実に理解していくことが肝心ですね。