every Tech Blog

株式会社エブリーのTech Blogです。

SPMでマルチモジュール/マルチターゲット開発

はじめに

この記事は every Tech Blog Advent Calendar 2023 の 8 日目です。

トモニテのiOSアプリは今年、トモニテ妊娠アプリの開発を期にSPMを用いたマルチモジュール構成に移行しました。 これらのアプリにはアカウント管理やデザインシステムなど共通部分が多くあります。また一部機能は重複しているため、コード共通化をしやすくするのが主目的でした。

この記事ではマルチモジュール構成への移行をどのように進めたかと結果について書きたいと思います。

コード共通化の方針

以下の選択肢がありました。

  • A: コード共通化をしない
  • B: 共通部分を別リポジトリに切り出し、アプリもそれぞれ別リポジトリで運用する
  • C: 1プロジェクト(1リポジトリ)で複数アプリを開発する

コード共通化にはデメリットもあります。片方のアプリを修正した時もう一方のアプリにも影響する可能性があり、本来は必要のなかった調査や動作確認が必要になるかもしれません。しかし今回は共通部分が多く共通化のメリットの方が大きいと判断したためAは選びませんでした。

Bは、共通部分がアプリ本体と疎で、変更頻度が低ければ良い選択肢だったと思いますが、今回は選びませんでした。

モジュール構成を決める

モジュール間の依存が循環しないように関係を整理しつつ、分割方法を決めます。

まず、共通部分を以下のように、レイヤーに沿って分割しました。

  • Utilities: 便利クラス、Extensionなど
  • Network: 外部通信
  • Model: モデル
  • Core: Feature間の画面遷移の仕組みなどを提供する

一方、機能ごとに Feature モジュール群を作成しました。

  • Home: ホーム
  • Media: 記事や記事検索機能など
  • Childcare: 育児記録機能
  • Babyfood: 離乳食機能

これらの Feature モジュールは共通モジュール群に依存しますが、各Feature が相互に依存することは禁止しています。

Featureモジュール間の参照をせずに、Feature間の画面遷移を可能にする実装についてはこちらの記事を参考にさせていただきました。

メルペイのスケーラビリティを支えるマルチモジュール開発

この仕組みによってFeatureモジュールの独立性が保たれ、必要なモジュールだけをターゲットに組み込むことができるようになりました。

モジュールを切り出す

方針に沿ってモジュールを作成していきました。他への依存が少ない部分から順番に進める必要があります。Utilities -> Network -> Model -> Core -> 各Feature のような順序です。

Xcodeでプロジェクト内にパッケージを作成し、 Package.swift ファイルにパッケージの定義と依存関係を記述します。

import PackageDescription

let package = Package(
    name: "Network",
    platforms: [
        .iOS(.v14)
    ],
    products: [
        .library(
            name: "Network",
            targets: ["Network"])
    ],
    dependencies: [
        .package(url: "https://github.com/Moya/Moya.git", .upToNextMajor(from: "15.0.0")),
        .package(path: "Utilities")
    ],
    targets: [
        .target(
            name: "Network",
            dependencies: [
                "Moya",
                "Utilities"
            ],
            resources: [.copy("Stab")]
        )
    ]
)

あとはファイルをパッケージの中に移動し、外部から参照される宣言をpublicにするなど、アクセス修飾子を修正します。 移動したコードに本来あるべきでない依存があった場合は、依存関係をなくすための修正が必要になる場合も多々あります。

作業量がかなり多くなりますが、一度に終わらせる必要はなくパッケージ単位でリリースが可能です。通常の開発と並行して少しずつ進め、3ヶ月程度かかりました。

開発用の minimumTarget

マルチモジュールの利点を活かし、新規機能や新規画面を作る場合の開発効率を上げるために minimumTarget というものを作りました。 開発対象の Feature と最低限の共通部分だけをターゲットに組み込んで開発でき、以下のような利点があります。

  • ビルド時間短縮
    • トモニテ本体と比較して、クリーンビルド時間 141 秒 → 38 秒
  • シミュレータでのデバッグ開始が早い
  • SwiftUIのプレビューを利用可能
    • 本体ではプレビューが表示されるまでの時間が非常に長く実質利用できなかったのが利用可能になります

Xcode Cloud

デバッグ用の Firebase App Distribution 配布と App Store Connect へのサブミットを Xcode Cloud で自動化していて、リポジトリに変更を加えると二つのアプリが配布/サブミットされます。

