AWSソリューションアーキテクト アソシエイトの取り方

筆者について

  • 取得年月:2021/07
  • Score:約780 (720が合格らしいので結構ぎりぎり)
  • 勉強期間:1ヶ月程度
  • 学習のスタンス:GCPの方がシンプルで好みだが、仕事のサービスはAWSで構築されているので仕方なくAWSの学習

事前知識

  • AWSの知見はほぼなし
    • Lambda, S3, Rekognitionは、モバイルアプリ開発で使ったことがある
  • AWS以外のクラウドの知識もほぼなし
  • k8sは、会社の勉強会で輪読していたので、それなりに知っている

学習方法

1. 資格 対策本を2回半読む

book.impress.co.jp

この本を通しで2回読んだ。 1回目は全体像を把握するためにざっくり読んで、2回目はじっくり読んだ。 その後、試験日1週間前に、あまり覚えられていない箇所のみ読んだ。

この本の内容だけで500 ~ 600くらいは取れると思う。

この本は出版日が新しいが、すでに内容が古い箇所があるので注意。自分が気づいた箇所を挙げる。

  • 本では、S3のReadなどは「結果整合性」という扱いだが、「強い一貫性」になっている。
  • EC2のスケジュールドリザーブインスタンスは、新規購入はできなくなっている。同じことをするなら「オンデマンドキャパシティ予約」を使う。

2. AWSのサービス別資料を読む

サービス別資料 | AWS クラウドサービス活用資料集

Youtube/PDFでなく、SlideShareを見た。

仕事で関わりがあるサービスを中心にEC2, Auto Scaling, S3, ECS, API Gateway, SQS, SNS, RDS, Auroraあたりの資料を読んだ。

それぞれのサービスの発展的な機能の説明資料はスルーした。

3. AWS公式のハンズオンを実施

ハンズオン資料 | AWS クラウドサービス活用資料集

実施したハンズオン

  • Security #1 アカウント作成後すぐやるセキュリティ対策
  • Network編#1 AWS上にセキュアなプライベートネットワーク空間を作成する
  • Network編#2 Amazon VPC間およびAmazon VPCとオンプレミスのプライベートネットワーク接続
  • スケーラブルウェブサイト構築編
  • Amazon EC2 Auto Scaling スケーリング基礎編
  • AWS 上で静的な Web サイトを公開しよう!
  • 監視編 サーバーのモニタリングの基本を学ぼう

4. 模擬試験を実施

【SAA-C02版】AWS 認定ソリューションアーキテクト アソシエイト模擬試験問題集(6回分390問) | Udemy

試験は1~6まである。その内、1~4までを実施。 試験日の直前あたりで、間違えた問題の回答を再読した。

この模擬試験は、実際の試験より難しいようなので、試験の正解率が60%台でも合格の可能性はある。自分の場合は、各試験の1回目の正解率は60% ~ 70%台で、2回目で70 ~ 80%台になった。

試験当日

自宅にて、試験官にPCのカメラで監視される形で試験を受けた。 模擬試験と同じように、淡々と問題を解答。

つまづいたこと

  1. 最初は、いつでも受験可能で試験中のWeb検索も仕放題だと思っていた。実際は違う。少なくとも当日いきなり受験はできない。自宅からの試験でも試験官にカメラで監視されるので、Web検索もできない。

  2. 試験の際、顔写真付きの証明書が必要になる。有効な顔写真付き証明書を持っておらず、期限切れのパスポートを作り直した。パスポートの作成期間は、本籍地の戸籍抄本の取寄せを含めると10 ~ 14日くらいはかかるので注意。

雑記

Effective Goのまとめ

Effective Go読んだのでまとめ。 C言語 + オブジェクト指向言語の経験者向け。

Formatting

  • go fmt使え
  • indentはスペースでなくタブ

