タイプスクリプトによるリポジトリパターン


ハイ👋
この記事では、リポジトリのパターンについて話をし、認証を処理するためのリポジトリをどのように実装しているかを示しています.それでは、それらのテストの書き方を教えてあげましょう.また、依存関係の注入、抽象化などの他のトピックを簡単に行きます.など

リポジトリ


リポジトリとは何かを知らない場合は、以下のような空想的定義を行います.

Repositories are classes or components that encapsulate the logic required to access data sources. They centralize common data access functionality, providing better maintainability and decoupling the infrastructure or technology used to access databases from the domain model layer.


今、私はより人間の読みやすいバージョンを与えるようにします、そして、私は我々の認証機能を例として使います.
私は、ユーザーがGoogleを私のアプリにサインするのを使用することができて欲しいです、流れはこのようなものです:
  • ユーザーがGoogleでログインします.
  • Googleから返されたトークンを取得し、バックエンドに送信します.
  • バックエンドはすべての将来のリクエストに使用できるアクセストークンを返します.
  • トークンをローカルに保持し、ユーザをサインしておく.
  • このことから、3つのデータソースが必要です.
  • GoogleAuth
  • AuthApi : それはバックエンドと通信する
  • LocalStorage : ログインしているユーザを保持する場所
  • それから、我々はあるAuthRepository 内部的にこれらの3つのデータソースを使用し、認証を処理するために使用できる単純なAPIを公開します.私たちはすぐにこれを実装する方法を見るでしょう、しかし、これは倉庫が私の単純な視点で働く方法です.

    抽象化はあなたの友人


    The AuthRepository 非常に良い抽象例です.それはそれを使用する他のすべてのパーティーからの実装の詳細を非表示にします.例えば、反応コンポーネントはこの倉庫を使用することができて、ちょうど呼び出しによってユーザに署名することができますauthRepo.signInWithGoogle() . コンポーネントがどのようにサインインを行うのか気にしない、または知っていない、それは我々が作業を行うには、3つのデータソースを使用することを知りません.この抽象化は非常に便利です、例えば、我々がコンポーネントをテストしたいならば、我々はちょうど倉庫を模擬することができて、それを確かめることができますsignInWithGoogle が呼び出されました.私たちは、私たちがすでにテストを書いていたので、サインinが働くと確信するでしょうAuthRepository . この抽象化を書きましょう.
    interface IAuthRepository {
      signInWithGoogle(): Promise<User>;
    }
    

    NOTE1: The 'I' in 'IAuthRepository' stands for Interface (duh), and It's just a naming convention.

    NOTE2: There should be more function declarations in the interface, like signOut() ...etc, but I wrote only one for sake of simplicity.


    そして、どのようにリポジトリは、実装の詳細を要約すると、データソースも同じことを行う必要があります.あまりにもインタフェースを動かしましょう.
    interface IGoogleAuth {
        // returns a token
        signIn(): Promise<string>
    }
    
    interface IAuthApi {
      signInWihGoogle(token: string): Promise<User>;
    }
    
    interface ILocalStorage {
        write(key: string, value: string): Promise<null>;
        read(key: string): Promise<string | null>;
    }
    
    あなたはUser 任意の方法を入力します.簡単にするには、次のように使います.
    type User = {
        accessToken: string
    }
    
    今、我々は、我々が実装を開始するために必要なすべてを持っているAuthRepository

    実装


    実装するクラスを作成しますIAuthRepository , また、すべての関数宣言に対して具体的な実装を行います(この場合、1つだけです).
    class AuthRepository implements IAuthRepository {
        signInWithGoogle(): Promise<User> {
            throw new Error("Method not implemented.");
        }
    }
    
    クラス内のデータソースへのアクセスが必要なので、それらに依存するようにしましょう.
    class AuthRepository implements IAuthRepository {
        private _authApi: IAuthApi;
        private _googleAuth: IGoogleAuth;
        private _localStorage: ILocalStorage;
    
        constructor(
            authApi: IAuthApi,
            googleAuth: IGoogleAuth,
            localStorage: ILocalStorage,
        ) {
            this._authApi = authApi;
            this._googleAuth = googleAuth;
            this._localStorage = localStorage;
        }
    
        signInWithGoogle(): Promise<User> {
            throw new Error("Method not implemented.");
        }
    }
    
    プライベートフィールドを追加し、コンストラクタの中で初期化しました.
    フィールドは、リポジトリからデータソースにアクセスできるように意味をなさないため、プライベートにする必要があります.
    また、コンストラクタで依存関係を渡すことが重要です.まず、データソースの実装を書くことはできません.また、このような依存関係を注入することで、データソースの模擬実装を通過できます.
    今、私たちは実際の実装を書くことができますsignInWithGoogle() :
    async signInWithGoogle(): Promise<User> {
          const token = await this._googleAuth.signIn();
          const user = await this._authApi.signInWihGoogle(token);
          await this._localStorage.write('accessToken', user.accessToken);
          return user;
    }
    
    それは、私たちのリポジトリが実装されます.すぐにテストを開始できます.でも前に...

    エラー処理


    今までのところ何もエラー処理していません.私たちの倉庫は、現時点では非常に安全ではない、何も間違って行くことができます.例えば、ユーザのネットワークは不安定であるかもしれません、そして、若干の要請はtimeoutです.または何かが間違ってGoogle Authで行くことができる、あなたは知っている.これは、データ層には非常に厄介な予測不可能な世界です.
    はい、あなたは倉庫のすべての呼び出しをラップすることができますtry catch ブロック.本当にやりたいですか?まず第一に、あなたはそれを行う義務がありません、どんな規則時間エラーもないので、あなたに思い出させるどんな規則もあなたに思い出させません.エラーをキャッチすることを忘れたので、クラッシュするアプリをしたくない.第二にtry catch ちょうど醜いです.
    リポジトリパターンを使用する別の利点があります.リポジトリ内のすべてのエラーをキャッチできます.try catch 再び外に.まず、認証中に発生するすべてのエラーを指定する必要があります.

  • ネットワークエラー:ユーザーがオフラインまたは接続が不安定な場合

  • 一般的なエラー
  • 私たちは多くの点でこれらのエラーを表すことができます.
    enum AuthError {
      general,
      network,
    }
    
    それから、我々は我々の機能を戻す必要がありますAuthError Aの代わりにUser を返します.最初、私はそれを組合に帰ろうとしましたAuthError | User , しかし、私はリターン値の正確なタイプをチェックする良い方法を見つけませんでした.私はタイプガードを使うことができました、しかし、私はより良い方法があると思います.The fp-ts ライブラリは、多くの機能プログラミンググッズを提供していますEither 種類:
    type Either<E, A> = Left<E> | Right<A>
    
    見てわかるようにEither のいずれかを左または右のインスタンスです.左は失敗のために使われます、そして、権利は成功のために使われます(see the docs) .
    戻り値のタイプを変えましょうsignInWithGoogle 関数を返すEither<AuthError, User> , インターフェイスと実装されているクラスの両方で行うべきです.
     interface IAuthRepository {
       signInWithGoogle(): Promise<Either<AuthError, User>>;
     }
    
    実装では、right(user) の代わりにuser 万事うまくいくならば、我々は帰りますleft(error) :
    async signInWithGoogle(): Promise<Either<AuthError,User>> {
      try {
        const token = await this._googleAuth.signIn();
        const user = await this._authApi.signInWihGoogle(token);
        await this._localStorage.write('accessToken', user.accessToken);
        return right(user);
      } catch (e) {
        // Check for the type of error here
        // and return the corresponding value
        // if (e is a network error)
        //   return left(AuthError.network);
        return left(AuthError.general);
      }
    }
    
    今、我々が呼ぶときはいつでもsignInWithGoogle , 返り値がleft or right , を実行します.
    const result = await authRepo.signInWithGoogle();
    if (isRight(result)) {
      // The sign in succeeded
      const user = result.right;
      // do something with the user
    } else {
      // The sign in failed
      const error = result.left;
      // do something with the error
    }
    
    このメソッドを使用すると、すべての例外は、リポジトリに到着したときに伝播を停止し、後で処理できる正規オブジェクトに変換されます.

    テスト


    ここでは楽しい部分は、我々は我々の倉庫を実装して以来、我々はそれを試して、非常に簡単です今テストします.すべての依存関係がコンストラクタに注入されるので、私たちは単にモック依存関係を作成することができます、そして、それらと共に倉庫をinstanciateします.あなたは冗談を使ってこれを得ることができますが、より良い方法があると思います.

    エントランス


    とてもクールな図書館がありますts-mockito , あなたがコードに依存注入を使用するとき、それは非常によく働きます(現在我々がしているように).それはあなたがモック、スタブ関数呼び出しを作成し、他の多くのクールな機能は、間違いなくそれをチェックアウトすることができます!テストするために使いますAuthRepository .
    この長いテストファイルを見てください.
    import { left, right } from "fp-ts/lib/Either";
    import { anything, instance, mock, reset, verify, when } from "ts-mockito";
    /// Also import the interfaces and other things...
    
    /// Create mock dependencies
    const MockGoogleAuth = mock<IGoogleAuth>();
    const MockAuthApi = mock<IAuthApi>();
    const MockStorage = mock<ILocalStorage>();
    
    /// Instatiate AuthRepository with the mocks
    const authRepo: IAuthRepository = new AuthRepository(
      instance(MockAuthApi),
      instance(MockGoogleAuth),
      instance(MockStorage)
    );
    
    // Reset the mocks before each test
    // So tests won't be dependent of each other
    beforeEach(() => {
      reset(MockAuthApi);
      reset(MockGoogleAuth);
      reset(MockStorage);
    });
    
    // Testing `signInWithGoogle`
    describe("signInWithGoogle", () => {
      // Test case 1
      test("should persist and return the user if all goes well", async () => {
        // arrange
        const googleToken = "googleToken";
        const user: User = { accessToken: "accessToken" };
        // When signIn is called on MockGoogleAuth, resolve with `googleToken`
        when(MockGoogleAuth.signIn()).thenResolve(googleToken);
        // When signInWithGoogle is called on MockAuthApi, resolve with `user`
        when(MockAuthApi.signInWihGoogle(googleToken)).thenResolve(user);
        // act
        const result = await authRepo.signInWithGoogle();
        // assert
        // the result should be `right(user)`
        expect(result).toStrictEqual(right(user));
        // MockGoogleAuth.signIn should be called once
        verify(MockGoogleAuth.signIn()).once();
        // MockAuthApi.signInWithGoogle should be called once, with `googleToken`
        verify(MockAuthApi.signInWihGoogle(googleToken)).once();
        // The access token should be persisted
        verify(MockStorage.write("accessToken", user.accessToken)).once();
      });
    
      test("should return an auth error if something wrong happened", async () => {
        // arrange
        // Make it so GoogleAuth throws an exception
        when(MockGoogleAuth.signIn()).thenReject(new Error("some error"));
        // act
        const result = await authRepo.signInWithGoogle();
        // assert
        // the result should be `left(AuthError.general)`
        expect(result).toStrictEqual(left(AuthError.general));
        // MockGoogleAuth.signIn should be called once
        verify(MockGoogleAuth.signIn()).once();
        /// MockAuthApi.signInWithGoogle should never be called
        verify(MockAuthApi.signInWihGoogle(anything())).never();
        // No access token should be persisted
        verify(MockStorage.write("accessToken", anything())).never();
      });
    });
    
    
    うまくいけば、このテスト戦略は明確でした、そして、それはあなたに将来あなたのコードをテストする方法のアイデアを与えました.それはかなり簡単だと思う.

    結論


    ここでは、この記事を通して学んだ主なポイントです.

  • リポジトリ:複数のデータソースをグループ化し、APIを公開するコンポーネント/クラスは、すべての実装の詳細を隠します.また、“エラーバスター”として、それはすべての野生の例外をキャッチし、失敗の場合は、通常のオブジェクトを返します.

  • 抽象化(インターフェイスを使用する):インターフェイスを使用すると、同じAPI(例えばモックと実際の実装)に対して複数の実装を持つことができます.また、変更を容易にするAuthApi たとえば、残りの実装からGraphSQLに.など.これは、開発プロセスをスピードアップ、特に場合は、チームとして働いている.あなただけのインターフェイスを設定し、それに依存する他のものを実装を開始します.あなたがこの記事で見たように、我々はAuthApi , その他の依存関係もない.

  • 依存性注入:この記事のDIの詳細については知りませんでしたが、クラスコンストラクタに依存性を注入する方法を見ました.また、これを行うことなく、すべての依存関係を実装するまで、リポジトリを実装する能力を持っていません.あなたは、抽象化とDIを互いに補完すると言うことができます.

  • 役に立つライブラリ

  • ts-mockito : モッキング、および一般的なテストを支援する非常に便利なライブラリです.

  • fp-ts : すべての機能プログラミンググッズを入力するライブラリです.私たちはEither この記事のタイプは、それは多くを提供しています.
  • 私はあなたが良い読んで、さよならを願っています.