結果

これまで書いてきたとおり開発効率を向上できました。

  • 複数のアプリに同様の変更を加える場合、重複した開発をせずに済む
  • モジュール間の依存関係が整理され、コード変更の影響範囲を把握しやすい
  • パッケージ内のファイルは xcodeproj ファイルで管理されなくなるので、 xcodeproj のコンフリクトがほぼ無くなる
  • minimumTarget で時間短縮

一方よくない点もありました。

  • 複数のアプリへの影響を考慮しながら開発する必要がある
  • 本来開発対象ではないアプリへの影響を調査したり、動作確認が必要になる可能性がある

しかし全体としては利点が大きく、移行した価値があったと判断しています。 この記事がどなたかの参考になれば幸いです。

ゼロからはじめるシステム引き継ぎ

はじめに

エブリーで小売業界向き合いの開発を行っている @kosukeohmura です。

今年、エブリーでは ネットスーパーのシステムを株式会社ベクトルワン様から引き継ぎました。その裏で、私たちのチームでは知見のないシステムを、自分たちで運用・開発可能な状態にするように様々なことをやってきました。

ここでは every Tech Blog Advent Calendar 2023 の 7 日目の記事として、システムを別の会社から引き継いだ中で考えたこと・やってきたことを紹介したいと思います。今回引き継いだシステムは具体的には Web アプリケーションサーバー、スマホアプリ、複数のスマホアプリ向け API サーバー、及びそれに付随するシステム(非同期処理・バッチ処理基盤、ロードバランサーなど)です。

なお、私は引き継ぎ作業の前段階(デューデリジェンス、大枠のスケジュール策定、契約締結など)が済んで、さあ実際にシステムを引き継ぐぞというところでこのプロジェクトのオーナーとなったので、実際のシステムの引き継ぎ作業に絞ってお話します。

目指す状態と現状のギャップを考える

私はシステムを他社から引き継いだ経験がなく担当となった当初何をしていいかわかりませんでした。両社のソースコードは GitHub で管理されていたので、とりあえず自社の開発メンバーを 外部のコラボレーター として招待していただきました。その後ソースコードをざっと眺めましたが、今回引き継いだネットスーパーのシステムはそれなりに大きく、ソースコードを読み続けてシステムを理解するのは筋が悪そうだとわかりました。

そこで引き継ぎのプロジェクトを通してどうなりたいかを考えだしました。システムを引き継いだらそれは自社のシステムとなります。ということは自社のメンバーで運用を行っていくのはもちろん、障害が起こったら自分たちで復旧し、プロダクトの詳細な仕様やバグへの説明責任も基本的には自分たちが持つことになります。その役割が果たせる状態と現状とのギャップは何か、それを埋めていくには何をすべきかを考えていきました。

足りない情報を要求・整理

何もわからないという状態を紐解いていくと、当たり前ではあるのですが自社のプロダクトなら当然知っているような情報が欠如していることに気づきました。例えば次のようなものです。

  • システムの実現するサービスとユースケース
  • 全体的なシステムの構成、構成要素それぞれの関係
  • システムと周辺システム、外部のシステムとの関係
  • ネットワーク、DNS
  • アクセス制御
    • 各種アカウント・認証情報
  • アプリケーションコード
    • 責務の分割のされ方
    • アプリケーションコード・テストコード記述のルール・方針
  • データベースの構造
  • システムの監視方法、正常性の把握の方法、異常時の対処方法
  • セキュアな情報の取り扱い方
  • 各種運用フロー
    • QA
    • リリース
    • 手動の作業

etc...

これらをリスト化し、それぞれについて情報を要求・整理していきました。知りたい項目についてドキュメントが無いこともありますし、ドキュメントが存在しても断片的、あるいは前提知識を必要としたりします。不足する情報はドキュメントを用意してもらったり、断片的な情報は受け取った後に情報をまとめて包括的に構成します。

この時強く思ったことは、引き継ぎは引き継ぐ側と引き継がれる側が協力して行うプロジェクトであるということでした。引き継がれる側としてはただ情報を待っているのではなく、どんな情報がなぜ必要で、どのようなアウトプットを期待しているのかをなるべく明確に伝える必要があると感じ、一つ一つの項目ごとにどういう状態になれば引き継ぎが完了となるのかの合意を取っていきました。

コミュニケーションツール、ドキュメンテーションツールの重要性