Commentary

  • 文法としては、C++と同じ
  • トップレベルコメントはJavadoc的な立ち位置。go docで出力する。普通のテキスト扱い。HTML使ったり自前の整形などはしないで、普通のテキストとして記述するのが良い。
  • すべてのパッケージはパッケージコメント、つまりパッケージ節の前にあるブロックコメントを持つべき
  • 関数宣言などにつけるコメントの最初の文は、宣言されている名前から始まる1文の要約であること
  • 変数宣言のグループ化とコメントを組み合わせると、個々の変数でなくまとめてコメントを付けれる

Names

  • パッケージ名
    • シンプルな名前に
      • 名前が衝突しても別名を付けれる
    • 慣習1:小文字でシングルワード
    • 慣習2:ディレクトリ名。src/encoding/base64だとしたら"base64"
    • 長い名前を付けるのではなく、関数のdocを書くほうがしばしば良い
  • Getters/Setter
    • 先頭に"Get"を付けない
    • Setterは"Set"付ける
  • Interface names
    • 1メソッドしかないのものは"-er"にする。Reader/Writerなど。
    • メソッド名が、Write, Close, Flush, Stringなどの標準的なシグネチャと機能に一致しているなら、それらの名前を付ける。そうでないなら、標準的な名前は避ける。
  • Snakecaseでなく、MixedCapsまたはmixedCaps

Semicolons

  • つけなくてOK。自動挿入されるので。
    • セミコロン使うと良いケースは、if err := file.Chmod(0664); err != nil { ... }、というようなエラーハンドリングくらい(筆者注記)

Control structures

  • for/if/switchがある
    • 特筆すべきはswitch
  • switch
    • caseには、定数以外も指定できる。ifで書くような条件文も書ける。変数の型による分岐もできる
    • 特に指定しなければfallthroughはなし
  • Labelを付けて、break

Functions

  • 多値返しできる。 C言語のようにポインタを引数に渡さなくても良い。
  • 戻り値に名前を付けて、変数として値を設定したり取得できる
  • "defer func(args)"で、deferの呼び出し元の関数の終了時にその関数を実行できる
    • 複数deferを記述した場合の関数実行順は、LIFO
    • 引数は即評価される。deferの呼び出し元の関数の終了時ではない。

Data

newによるallocate

  • 戻り値は*T
  • 中身はゼロ値

Constructors and composite literals

  • オブジェクト作成時にゼロ値だと困るときはコンストラクタ作る。名前は"New<型名>"。
  • オブジェクト作成をリテラル形式で書くと実装が簡潔になる
    • new(File)と&File{}は等価
  • C言語と違って、ローカル変数の参照を返せる

makeによるallocate

  • slice/map/channel用
    • これらをnewで作成しないのは、ゼロ値だと困るから。sliceでいえば、array、len、capも初期化したいから。

Arrays

  • C言語と違って値型。 関数呼び出し時、呼び出し元と変更内容を共有したい、値のコピーを避けたいときは、ポインタ型を使う。
  • 要素の型は同じでもサイズが違うなら、別の型扱い

Slices

  • 関数呼び出し時、Arraysと違い、呼び出し元と要素の変更は共有される。内部的には配列のポインタを保持しているため。
  • 関数呼び出し時、呼び出した関数の中でサイズを変更する場合は、結果をSliceで返すなどする。内部的には長さは値型として保持されているため。

Maps

  • 等価演算子が実装されている型ならkeyに使える
  • 存在しないkeyを参照するとゼロ値が返る
  • 存在チェックはをするなら"v, ok = map[key]"。okがtrueなら存在するkey。
  • 要素削除はdelete(map, key)。要素は存在しなくてもOK

Printing

正直あまり覚える必要ないと思う。

  • Printfなどで使う%vは重要。
    • Printlnは%vで値を表示してくれるっぽい。
    • %vは、値のデフォルトの文字列を返す
    • %+vは、構造体のフィールド名も出力
    • %#vは、go syntaxとして値を出力
  • %xは、文字列や配列にも使える

