0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Firebase】Firebase Functionsのテストを書く時に、DIするServiceクラスの内容によってテストの内容を切り替えたい場合のTips

Posted at

DIしたいインターフェースの内容

テストを行いたい呼び出し可能関数が、次のインターフェースを実装したクラスに依存しているとします。

import type { User } from "~/domain/model/User";

export interface UserService {
  updateEmail: (uid: string, email: string) => Promise<User | undefined>;
  updateName: (uid: string, name: string) => Promise<User | undefined>;
}

この時に、updateEmail()メソッドの返り値によって呼び出し可能関数の結果が変わるとします。

import { container } from 'tsyringe';
...

export const updateEmail: CallableMethod = async (
  data,
  context,
): Promise<UpdateEmailResponseData> => {
  const userService = container.resolve<UserService>('UserService');

  const result = await userService.updateEmail(uid, newEmail);

  if (!result) {
    throw new functions.https.HttpsError('internal', 'メールアドレスの更新を行うことができません');
  }

  return {
    newEmail: newEmail,
  };
}

偽の実装クラスの作成

この場合、UserServiceインターフェースのクラスをテストごとに何度も実装するのを避けるため、偽の実装クラスを作成します。

import type { UserService } from "../UserService";

import { User } from "~/domain/model/User";
import type { User as UserModel } from '~/infrastructure/database/model/User';

const userModel: UserModel = {
    ...
};

export class FakeUserServiceImpl implements UserService {
  async updateEmail(uid: string, email: string): Promise<User | undefined> {
    const user: User = new User(userModel);
    return user;
  }

  async updateName(uid: string, name: string): Promise<User | undefined> {
    const user: User = new User(userModel);
    return user
  }
}

成功用のテストを実装

そして、成功用のテストを実装します。

import 'jest';
import 'reflect-metadata';
import firebaseFunctionsTest from 'firebase-functions-test';
import * as firebaseFunctions from 'firebase-functions';

const test = firebaseFunctionsTest();

import { updateEmail } from '~/routes/staymile/tab-four/change-email/updateEmail';
import { container } from 'tsyringe';
import { UserService } from '~/application/service/UserService';
import { FakeUserServiceImpl } from '~/application/service/impl/FakeUserServiceImpl';

describe('updateEmail', () => {
  it('成功するテスト', async () => {
    const uid = 'user-id';
    const newEmail = 'newemail@example.com';

    container.register<UserService>('UserService', {
      useClass: FakeUserServiceImpl,
    });
    const wrapped = test.wrap(firebaseFunctions.https.onCall(updateEmail));
    const result = await wrapped(
      {
        newEmail: newEmail,
      },
      {
        auth: {
          uid: uid,
        },
      },
    );

    expect(result.newEmail).toBe(newEmail);
  });
}

次に失敗するテストを書きたいので、updateEmail()メソッドがundefinedを返すようにしたいです。そのため、新たに偽のクラスを作成するという方法もあるのですが、あまりに冗長です。

偽のクラスの再実装を防ぐ

そこで、偽の実装クラスの再生成を防ぐためにjest.spyOnを使って、偽クラスのメソッドの返り値を置き換えることにします。

jest.spyOn(FakeUserServiceImpl.prototype, 'updateEmail').mockResolvedValue(undefined);

失敗テストの作成

これによりupdateName()メソッドがundefinedを返した場合に、例外が発生することをテストすることができるようになりました。

it('メールアドレスの変更に失敗した場合はエラーを発生させる', async () => {
    jest.spyOn(FakeUserServiceImpl.prototype, 'updateEmail').mockResolvedValue(undefined);

    const uid = 'user-id';
    const newEmail = 'newemail@example.com';

    container.register<UserService>('UserService', {
      useClass: FakeUserServiceImpl,
    });
    const wrapped = test.wrap(firebaseFunctions.https.onCall(updateEmail));
    expect(
      wrapped(
        {
          newEmail: newEmail,
        },
        {
          auth: {
            uid: uid,
          },
        },
      ),
    ).rejects.toThrowError();
  });

参考にした記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?