前述の通り、引き継ぎは自社・他社含めた協力プロジェクトであり、その中では多くのやり取りや資料の作成が行われます。社内ならば既定のツールを使用すればいいですが、社外の方とのやり取りでは既定のツールというものがありません。しかしフロー情報とストック情報を記入できるツールの導入は非常に重要です。

私達は共用のコミュニケーションツールとして Slack や Zoom を、ドキュメンテーションツールとして Notion を、タスク管理に Google スプレッドシートを使用しました。いずれのツールも一方が他方の普段使いのツールに相乗りする形を取っています。この場合引き継ぎプロジェクトが終わればツールへの相乗りも終了するので、相乗りしている側は必要な情報をエクスポートすることになります。

契約上の引き継ぎ時点を迎えての作業

引き継ぎのプロジェクトは契約上の引き継ぎ時点より数ヶ月前から始めましたが、実際の移管作業については契約上の引き継ぎ時点の近辺で行います。例えば次のようなことです。

  • Git リポジトリ移管
  • クラウドベンダー、ドメインレジストラなど各種契約の移管
  • 各種管理者の認証情報の受領

それぞれについて軽く触れます。

Git リポジトリの移管

GitHub の リポジトリの移譲 作業を行いました。ドキュメントに書いてあることではあるのですが、組織間のリポジトリの移譲には

  • 移譲前のリポジトリに対する管理者権限(または移譲前の組織の管理者権限またはオーナー権限)
  • 移譲先の組織のリポジトリを作成する権限

が必要でした。外部コラボレーターとして移譲前のリポジトリに招待してもらっている引き継ぎ元の開発者アカウントをリポジトリの管理者にしていただき、そのアカウントにて移譲作業を行いました。

リポジトリ移譲後は GitHub 引き継ぎ後にソースコードの内容を質問させていただくことを考え、逆に引き継ぎ元の開発メンバーを外部のコラボレーターとして招待させていただきました。

クラウドベンダー、ドメインレジストラなど各種契約の移管

システムの稼働や運営に必要な契約の付け替えを行います。契約しているサービスによって移管の方法は様々でした。ここでは AWS アカウントとドメインレジストラの移管の方法に触れます。

AWS

AWS Organizations のメンバーアカウントを他の組織へ移行する_ Part 1 _ Amazon Web Services ブログ の記事を参考に、メンバーアカウントの移管作業を行いました。

今回は両社ともに組織アカウントを使用しており、また移管対象のメンバーアカウントに移管対象外のシステムが含まれていなかったことから、単にメンバーアカウントを組織アカウントへ移行するのみで済みました。引き継ぎ対象外のリソースが存在するなどで AWS アカウントごとの移管が難しい場合には、前もって計画的にリソースを別の AWS アカウントに移すなどし、AWS アカウント移管を行える状態を作っておくことが必要です。またアカウントごとの移管が現実的ではない場合は、リソース単位で移管を行うことになると思います。

細かい話ですが、AWS アカウントの移行ではその時間を指定するようなことができず、移管作業を行ったタイミングで移管が行われるようです。出来ればとある日時以降の料金の請求がエブリーに対して行われるようにしたかったのですが、その方法は調べた限り無さそうでした。

ドメインレジストラ

引き継ぎ元ではお名前.com が利用されており、エブリーでは別のレジストラを主に使用していたのですが、今回は引き継ぐドメインが十数個と多く、引き継ぎ作業の楽さを優先してエブリーのお名前.com へとドメインを移管しました。お名前.com には お名前 ID 付け替え という機能があり、比較的楽にドメインを移管できました。今後必要となれば社内でお名前.com から普段使いのレジストラへの移管も検討しますが、優先度は低く置いています。

各種管理者の認証情報の受領

各種サーバーの root ユーザーの SSH 鍵や、データベースの root アカウントの認証情報の共有を頂きます。引き継ぎ元の開発者によるアクセスが必要なくなったら、認証情報を変更ができると良いでしょう。

引き継ぎの後にやったこと

ここまでで引き継ぎ作業としては終了ですが、引き継いだままの状態では社内の運用のフローとの齟齬が開発時の戸惑いに繋がったり、改めてプロダクトの状態を自社視点でみると最適化すべき部分が見つかります。そのそれぞれを自社の基準や文化に合わせて修正していくことはアジリティやサービス品質の維持・向上に繋がります。