Append

  • sliceに要素追加:append(slice, a, b, c)
  • sliceにsliceを追加:append(slice, ...slice2)。"..."はリストを展開する意味。可変長引数にarrayやsliceを渡すとき使う。

Initialization

Constants

Variables

  • 初期化のタイミングは実行時

Init Functions

  • 変数宣言の初期化子を評価された後に実行
  • 使い所
    • 変数宣言では設定しきれない値の初期化
    • 状態の正しさを保証する

Methods

  • 値メソッドはポインタと値に対して呼び出すことができる
  • ポインタメソッドはポインタに対してのみ呼び出すことができる

Interfaces and other types

  • 型は複数のインタフェースを実装することができる
  • 型変換の方法
    1. 型switch
    2. value.(typeName)を使う
      • 具体例:"str, ok := value.(string)"。戻り値1つしか受けらないと変換失敗時にpanic
  • あるinterfaceを実装している型について、そのinterfaceとして使わないなら、コンストラクタの戻り値の型は、そのinterface型にする
  • ほとんど全てのものがメソッドを持つことができる
    • 構造体、組み込み型、関数など

The blank identifier

"_"の使いどころ。

  • 多値返しの関数の結果を受けるときに、値を捨てる
  • 開発段階で、パッケージの未使用importによるエラーを避ける
    • "var _ = fmt.Printf"のように書くと、fmt未使用のエラーを回避できる
    • この記述は、imoprt文の直後に置くのが慣習
  • パッケージのinit関数を実行させるため(そのinit関数による副作用が必要なとき)
  • ある構造体が特定のインターフェースを実装しているかチェックする
    • トップレベルで、"var _ json.Marshaler = (RawMessage)(nil)"のように書くと、RawMessageがjson.Marshalerを実装しているかチェックできる。
    • コード内に静的変換が存在しない場合にのみ使用する

Embedding

type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}

このように書くと、ReadWriterは、ReaderWriterからReadやWriteを直接呼べる。

良い点

  • 内側の構造体のメンバへのアクセスが簡潔にできる
  • 内側の構造体が実装しているinterfaceを引き継げる
    • 例でいえば、ReadWriterはReaderとWriterの両方のinterfaceを実装している扱い

メンバ名が被った時のルール

  • 外側のメンバで上書き
  • 同じ階層のメンバ名が被るとエラー。ただし、そのメンバに参照しなければエラーにならない。

Concurrency

Share by communicating

基本:メモリ上での値の共有は避け、通信によって値を共有する

(Mutex避けるということ)

Goroutines

  • 複数のOSスレッドに多重化されているので、I/Oを待っている間などに1つのスレッドがブロックされても、他のスレッドは実行され続ける
  • "go func()"のように書く。func()の実行完了は待たないで次に進む。
  • 関数リテラルを使うと便利

Channels

  • makeで作る
  • 使い方はいろいろ。たとえば、サーバのリクエストの処理数の制限。

var sem = make(chan int, MaxOutstanding)

func Serve(queue chan *Request) {
    for req := range queue {
        req := req // Create new instance of req for the goroutine.
        sem <- 1
        go func() {
            process(req)
            <-sem
        }()
    }
}

req := reqは重要。これを忘れると、全てのgoroutineから見えるreqは、最新のreqの値になってしまう。ループ変数reqのスコープは、for文の1イテレーションではなくfor文終わるまでだから。

Channels of channels

  • channelにchannelをもたせることもできる

Parallelization

Channelを使うと並列処理できる。

const numCPU = runtime.NumCPU()

func (v Vector) DoAll(u Vector) {
    c := make(chan int, numCPU)  // Buffering optional but sensible.
    for i := 0; i < numCPU; i++ {
        go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
    }
    // Drain the channel.
    for i := 0; i < numCPU; i++ {
        <-c    // wait for one task to complete
    }
    // All done.
}

runtime.NumCPU()の代わりにruntime.GOMAXPROCSというのも使える。これは、 実装者が指定したプログラムが同時に実行できるコア数。デフォルト値はruntime.NumCPU()。