なお、今回は引き継ぎを受けた後にも引き継ぎ元の開発者の方々に一定期間はサポートをいただけることになっています。契約上の引き継ぎ時点を過ぎ、必要な情報を頂いたつもりでも色々な情報が足りていないことに後から気づく場面が多くありましたので、契約等が許す限り一定期間サポートを受けられる体制をつくることが理想だと強く感じます。

次に具体的に引き継ぎを受けた後にやったことをいくつか紹介します。

クラウドサービスの料金の傾向・コスト構造の確認

AWS Web コンソール内の Cost Explorer にて料金の傾向を簡単に把握し、過剰なリソースがないかをざっとチェックしました。実際に、とあるマシンのインスタンスタイプが不自然に大きいことに気づきその変更を行いました。

これについては早くやるほどコストが節約できるので、AWS アカウントの移管が終わり、Cost Explorer が閲覧できるようになった初日に実施しました。結果として月々 6 万円以上のコスト削減に繋がりました。

手動運用の自動化

手動で行われていた運用について自動化出来そうなところがいくらかあったので、運用の背景を引き継ぎ元の開発者に伺いつつ、無理なく出来そうな部分については自動化をすすめています。

一見自動化できそうでも出来ない理由があったりするので、サポートいただけるうちに背景を聞きます。この作業では手動運用が削減できるだけではなく、システムについてより深く知る機会にもなりました。

監視機構の確認、追加

引き継ぎ前から行われているシステムの監視について整理し、社内の基準を鑑みて監視しておきたくなった点については新たに仕組みを導入しています。

合わせて既存の監視機構の通知先を変更し、自社の開発チームで以上に気付けるようにします。

こちらも不明点は引き継ぎ元の開発者に協力をいただき解消していきました。作業を通して、システムの全体への把握を強める機会にもなりました。

共同でのトラブル対応

移管後しばらくしてちょっとしたトラブルが起こりました。ソフトウェアエンジニアにとって、知らないシステムからの大量のアラートほど絶望するものは無いかもしれません。

ここでも引き継ぎ元の開発者にトラブルの概要を伺いながら原因を特定し、スムーズに対応することができました。もし自社のメンバーだけでの対応だったなら原因の特定や対処方針策定に相当な時間がかかっていたと思います。

余談ですが、このトラブルの原因となったバグは、システムの引き継ぎ前から潜んでいたものが偶然引き継ぎ直後に露見したというものでした。心臓に悪いので、もう少し空気を読んで露見するタイミングを選んでほしいものです。

おわりに

「ゼロからはじめるシステム引き継ぎ」と題して、何もわからないところからシステムを引き継いだプロジェクトについて紹介しました。こう書いてみて思ったこととして、今回は他社からシステムを引き継ぐ形でしたが、たとえ社内であっても異動があったり、新しくジョインされた方は何もわからないような状況に置かれる事に気づきました。システムのドキュメンテーションやクラウド料金・監視体制の見直しなんかは継続的に社内でも行っていけると良いなと思いました。

こういったプロジェクトに携わられる方はそう多くないとは思いつつ、どなたかの役に立つと幸いです。お読みいただきありがとうございました。

DI toolkit samber/do の紹介

はじめに

この記事は every Tech Blog Advent Calendar 2023 の 6 日目です。

今回は「DI toolkit samber/do の紹介」と題しまして、samber/do のざっくりとした紹介と、今後リリースされるであろう次期バージョンでの変更点についてまとめていきます。

samber/do とは

samber/do は Go で DI を実現するモジュールのひとつです。

同様の領域ではgoogle/wire が一番メジャーでしょうか。 その次にuber-go/fx が使われている印象です。

google/wire は現在はメンテナンスモードで、継続的な開発は行われていません。 uber-go/fx は reflection を利用した高度な機能を提供しているほか、単なるDIツールではなく、アプリケーションフレームワークとしての性格も兼ね備えている点が特徴です。

今回紹介する samber/do は reflection も code generation も用いず、シンプルに依存性の解決のみに注力している点が特徴的なモジュールです。

samber/do の DI

以下、公式の Quick start より転載です。

import (
    "github.com/samber/do"
)

func main() {
    injector := do.New()

    // provides CarService
    do.Provide(injector, NewCarService)

    // provides EngineService
    do.Provide(injector, NewEngineService)

    car := do.MustInvoke[*CarService](injector)
    car.Start()
    // prints "car starting"

    do.HealthCheck[EngineService](injector)
    // returns "engine broken"

    // injector.ShutdownOnSIGTERM()    // will block until receiving sigterm signal
    injector.Shutdown()
    // prints "car stopped"
}
type EngineService interface{}

func NewEngineService(i *do.Injector) (EngineService, error) {
    return &engineServiceImplem{}, nil
}

type engineServiceImplem struct {}

// [Optional] Implements do.Healthcheckable.
func (c *engineServiceImplem) HealthCheck() error {
    return fmt.Errorf("engine broken")
}

func NewCarService(i *do.Injector) (*CarService, error) {
    engine := do.MustInvoke[EngineService](i)
    car := CarService{Engine: engine}
    return &car, nil
}

type CarService struct {
    Engine EngineService
}

func (c *CarService) Start() {
    println("car starting")
}

// [Optional] Implements do.Shutdownable.
func (c *CarService) Shutdown() error {
    println("car stopped")
    return nil
}

基本的に do.Provide[T any](*do.Injector, func(*do.Injector) (T, error)) で生成関数を登録し、do.Invoke[T any](*do.Injector) で値を取得するという流れです。

uber-go/fx のように生成関数の引数から reflection で自動的に解釈して依存ツリーを作ってくれるような仕組みはないため、利用側で samber/do に依存した生成関数を定義する必要があります。

samber/do のメリット、デメリット

samber/do は非常に小規模なモジュールで、コア機能は依存性を登録することと依存性を解決することのみです。 それゆえに取り回しが非常にしやすく、例えばあるタイミングで依存性ツリーを再構築したい(実例として、Config のホットリロードなど)、と言うようなことも自分でアプリケーションを作り込めば無理なく実現可能です。

しかし、samber/do に依存する生成関数を定義しなければならない点はやや取り回しは悪いと感じるかもしれません。 また、依存性ツリーの構築はコンパイル時ではなく実行時に動的に行われるため、構築されたツリーに瑕疵がないか(登録が不十分で依存性の解決ができない)は利用者が担保する必要があります。 私も実際にテストを書いていて、うっかり生成関数の登録を忘れていて依存性解決の際にエラーになるということが稀にありました。

この辺りをDIツール側で担保してほしいというニーズが強い場合、google/wire や uber-go/fx の方が合っているかもしれません。

私が開発に関わっているプロジェクトでは、今の所規模も小さいこともあってこのようなトラブルは発生していませんが、いずれ向き合わなければならない問題だと認識しております。 近い将来に google/wire と似たようなアプローチで samber/do 向けの依存ツリーを静的に生成するモジュールを作ろうかと考えているところです。

samber/do@v2

samber/do は非常に新しいモジュールです。一般的に、新しいモジュールを導入する場合、それらがどのようにメンテナンスされているかを把握しておくことが重要です。

ところで私は最近 samber/do に次のメジャーバージョン v2 の計画があることに気づきました。

まだ計画段階ですが、いくつかこれが欲しかったんだ!と言う機能が盛り込まれる予定ですので、いくつかピックアップして紹介します。

Scope

v2 における目玉となる機能です。 依存ツリーを一つのまとまり(Scope)として、Scope間のツリー構造を構築し、依存性解決の際にScopeツリーを辿りながら値を取得することができるようになります。

これだけ聞くとなんのこっちゃ、と思うかもしれませんが、Java/Spring Boot に慣れている方であれば @ApplicationScoped@SessionScoped@RequestScoped のように生存時間が異なるオブジェクトを一つの依存性ツリーでまとめて管理できるようになると言えばイメージできるかもしれません。

Java/Spring Boot を触っていない人は全くわからない話で申し訳ありません。実際のサンプルコードがあるので、そちらを読むと良いかもしれません。

依存ツリーの明確化

これまでは do.Injector のフィールドにサービスを単純にmapで保持していたのですが、依存ツリーをDAG(有向非巡回グラフ)として明確に保持するように変わります。 これによって実行時にサービス間の依存関係を取得できるようになるため、DI部分で何かトラブルがあった際に問題を特定することが容易になります

循環参照の検出

依存性解決の際に循環参照を検出し、エラーにすることができるようになります。

Transient services

依存性解決のたびに毎回生成関数を呼び出して新たなオブジェクトを生成するサービスを登録することができるようになります。 現時点では以下のように引数なしの関数をサービスとして登録し、依存性解決後に自分で関数を呼び出してオブジェクトを取得する工夫が必要です。

import (
    "time"

    "github.com/samber/do"
)