A leaky buffer

Channelを使うと並列処理と関係ないリソース管理にも使えるよ、という話。

Errors

基本

  • 関数の多値返しを利用してエラーを返す + チェックする
  • エラー型には、組み込みinterfaceのerrorを実装させる
  • 可能であれば、エラー文字列は、エラーを発生させた操作やパッケージを示す接頭辞を持つなどして、その発生源が分かるようにする

panic

  • 回復不能なエラーのときに使う。初期化処理失敗時など。
  • panic時の挙動
    • 関数の戻り値としてnilが設定される
    • goroutineのスタックの巻き戻しが行われる。deferで指定した関数が呼ばれる

recover

  • panicから復旧させるときに使う。これをしないとプログラムはクラッシュする。
  • deferとセットで使う
  • 用途
    • 複数gorouttine を実行していて、問題が起きたgoroutineだけシャットダウンする
    • エラー処理を簡潔に書く
  • recover後の再度panicさせると、元のpanic要因と再panic要因の両方がクラッシュレポートに含まれる

A web server

"html/template"や"net/http"を使うと、Webサーバ作ったり、HTMLテンプレートが簡単に使える、というだけの話。

感想

他のEffective Xと違って、「Go言語入門」の一部。A Tour of Goの次に読んだ方が良い。

これまで読んだソフトウェア関連で良かった本

実装系

Effective C++

www.maruzen-publishing.co.jp

Effective XXXの元祖?。 コードスタイルの観点だけでなく、ありがちなミスの予防、パフォーマンス、設計観点のプラクティスが多いのが良い。

Effective Java

www.maruzen-publishing.co.jp

Effective C++Java版。 こちらもコードスタイル以外の観点でプラクティスを挙げてくれているのが良い。

Java言語で学ぶデザインパターン入門

www.hyuki.com

最初にデザパタを学んだ本。他のデザパタの本よりとっつきやすい。今もしばしば見る。

プログラミングGauche

www.oreilly.co.jp

Gauche関数型言語Schemeの処理系の一種。関数型言語に初めてにして唯一学んだもの。 Schemeに限らず、関数型言語を1つは体験しておくのは、おすすめできる。

数列の漸化式を立てるような感覚で、コーディングできるのは新鮮な体験だった。 マージソートC言語などで書くよりも簡潔に分かりやすく実装できる。 可変オブジェクトよりも不変オブジェクトを使った方が堅牢なつくりにできるというのもSchemeで学んだ。

この本読んでしばらくは、再帰関数で実装しないと気が済まない病ににかかっていたこと。 (関数型言語再帰処理に向けた最適化が行われているが、そうでない言語ではそうでないし、StackOverflowの問題がある)

CODE COMPLETE

shop.nikkeibp.co.jp

古典。いろいろなプラクティスを学んだ。 この本で紹介されていた、実装前に擬似コードを書くテクニックは今も使っている。

Pthreadsプログラミング

www.oreilly.co.jp

C言語のスレッドを扱うライブラリpTthreadsの本・ ライブラリ自体よりも並列処理の基礎的な考えやパターンを学べたのが良かった。

アルゴリズムクイックリファレンス

www.oreilly.co.jp

難易度が丁度良かった。「アルゴリズム入門」系の本よりは、種類も多いし、理屈の説明もしっかりしている気がする。 ダイクストラ法などの基礎的なグラフアルゴリズムは、この本で学んだ。

モダンC言語プログラミング

www.kadokawa.co.jp

「C言語でもオブジェクト指向できるから!」という本。 ボイラーコードを書くのが面倒なものの、確かにC言語でも普通にオブジェクト指向プログラミングできる。 「C++使えるけど、他の人達がC++覚えられないのでC使え!」という開発案件では世話になった。 (今になってみると、Cしか使えない人たち向けのソースなのに、オブジェクト指向使うのは嫌がらせ感あるw)

レガシーコード改善ガイド

www.shoeisha.co.jp