func main() {
    injector := do.New()

    do.ProvideNamed(injector, "nowFunc", func(_ *do.Injector) (func() time.Time, error) { 
      return func() time.Time { return time.Now() }, nil
    })
    nowFunc := do.MustInvokeNamed[func() time.Time](injector, "nowFunc")
    now := nowFunc()
    println(now.Format(time.RFC3339))
}

tag ベースでの依存性注入の自動化

これはまだ v2 に入るかは不透明ですが、生成関数を毎回自分で書くのはそれなりに面倒なため、uber-go/fx のように reflection を使って自動的に依存性注入を行うヘルパー関数が提供される予定です。

おわりに

今回は samber/do という Go で DI を実現するモジュールについて紹介をしました。

Go は他の言語に比べて DI が採用されるケースが少ない印象です。もちろんDIは銀の弾丸でも、唯一の答えでもなく、採用するかどうかはプロジェクトごとに判断が必要です。

あくまで個人的な意見ですが、今回紹介した samber/do は比較的 Go の思想に寄り添った形で無理なく DI を導入できるバランスのいいモジュールだと思います。

今回の記事がみなさんの参考になれば幸いです!

新たなチームメンバへの贈り物

こんにちは、トモニテ開発部の Android エンジニアです。
この記事は every Tech Blog Advent Calendar 2023 の 5 日目です。

最近、 Android エンジニアに新たなメンバが増えました。
こんなこともあろうかと作っておいた贈り物としてドキュメントがありますので、どんなものか紹介します。

どんなドキュメントなのか

GitHub にあるプロダクトのリポジトリの Wiki に、

  • アプリの構成
  • 開発の前に
  • 開発時の Q&A
  • File Templates

をまとめたものです。

アプリがどんな作りなのか?
開発するにあたり守るべきことはあるか?
xxx をやりたいときはどうすればよいか?
を知り、今後の開発作業での詰まりポイントを減らしていきたい、という思いから作成しました。

ドキュメントの内容 1: アプリの構成

自分自身、まずはアプリの歩き方を知りたくなるので、アプリの地図たる構成をまとめました。
アプリはマルチモジュールで作成していたため、どこにどんなモジュールがあるのかを列挙しています。

一部抜粋するとこんな感じです。

presentation: プレゼンテーション層
┣━ common: プレゼンテーション層で使う便利な処理置き場
┣━ feature: ユーザ向けの機能置き場
┃ ┣━ home: ホーム
┃ ┣━ article: 記事詳細

ここでは他にも、モジュール間の依存関係や各モジュールの役割も記載し、全体像を把握してもらえるようにしています。

ドキュメントの内容 2: 開発の前に

開発作業を進めるにあたり基本的な情報となるものをまとめています。
内容は、

  • ブランチの命名ルール
  • アプリ起動のルート(通常のアプリ起動、Push 通知による Notification 経由、Scheme 起動)ごとのエントリポイントの明記
  • ライブラリは Version catalog でまとめていること
  • 後述の File Templates を使うと楽になる実装があること

を記載しています。
これからの開発で意識してもらいたいルールや、覚えておいてもらえると役にたつものたちです。

ドキュメントの内容 3: 開発時の Q&A

現状の設計に対して「こうするとやりたいことが実現できます」といった情報をまとめています。
一例としては、

  • 新機能を作りたい
    • 各レイヤーで実装するガワを作成したり DI モジュールへの登録といった流れを記載
  • 他画面へ遷移したい
    • feature モジュール間で依存を持たないようにしているため、遷移を行うための router について記載

などがあります。
機能開発やメンテナンスを容易に行えるようにという目的での記載です。

ドキュメントの内容 4: File Templates

AndroidStudio の Settings にある File and Code Templetes で使っているテンプレートを記載しています。
全社的な AndroidStudio の Settings をエクスポートしたファイルは別途管理されているのですが、こちらはプロダクト固有なので Wiki 中に記載しました。

テンプレートの内容としては、プレゼンテーション層、ドメイン層、データ層で主に実装することになるインターフェースやクラスのガワを出力するというものです。
実装してほしい箇所を todo として出力しているので、そこを埋めるだけで他と同様の作りにできます。
これにより、実装箇所のガイドに沿って作業を進めるだけで自然と同じようなメンテナンスをするだけで OK となる作りを広げることができます。

運用してみてどうだったか

ドキュメントを読んだ新メンバに感想を聞いたところ、こちらの期待した通りの役立ち方をしていたようでした。
わかってる人が書いた資料なのでメモを取るよりも正確であり、かつそれを自身の確認するペースで読み込める点が良かったとのことです。

最後に

ドキュメントの内容を細かく記載していくとその時の情報としては正しく、あると嬉しいものなのですが、内容のメンテナンスを怠ると嘘つきの書になってしまいます。
定期的に更新タイミングを設けていき、新たなメンバが早めに真価を発揮できる環境を維持していきたいです。

Playwrightを活用したE2Eテストの導入

Playwrightを活用したE2Eテストの導入

はじめに

この記事は、every Tech Blog Advent Calendar 2023 の4日目の記事です。

tech.every.tv

はじめまして。 株式会社エブリー DELISH KITCHEN 開発本部の羽馬(@NaokiHaba )と申します。

今回は、簡単なハンズオンを通して、Playwrightの基本的な使い方を紹介していきます。

実装したソースコードは 以下のレポジトリで公開していますので、興味のある方はご覧ください。

github.com

想定読者

この記事では、以下のような方を想定しています。

  • playwrightを触ってみたい方。
  • E2Eテストを導入したい方。

ハンズオンの前提条件

この記事を読む前に、以下の準備をお願いします。

  • Node.jsのセットアップ
    • お済みでない方は、こちらを参考にNode.jsをインストールしてください。
  • GitHubアカウントの作成
    • GitHub のアカウントをお持ちでない方は、こちら からアカウントを作成してください。
  • GitHubリポジトリの作成
    • お済みでない方は、こちら を参考に任意のリポジトリを作成してください。

この記事で得られるもの

この記事を読むことで、Playwrightを使ったE2Eテストの導入ができるようになることを目指します。

実行環境

  • Next.js v14.0.3
  • playwright v1.40.1
  • Mac OS Sonoma v14.1.1

Playwrightを活用したE2Eテストの導入

Playwrightとは

github.com

  • Microsoftが開発したE2Eテストツール
  • Chromium、WebKit、Firefoxを含むすべての最新のレンダリングエンジンをサポートしているNode.jsベースのライブラリ
  • PuppeteerとPlaywrightはほとんど同じチームによって開発されている

以下のブログでPuppeteerとPlaywrightの比較がまとめられていますので、興味がある方はご覧ください。

blog.logrocket.com

Playwrightの特徴的な機能

Test generator

codegenコマンドを使用してテストジェネレータを実行し、その後にテストを生成したいウェブサイトのURLを入力します。

URLなしでコマンドを実行し、代わりにブラウザウィンドウに直接URLを追加することもできます。

$ pnpm exec playwright codegen demo.playwright.dev/todomvc

この画面で任意の操作を行うと、テストコードが自動的に生成されます。

使用してみた感想としては、テストコードを書いたことがない方でも、この機能を使えばテストコードを自動生成できるので、テストコードを書くハードルが下がるのではないかと思います。

UI Mode

Playwright v1.32.0 から、UIモードが追加されました。

UIモードは テストを実行したり、デバッグするための機能を提供しています。

$ pnpm exec playwright test --ui

起動に成功すると、以下のような画面が表示されます。

ここからは、使ってみてこの機能が便利だと感じた点を紹介していきます。

Watch mode

テストコードの変更を検知して、自動的にテストを実行してくれます。

テストコードを修正して、実行結果を確認するという作業を繰り返す際に便利です。

定義したアクションごとのスナップショット

テストコードを実行すると、定義したアクションごとにスナップショットが作成されます。

どのタイミングでテストが失敗したのか・どのような操作を行ったのかなどを確認する際に便利です。

他にも、便利な機能が多数ありますので、 詳しくは、公式ドキュメント を参考にしてください。

ハンズオン

ここからは、PlaywrightをNext.jsに導入してE2Eテストを実装していきます。

あくまで、一例としてNext.jsを利用していますが、その他のフレームワークでも同様の手順で導入できると思います。

Next.jsをセットアップする

Next.jsのセットアップ方法は、こちらを参考にしてください。

ここでは詳細な手順は割愛しますが、 今回は ~/Documents に Next.jsをインストールしています。

# 任意のディレクトリに移動してください
$ cd ~/Documents

# プロジェクト名は任意のものを指定してください
# ここでは、playwright-next-app-sample というプロジェクト名で作成しています
$ npx create-next-app@latest

$ cd playwright-next-app-sample

# pnpm を利用していますが、npm や yarn・bunでも問題ありません。お好きなものを利用してください
$ pnpm dev