「テストがないコード = レガシーコード」という考えの本。 テスト可能な設計になっていない既存コードに、少しつづテスト追加 + リファクタリングをしていく手法が満載。 レガシーコードを題材にしたテスト駆動開発といった印象。 既存コードがテスト可能な設計になっていないので、「自動テスト書けません」、「いきなり理想形に作り直します」的な人に読んで欲しい本。

レガシーコードのメンテナンスばかりで鬱屈している人を励ます言葉が書かれており、そこには少し感動した。

レガシーコードからの脱却

www.oreilly.co.jp

「レガシーコードが生まれる仕組みが分かっていないのに、ソフトを作り直しても別のレガシーコードが生まれるだけだ」という主張には同意。 すぐに作り直す病がある人達に読んで欲しい本。

上の本はコード寄りだが、こちらは開発プロセス寄りの本。

集合知プログラミング

www.oreilly.co.jp

ベイズフィルタによる迷惑メールフィルタ、Webクローラーなどを実装していく本。 もう賞味期限切れだと思うが、発売当時は、機械学習がブームになる前でかなり面白かった。

設計系

ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本

www.shoeisha.co.jp

ドメイン駆動の話は読んだり聞いていて眠くなることが多いが、この本はそうではなく最後まで読めた。 これまで自分の仕事ではドメイン駆動関係なかったが、それでもレイヤーごとの責務の割り当て方はとても参考になった。 業務ルールをソースコードに反映させる部分の話がないので、この本のことだけ実践してもドメイン駆動設計にならないと思うが。

ユースケース駆動開発実践ガイド

www.shoeisha.co.jp

要求から実装に落とし込むまでのプロセスが生前としていると思った。 一番参考になったのは、要求からユースーケースを記述する方法。簡素で分かりやすいと思った。

Clean Architecture 達人に学ぶソフトウェアの構造と設計

www.kadokawa.co.jp

いろいろな人が語っているので語ることのない本。

「1970年代だか1980年代のコンピュータエンジニアが現代にタイムスリップしても、すぐに適応できるだろう」という旨の部分はそうだと思った。 移り変わり早いように見えて、インターネットの普及、マシンリソースがリッチになったこと以外は、このころと根本は変わっていない気がする。

現実で、この玉葱の図の通りにレイヤーを分ける = クリーンアーキテクチャと言われると、疑問を感じることがある。

モノリスからマイクロサービスへ

www.oreilly.co.jp

既存サービスに劇的な影響を与えないで、逐次的にサービスを分解していく方法が書かれている本。 ただただ、感心した。根本的な考えは、既存ソースコードを捨てないでリファクタリングしていくのに似ている。 過去に「コード書くのが上手い人は、アーキテクトの資質がある」という主張を聞いたことがあるが、そのとおりだと思った。

開発プロセス

ソフトウェア見積り 人月の暗黙知を解き明かす

www.amazon.co.jp

いろいろな見積もり技法が載っている本。複数人見積もり、最悪・普通・最良の3点見積もりなど。

3点見積もりは手軽でおすすめできる。 なぜ、これが良いかというと、1点見積もりをすると、最良ケースで見積もりする傾向が多いから。

過去実績工数を使った単純な技法が、COCOMOのような数式を使った見積もり技法より劣るわけでないことを知れたのもよかった。 過去実績工数を使った見積もりは組織の文化やプロセスも暗黙のうちに考慮されるが、数式を使うタイプの技法はそうでないから。

他にも、完全にあてずっぽう見積もりよりも、何か仮定を起き、それが正しいとして定量的な見積もりを行う方が良いという話も良かった。

アジャイルサムライ

shop.ohmsha.co.jp

いろいろな人が語っているので語ることのない本。語り口が面白い。

カイゼン・ジャーニー

www.shoeisha.co.jp

SIerにいた頃に読んで、プロセス改善の行動を起こすことに勇気をくれた本。 基本は、スクラムの紹介が多い。カンバン、ドラッカー風エクササイズあたりはとても参考になった。