http://localhost:3000/ にアクセスして、以下のような画面が表示されれば成功です。

Playwrightをセットアップする

Playwrightをセットアップするには、以下のコマンドを実行します。

詳しくは、公式ドキュメント を参考にしてください。

$ pnpm create playwright

Choose between TypeScript or JavaScript (default is TypeScript) # TypeScript を選択
Name of your Tests folder (default is tests or e2e if you already have a tests folder in your project) # 任意のフォルダ名を入力 (今回は tests を入力)
Add a GitHub Actions workflow to easily run tests on CI
Install Playwright browsers (default is true) # true を選択

pnpm create playwright を実行すると、以下のようなディレクトリ構成が作成されます。

- tests
- tests-example
- playwright.config.ts

最後に、テストを実行して以下のような結果が表示されれば成功です。

$ pnpm exec playwright test

➜  playwright-next-app-sample git:(main) ✗     pnpm exec playwright test

Running 6 tests using 5 workers
  6 passed (4.1s)

To open last HTML report run:
  pnpm exec playwright show-report

実行結果は、playwright-report ディレクトリに保存されます。

pnpm exec playwright show-report を実行すると、実行結果をブラウザで確認できます。

$ pnpm exec playwright show-report

Next.jsのサンプルアプリケーションを起動する

今回は、Next.jsのサンプルアプリケーションを利用してテストを実装していきます。

テストコードを実装する前に、Next.jsのサンプルアプリケーションを起動しておいてください。

# Next.jsをローカルで起動
$ pnpm run dev

テストコードの作成

tests/example.spec.ts に以下のテストコードを記述します。

// example.spec.ts
import { expect, test } from '@playwright/test';

// テストコードの実行前にTOPページにアクセスする
test.beforeEach(async ({page}) => {
    await page.goto('http://localhost:3000');
});

test('Get started by editing src/app/page.tsx が表示される', async ({page}) => {
    await expect(page.getByRole('main')).toContainText('Get started by editing src/app/page.tsx');
})

test('Docページに遷移できる', async ({page}) => {
    const page7Promise = page.waitForEvent('popup');
    await page.getByRole('link', {name: 'Docs -> Find in-depth'}).click();
    const page7 = await page7Promise;
    await expect(page7.getByRole('heading', {name: 'Introduction'})).toBeVisible();
})

テストの実行

UIモードを利用してテストを実行していきます。

$ pnpm exec playwright test --ui

テストの実行結果は、以下のようになります。

コマンドライン上でテストの実行結果を確認することもできます。

$ pnpm exec playwright test

Running 6 tests using 5 workers
  6 passed (4.2s)

To open last HTML report run:

  npx playwright show-report

GitHub Actionsでテストを実行する

ここからは、オマケとして GitHub Actionsを利用したワークフローを実装していきます。

Playwright はセットアップ時に、GitHub Actionsの設定ファイルを自動で作成してくれます。

今回は、そのまま利用してテストを実行していきますが、必要に応じてカスタマイズしてください。

name: Playwright Tests
on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18
      - name: Install dependencies
        run: npm install -g pnpm && pnpm install
      - name: Install Playwright Browsers
        run: pnpm exec playwright install --with-deps
      - name: Run Playwright tests
        run: pnpm exec playwright test
      - uses: actions/upload-artifact@v3
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

playwrightの設定ファイルを変更する

playwright.config.ts を編集して、テストの実行前にローカルサーバーを起動するようにします。

複数環境で切り替えたい場合は、環境変数を利用して切り替えることもできます。

// playwright.config.ts

export default defineConfig({
    /* Run your local dev server before starting the tests */
    webServer: {
        command: 'pnpm run dev',
        url: 'http://127.0.0.1:3000',
        reuseExistingServer: !process.env.CI
    }
});

テストの結果を確認する

それでは、ここまでの差分をコミットして、GitHubにプッシュしてください。

GitHub Actionsでテストが実行されていることを確認することができます。

最後に

以上で、Playwrightの基本的な使い方を紹介しました。

Playwrightは、Puppeteerと比較しても遜色ない機能を持っているので、今後はPlaywrightを利用してE2Eテストを実装していきたいと思います。

また、Vscodeに拡張機能が用意されているので、VsCodeユーザーはぜひ利用してみてください。

この記事が、Playwrightを触ってみたい方やE2Eテストを導入したい方の参考になれば幸いです。