カンバン仕事術

www.oreilly.co.jp

カンバンの本。タスクボード ≠ カンバン。大まかには以下の流れ。スクラムよりもゆるくて良い。五月雨式に開発項目が降ってくる環境にも向いている。

  1. ボードと付箋を使って仕事を見えるか
  2. WIP(仕掛り作業の数の制限)を設定したり、ルールの調整
  3. 開発プロセスボトルネックを見つける
  4. 改善策を考える&実施
  5. サイクルタイム、リードタイムなどを見て効果があったか考える
  6. 1に戻る

SIerの頃、この本の影響で物理ボードを使っていたが、電子ボードよりすぐに見れるのがとても良かった(コロナのおかげで物理ボード使う機会は当面なさそうだが)。

教養

Unix/Linuxプログラミング理論と実践

asciimw.jp

雰囲気で使っていたUnix/Linuxのことが一段と深く理解できた。特にパイプやソケット。

C言語ポインタ完全制覇

gihyo.jp

ポインタについて、とても詳しいです(^q^)。C言語使っているなら、入門書の次に読むのおすすめ。 自分が読んだのは2006年度版だが、2017年に新しい版が出ている。

コンピュータの構成と設計

www.nikkeibp.co.jp

CPU + アセンブラ目線でのコンピュータの仕組みが分かる本。 これを読んでおくと、いろいろなことの理屈が想像できるようになる。 (具体的には、関数呼び出しが重いこと、プリミティブ型は参照ではなく値渡しの方が早いこと、割り算は遅い、バッファローオーバーフローの仕組みなど)

マスタリングTCP/IP 入門編

www.ohmsha.co.jp

TCP/IPプロトコルの解説本。 今どきスタンドアローンプログラムはなく、ほぼ通信する。であれば、どういう仕組みで通信が行われているのかは理解しておいて損はない。

リファクタリング・ウェットウェア

www.oreilly.co.jp

マインドマップの使い方、集中の仕方が参考になった。特にマインドマップを使う読書術。

オブジェクト指向UIデザイン──使いやすいソフトウェアの原理

gihyo.jp

書いてあることは目新しくないが、「オブジェクト指向デザイン」「タスク志向デザイン」という言葉を作ってくれたのが良かった。

  • オブジェクト志向デザイン
    • 操作対象を選ぶ -> 何か操作するという流れのUI
    • 概ねのソフトのUIはこれ
  • タスク志向デザイン
    • 操作する -> 操作する -> 操作するというUI
    • 最後になるまで実行結果が想像しづらいので、あまり良くない
    • 典型はインストールウィザード
    • あえてこれにする必要はない

ライト、ついてますか―問題発見の人間学

honto.jp

あるビルのエレベータが遅くて渋滞ができてしまう問題の解決方法がすごく良い。 エレベーターの基数を増やすわけでもなく、スピードを上げる訳でもない別の方法。 技術以外の解決方法を考えることも重要ということを実感。

ビジョナリー・カンパニー2 飛躍の法則

shop.nikkeibp.co.jp

劇的な飛躍を求めるのではなく改善のサイクルをこつこつ回していくことの重要さを教えてくれた。

モバイルアプリのE2Eテストを運用して1年たった

男坂 Advent Calendar 2020 - Qiita 14日目。

仕事でモバイルアプリのE2Eテストを運用し始めて1年と少しが経過したので、テストコードの書き方、運用方法、感想、失敗したことを共有してみる。

背景

E2Eテスト導入前の状況

元々、開発対象のモバイルアプリにテストコードが一切なく、テストは全て手動で実施していた。また、正直、アプリの作りも雑でバグが多かった。

E2Eテストを導入した理由

手動でテストを実施するのは面倒だ。そして面倒だからこそ動作確認に手を抜いてしまいバグが多くなると、考えた。ということで、自動テストを追加して、それらの問題を解決しようという話になった。

普通であれば、Unit Testから充実させていく作戦を取ることが多いと思う。Unit Testの方が運用コストが低いからだ。テストピラミッドという考えでもUnit Testが自動テストの基礎でもある。

ただ、アプリのコードがテスト可能な実装になっておらず、Unit Testを書くためには大掛かりなリファクタリングが必須だった。そのため、まずは、E2Eテストで重要機能の動作を保証し、保証できた後にUnit Testを充実させる作戦にした。

E2Eのテストケースの選別

元々、手動で実施していたリリース前の動作確認項目をそのままテストケースにした。ただし、自動テストにしても安定性が低そうだったり、自動テストの実装難易度が高いものは自動化の対象外にした(プッシュ通知を開く、パスコードロック解除など)。

E2Eテストの書き方

AndroidiOSの両方でE2Eテストを導入した。

OS共通

テストコードにもデザインパターンがいろいろあるようだが、PageObjectパターンを採用した。テストコードを見たとき、どの画面のどの操作をしているのかが簡単に読み取れるようになるため。

テストフレームワークには、Espresso&XCTestを採用した。appiumのようなテストフレームワークを採用しなかったのは、プロダクト開発と自動テストを書くのが兼任だったため。プロダクト開発のノウハウがそのまま活かせるEspresso&XCTestの方が優位と判断した。

実行速度よりは、冪等性の担保、安定性、実行環境への依存性の少なさを重視した。テストコードで固定値sleepをするのは、実行速度にも安定性にも難があるので、極力避けた。

Android

Espressoを使ってテストコードを記述した。Android Studioから手動操作を記録してEspressoを使ったテストコードを生成できるのでそれを利用していた。生成したテストコードはそのまま使った訳ではなく、可読性や安定性が上がるように調整したり、端末依存性がなくなるように調整した。また、Page Objectパターンに当てはまっていないので、Page Objectパターンになるようにリファクタリングもした。

iOSと違い、手動操作からテストコードを生成する機能の完成度が高いので、テストケースを書くのは格段に楽だった。

各テストケース実行時には、Android Test Orchestratorで設定値を毎回クリアする設定にしていた。この設定のおかげで、テストケースの冪等性を担保しやすくなった。

通信などの非同期処理は、IdlingResourceで完了まで待機するようにしていた。IdlingResouceで処理を待機するのであれば、あまりWait処理はいらなくなるので。基本的には、非同期処理1つ1つに仕込みを入れるのではなく、RxであればRxJavaPlugins.setScheduleHandler、Coroutineであれば自作CoroutineContextを使うことで、全体的にIdlingResourceを設定していた。

iOS

XCTestで自動テストを実現した。Androidと違って、手動操作からのテストコード生成がまともにできなかったので、最初からコードをごりごり書いていた。

Androidと同じく各テストケース実行時には設定値を毎回クリアしていた。Android Test Orchestratorのような仕組みもないので、プロダクトコード側に独自の仕込みを入れて、設定値クリアを実現した。

IdlingResouceのような非同期処理の待ち合わせの仕組みはないので、タイムアウト値付きで特定の条件が整理するまでwaitさせる処理を自分で実装していた。

Androidよりもテストコードを実装する難易度が1, 2段高い。安定性の面でもAndroidよりも低い。

E2Eテストの運用方法

テスト実行環境

CIにて、特定のブランチにプッシュしたときや定刻になったときに、E2Eテストを実行させていた。Device FarmにはFirebase Test Labを採用した。採用理由は価格の安さ。

E2Eテストの実行パターン

主なE2Eテスト実行パターンは3種類にしていた(ブランチ戦略はGit Flow採用)。

  1. リリース前動作確認として
  2. 実行トリガーは、masterにpushされたとき
  3. 安定性が高いテストケースのみ
  4. ソフト構成は、なるべくRelease版に近いアプリ + 公開サーバ
  5. OSは最新OS、最新1つ前のOS、一番使われているOSを使用
  6. バイスは仮想デバイスと実機
  7. デグレチェックとして
  8. 実行トリガーは、developにpushされたとき
  9. 安定性が高いテストケースのみ
  10. ソフト構成は、Debug版に近いアプリ + 開発用サーバ
  11. OSは最新OS、最新1つ前のOS
  12. バイスは仮想デバイスのみ
    • 実行回数が多いのでコストを抑えるのが目的
  13. E2Eの各テストケースのヘルスチェックとして
  14. 実行トリガーは、毎日定刻になったら
  15. 全テストケースを実行
  16. その他の実行条件はデグレチェックのE2Eテストと同じ

特筆すべきなのは、3番目のヘルスチェックとしてのE2Eテスト。これは他の人にも自信を持っておすすめできる。これで各テストケースの安定性を見て、安定性が高いものだけをデグレチェックやリリース前の動作確認としてのE2Eテストに含めていた。こうすることで、E2Eテストが失敗したまま放置する状況を簡単に回避できる。また、E2Eのテストケースが失敗したときに、いつも失敗しているのか、偶発的に失敗したのかの判断を容易にする役割もある。

7~10日間、連続成功したら「安定している」と判断していた。一度安定していると判断したテストケースでも毎回失敗するようになったら、デグレチェックやリリース前の動作確認としてのE2Eテストの実行対象からは外していた。

追加したばかりのテストケースは、不安定なテストケース扱い。最初は、ヘルスチェックE2Eテストでしか実行させない。

E2Eテスト失敗時の扱い

通信やサーバ側起因の偶発的な失敗がかなり多い。それを踏まえて、E2Eテストが失敗したときは、以下のように対処していた。

  • 失敗のログなど見ないで、1回テストを再実行してPassするならそれでOKとする
  • E2Eテストが失敗しても、アプリのビルド&デプロイはする
  • E2Eテストを再実行して問題が起きる場合でも、手動で動作確認して問題ないならOKとする
  • テストケースの安定性が低いと判明した時点で、原因調査&対処 または リリース前動作確認やデグレチェックのE2Eテストからは外す

E2Eテストの感想

良かった点

きつい点

やはり運用コストが高い。

体感で月1くらいでE2Eテストが壊れる。要因はUIの変更、OSのバージョンアップ、ライブラリバージョンの変更、ビルド方法の変更など。

E2Eテストが壊れた原因が難解なこともしばしばある。まれにFirebase Test Labでipaのバンドル名が勝手に変更されてテスト失敗、しばしばAndroid 10から、WebViewの初期読み込みがかなり遅くなってテスト失敗など。

壊れる頻度 + 壊れた原因の調査の難易度が高いことから、プロダクト開発と並行してE2Eテストのメンテナンスはかなりきつかった。

Androidの方をメンテナンスするだけで精一杯で、iOSの方はメンテナンスしきれず、E2Eテストは壊れたまま放置した状態になってしまった。

べからず集

  • E2Eテストのメンテナンスを1人でやること
    • テストを壊す人は複数人にいるのに対して、修正する人が1人なのはきつい
    • その1人がメンテナンスやめたらすぐに廃れる
  • むやみやたらにE2Eのテストケースを作る
    • 単にテストケースを実装するのは楽だが、安定性を維持し続けるコストは高い
    • Unit Tesと違い、1つのテストケースの実行速度が遅いというのもある。テストのフィードバックは早く返すべき。遅いとテストを無視するようになる。
  • E2Eテスト失敗時に、アプリのビルド&デプロイをしないこと
    • アプリ外の要因でE2Eテストが失敗し続けることもあり、そういうときにビルド&デプロイしてくれないと、手動でビルド&デプロイする羽目になる
    • 誤検出も多いので、E2Eテスト失敗してもとりあえずデプロイまではして良い(何も調査しないでアプリを公開するのはNG)
  • E2Eテストの充実とプロダクト開発を兼任ですること(オーバーワークしない前提)
    • 既存のE2Eテストの維持 + プロダクト開発でもしんどい。E2Eテストの充実とプロダクト開発の兼任はなおさら無理。