DeNA Testing Blog

Make Testing Fun, Smart, and Delighting End-Users

Isabelle/Isar勉強会を社内で開催しました

こんにちは、SWETグループの鈴木穂高(@hoddy3190)です。

SWETグループのメンバー向けにIsabelle/Isar勉強会を開催しました。本記事では、勉強会の概要の紹介と、勉強会の資料の公開をします。

もしよろしければご活用ください。

Isabelleとは

Isabelleは、定理証明支援ツールの1つです。 数学の授業で証明を解く時、暗記した定義や定義を引き出して仮定や結論をみて試行錯誤しながら適用して解いていっていたと思います。Isabelleを使うと、利用できる定理を提示してくれたり、自動で定理を適用して証明を進めてくれたり、誤った証明を指摘してくれたりします。

Isabelleの大きな特徴として強力な自動証明が挙げられます。自動証明機能の典型例であるSledgehammerは、証明したい論理式を与えれば自動で定理を適用して証明を解き進めてくれます。 公理系も選択可能です。よく使われるのはHOLですが、他にもFOLやZFなどを扱えます。 Isabelleの実績として、オペレーティングシステムの機能に関する証明に使われました。1

Isabelleには証明の記述方法が2種類あります。apply-scriptsとIsarです。 今回の勉強会で用いているのはIsarです。Isarは今何を証明しているのかを宣言して証明を書くためapply-scriptsより読みやすいという特徴があります。

業務でのIsabelleの用途

仕様と実装に正当性関係が成り立つことを証明するのに使えます。 仕様と実装の正当性関係とは、実装が仕様を満たしているかどうかの関係のことを言います。

私はSWETグループにて仕様の欠陥をなるべく早くみつける取り組みにチャレンジしています。 詳細はこちらの記事の「発表内容」のスライド資料または動画をご覧いただければと思いますが、簡単にいうと仕様を形式仕様記述するというアプローチで取り組んでいます。

正当性関係が成り立っていることは論理式で表せますので、仕様と実装が形式的に書かれていれば、実装が仕様を満たしているかをIsabelleのような定理証明支援ツールを使って証明ができます。私はOCamlで記述した仕様と実装をIsabelleが読める形にトランスパイルして、正当性関係が成り立つことを証明しました。

Isabelle/Isar勉強会の概要

正当性関係が成り立っているとはどういうことなのかは、Isabelleを使って証明を進めることで理解が深まります。 さらには証明の手順が分かると、正当性関係が成り立つことを証明されるのを見越して、仕様はこう書けばよい、仕様に対して実装はこう書けばよいといった勘所も養われます。 このようなメリットのため、SWET内でIsabelle/Isarを学ぶための勉強会を開催しました。

勉強会は毎週1時間リモートミーティングで開催しました。 参加メンバーの人数は5,6人です。 参加メンバー全員がIsabelleをapply-scriptsで使ったことがある方でした。 実はこの勉強会よりも前に、Isabelleの基本的な使い方や、仕様と実装をIsabelleで記述し正当性関係に関する証明のやり方を学ぶ別の勉強会があり全員そこの参加者でした。 ただ本資料はIsabelle初学者でも分かるように作っています。

1時間の勉強会の内容は、座学と練習問題です。 座学では毎回私が資料をベースに説明をし参加メンバーと質疑応答を行います。 その後用意してある練習問題を参加メンバーに解いてもらいます。 毎回平均して30分ほど座学、残り30分ほどは演習問題を解く時間でした。 時間内に解ききれなかった練習問題は可能であれば別途各自で時間をみつけて次回の勉強会までに解いてもらうようにしました。 勉強会は合計7回開催しました。

参加メンバーからのフィードバックとして次のようなものが得られました:

  • 簡単な定理から徐々に証明していく形だったので、以前の勉強会に付いていけてなくても何とかなったのが良かったです。
  • apply-scriptsのほうが慣れている分楽な気がします。一方、apply-scriptsは確かにIsabelleを動かさないと何をしているのかがわからないですが、Isarだと読むだけでわかりそうでそこはよさそうに思います。

Isabelle/Isar勉強会の資料と練習問題

Isabelleの特徴は先に述べたとおり強力な自動証明ですが、今回の練習問題では自動証明は極力使わないよう解答してもらいました。 基本的な定理を理解した上で、定理を使って少しずつゴールをrefineしていくことがIsabelle/Isarに慣れるためには必須だと思ったからです。 ですから、もし練習問題を解く際は極力自動証明を使わないことをお勧めします。

さいごに

実際に業務で出てくる論理式に対して、今回は初学者向けということもあり例題や練習問題はシンプルです。 焦らず少しずつできることを増やしていってもらえればよいと思います。 本資料や練習問題に興味を持ってくださる方がいたら嬉しいです。

それから、SWETで働きたいエンジニアを絶賛募集中です。ぜひ採用ページをご覧ください!

時間を加味したモデリング

こんにちは、SWETの鈴木穂高(@hoddy3190)です。

現在SWETチームにて仕様の欠陥をなるべく早くみつける取り組みにチャレンジしています。 欠陥をみつけるタイミングが早ければ早いほど、開発中の手戻りに伴うコストを抑えられます。 たとえば、仕様作成フェーズ、実装フェーズ、QAフェーズの順で開発が進んでいくときに、仕様の欠陥が実装フェーズやQAフェーズでみつかると実装やQAのやり直しを引き起こしかねません。 そうした大きな手戻りを抑えるために仕様の欠陥をなるべく仕様作成フェーズでみつけることを目指します。

対象領域に出てくる要素をモデリングすることは、仕様に潜む欠陥を開発の早い段階でみつけるための、有効な手段のひとつです。 要素には、開発者がこれから作るシステムや、そのシステムのユーザー、そのシステムと直接的または間接的に相互作用する外部のシステムが含まれます。 単に図を書くというモデリングの他に、プログラムのように動くモデルを作るという方法もあります。動くモデルを作るとモデリングする過程での気づきや、モデルを動かしてみた時の気づきが後工程での手戻りを減らすことにつながります。

以前私が書いた「仕様記述テクニック『Promotion』の紹介」ではAlloyを使っていましたが、 本記事ではプログラミング言語OCamlをモデリング言語として使って時間を加味したモデルを記述する方法を紹介します。 多くのモデリング対象の振る舞いは時間が関係するため、モデルの記述で時間を扱えると表現の幅が広がります。 なお、OCamlの説明はしません。

OCamlでのモデリング

今回はプロセスをモデリングします。プロセスは対象領域の要素の振る舞いを指す用語として用います。 モデリングのやり方はさまざまあります。今回は対象のプロセスをプロセスが取りうる状態と遷移で表現します。 プロセスの型は次のようにします:

type ('ev, 'ch, 'state) process =
  'state -> (('ev, 'ch, 'state) trans) list

process型で求められる状態の型は次のようにします:

type state =
  | State_A
  | State_B of int

状態変数はコンストラクタの引数を使って表現します。

遷移ラベルには、イベント同期、受信、τを用意します。ここでτという遷移ラベルのついた遷移は、プロセス内部で自動的に遷移します。 遷移の型は次のようにします:

type ('ev, 'ch, 'state) trans =
  | Tau of 'state
  | Ev of ('ev * 'state)
  | Recv of ('ch * ('state, 'ev) guard * ('ev -> 'state))

コンストラクタTauはτを表します。 コンストラクタEvはイベント同期を表します。イベント同期は、チャネルを通じて値をやりとりします。 コンストラクタRecvは受信を表します。受信は、チャネルを通じて値を受け取ります。受信の場合、事後状態は受信した値によって決まります。ガードを満たすかどうかも受信した値によって決まります。 受信によって起きるイベントは、受信した値を使ってイベント同期で表します。

trans型で求められるイベントの型は次のようにします:

type ev =
  | Event_A
  | Ch1 of int

イベント型に呼応してチャネル型を定義します。イベントが属するコンストラクタを表すための型です。 OCamlでは引数をとるコンストラクタそのものを値として使うことができないので、このような構成にしています。 trans型で求められるチャネルの型は次のようにします:

type ch =
  | Ch_Ch1
  | Ch_Ch2

ここまでを踏まえて次の状態遷移図で表されるATMをモデリングしてみましょう。

f:id:swet-blog:20220404134852p:plain

ATMの振る舞いは、カードを受け取り(in)、PINナンバーを受け取り(pin)、PINナンバーをチェックし(check)、引き出す額を受け取り(req)、額分のお金を出し(dispense)、カードを出します(out)。

これをモデリングすると次のようになります:

let atm_process : (ev, ch, atm_state) process = fun s ->
  match s with
  | A_S1 vals ->
      [
        (* in?c *)
        Recv
          ( Ch_In,
            (fun _ _ -> true),
            fun ev ->
              let vals' = make_vals'_1 vals ev in
              A_S2 vals' );
      ]
  | A_S2 vals ->
      [
        (* pin?p *)
        Recv
          ( Ch_Pin,
            (fun _ _ -> true),
            fun ev ->
              let vals' = make_vals'_2 vals ev in
              A_S3 vals' );
      ]
  | A_S3 vals ->
      let vals' = make_vals'_3 vals in
      [ (* check *) Ev (Check, A_S4 vals') ]
  | A_S4 vals ->
      [
        (* req?n *)
        Recv
          ( Ch_Req,
            (fun _ _ -> true),
            fun ev ->
              let vals' = make_vals'_4 vals ev in
              A_S5 vals' );
      ]
  | A_S5 vals ->
      let vals' = make_vals'_5 vals in
      [ (* dispense!n *) Ev (Dispense vals.req, A_S6 vals') ]
  | A_S6 vals ->
      let vals' = make_vals'_6 vals in
      [ (* out!c *) Ev (Out (get_card vals), A_S1 vals') ]

時間遷移の追加

次に、物理的な時間経過を表現できるように表現の枠組みを拡大します。ここでは時間オートマトンという理論にある考え方を使います。 時間オートマトンにおける状態は、ロケーションと時間代入関数の組で表現します。 ロケーションは、今まで「状態」と呼んでいたものです。時間代入関数はクロック変数と時間をマッピングしたものです。クロックは経過した時間を表します。 遷移には2種類あり、あるロケーションから別のロケーションに遷移する離散遷移と、時間経過による時間遷移があります。

時間を表す型を用意します:

type datetime = int

クロック変数の型を用意します:

type clock = T

時間制約式は、時間に関する条件を表した式です。ある時刻までに遷移するといった遷移の条件や、ある時刻を越えてこの状態に留まることはできないといった状態が満たすべき条件を表すのに用いられます。時間制約式は次のようにします:

type ('clock) cons =
  | True
  | Eq of 'clock * datetime
  | Le of 'clock * datetime
  | Ge of 'clock * datetime
  | Lt of 'clock * datetime
  | Gt of 'clock * datetime
  | Not of 'clock cons
  | Or of 'clock cons * 'clock cons
  | And of 'clock cons * 'clock cons

時間代入関数は次のようにします:

type 'clock v = ('clock, datetime) Hashtbl.t

時間代入関数は時間遷移や、クロック変数のリセットを伴う離散遷移で更新されます。

不変述語は状態が満たす時間制約です。不変述語は次のようにします:

type ('state, 'clock) inv = 'state -> ('clock cons)

遷移に時間制約を与えられるようにしたいので、遷移する際に満たすべき時間制約とリセット対象のクロック変数を指定できるようプロセスの型を次のように変更します:

type ('ev, 'ch, 'state, 'clock) process =
  'state ->
  'clock v ->
  ( 'clock cons * (* 遷移する際に満たすべき時間制約式 *)
    'clock list * (* リセットするクロック変数 *)
    ('ev, 'ch, 'state, 'clock) trans ) list

遷移も合わせて変更します:

type ('ev, 'ch, 'state, 'clock) trans =
  | Tau of 'state
  | Ev of ('ev * 'state)
  | Recv of ('ch * ('state, 'clock, 'ev) guard * ('ev -> 'state))

ATMの例(モデリング)

ここまでを踏まえてさきほどのATMの振る舞いに変更を加えたものをモデリングしてみましょう。

f:id:swet-blog:20220324180050p:plain

pinの受け取りに時間が30かかる場合はカードを出す振る舞いを加えました。 これをモデリングすると次のようになります:

let atm_process (ev, ch, atm_state, clock) process = fun s v ->
  match s with
  | A_S1 vals ->
      [
        ( True,
          [ T ],
          (* in?c *)
          Recv
            ( Ch_In,
              (fun _ _ _ -> true),
              fun ev ->
                let vals' = make_vals'_1 vals ev in
                A_S2 vals' ) );
      ]
  | A_S2 vals ->
      [
        ( Lt (T, 30),
          [],
          (* pin?p *)
          Recv
            ( Ch_Pin,
              (fun _ _ _ -> true),
              fun ev ->
                let vals' = make_vals'_2 vals ev in
                A_S3 vals' ) );
        (Eq (T, 30), [], Ev (Out (get_card vals), A_S1 vals));
      ]
  | A_S3 vals ->
      let vals' = make_vals'_3 vals in
      [ (True, [], (* check *) Ev (Check, A_S4 vals')) ]
  | A_S4 vals ->
      [
        ( True,
          [],
          (* req?n *)
          Recv
            ( Ch_Req,
              (fun _ _ _ -> true),
              fun ev ->
                let vals' = make_vals'_4 vals ev in
                A_S5 vals' ) );
      ]
  | A_S5 vals ->
      let vals' = make_vals'_5 vals in
      [ (True, [], (* dispense!n *) Ev (Dispense vals.req, A_S6 vals')) ]
  | A_S6 vals ->
      let vals' = make_vals'_6 vals in
      [ (True, [], (* out!c *) Ev (Out (get_card vals), A_S1 vals')) ]

モデリングしたプロセスをエンジンを使ってREPL上で動かしてみます。 エンジンは、どのイベントが起こるとどの状態になるのかを可視化します。 ひとつ前の状態に戻ったり、遷移の履歴をダンプしたりもできます。 不変述語を満たしているかのチェックもエンジン側で行っています。

f:id:swet-blog:20220324180234g:plain

メニュー[0]を選ぶと時間遷移をします。 メニュー[1]を選ぶとたどってきた状態とイベントをダンプします。 メニュー[2]を選ぶとひとつ前の状態に戻ります(undo)。 メニュー[3]を選ぶとundoで戻った操作をやり直します(redo)。 メニュー[4]以降を選ぶと離散遷移をします。

状態遷移図を見ながら意図どおりに動いているかチェックします。 上の図では、初期状態A_S1から、メニュー[4]を選択していき、A_S2、A_S3、A_S4、A_S5、A_S6、A_S1と遷移することを確認しています。 その後、A_S1からA_S2に遷移し、メニュー[0]を選択して時間を30進めた後、A_S1に遷移することを確認しています。

並行合成

一般にシステムはユーザーや外部ソフトウェアなどと相互作用をもちます。 しかし、相互作用をもつ複数の要素から構成されるシステムを欠陥なく設計し実装することは難しいものです。 複数の要素から構成されるシステムをモデリングして開発の早い段階で先のように動かしてみることは、手戻りを減らすことに寄与することが期待されます。

システムを動かすためには、まず相互作用する複数のプロセスをそれぞれモデリングします。そしてそれらを合成します。合成は、複数の振る舞いを組み合わせてできたシステムの振る舞いを決定します。 並行合成は、合成する2つのプロセスそれぞれで発生する遷移のうち、同期させたい遷移を指定して合成します。指定された遷移以外の遷移は独立して起こります。

2つのプロセスを受け取り、並行合成したプロセスを返す関数の型を次のように決めます:

val composit:
  ?sync_set_by_ch:('ch -> bool) ->
  ?eq_ev:('ev -> 'ev -> bool) ->
  ('ev, 'ch, 'p_state, 'clock) process (* プロセスP *) ->
  ('ev, 'ch, 'q_state, 'clock) process (* プロセスQ *) ->
  ('ev -> 'ch) (* 対応チャネル取得関数 *) -> 
  ('ev, 'ch, 'p_state * 'q_state, 'clock) process

プロセスPとプロセスQは合成対象のプロセスです。合成されたプロセスがとりうる状態は、プロセスPがとりうる状態とプロセスQがとりうる状態の組になります。合成されたプロセスがとりうる状態はp_stateとq_stateのタプルで表現します。 対応チャネル取得関数は、与えられたイベントが属するチャネルを返します。 sync_set_by_chで同期イベント集合を表現します。同期イベント集合には、合成する2つのプロセスの相互作用に関係するイベントを指定します。たとえば2つのプロセス間でチャネルを通じて値を送受信する場合、値のやりとりをするイベントを指定します。チャネルを受け取ってtrueを返す場合、そのチャネルに属するイベントはすべて同期イベント集合に含まれることを意味します。 eq_evは受け取った2つのイベント同期を比較し等しいかどうかを判定する関数です。2つのイベント同期が同期イベント集合に含まれているとき同期するのかどうかを判定するのに使います。

ATMの例(並行合成)

さきほどのATMのモデルは変えずに、新たにユーザーの振る舞いをモデリングして、ATMと並行合成します。

ユーザーの振る舞いを次のように定めます: f:id:swet-blog:20220324180131p:plain

さきほどのcomposit関数を用いてATMの振る舞いとユーザーの振る舞いを並行合成したものを動かしてみます。 同期イベント集合は次のようにします:

let sync_set_by_ch ch =
match ch with
| Ch_In -> true
| Ch_Pin -> true
| Ch_Req -> true
| Ch_Dispense -> true
| Ch_Out -> true
| _ -> false

ユーザーとATMを合成した振る舞いの状態遷移図は次のようになります: f:id:swet-blog:20220324180045p:plain

初期状態を(U_S1, A_S1)にして動かしてみます。

f:id:swet-blog:20220324182633g:plain

まとめ

調べたい部分に限定したモデリングは実システムの実装よりもずっと小さなコストでできますから、実装が終わって調べていた振る舞いを、仕様作成の段階で調べることができます。 ここで誤りをみつけることができれば手戻り避けることができるので、開発時間を大きく短縮できます。 動くモデルを作って動かせば、仕様書を読んだだけだと気づけなかった仕様の欠陥にも気づくことが期待できます。 また設計上の選択についてさまざまな可能性を実験的に調べるといったことも可能になります。そうすることでよりよいプロダクトやサービスを開発できる可能性が高まると考えています。

参考

この1年すすめていた「プロジェクトの健康状態の可視化と予防」と「自動テストの適用範囲の拡大」という施策についての話

SWETグループの平田(@tarappo)です。

早いもので2021年度もとうとう終わりをむかえようとしています。 ふりかえりということで、ここ1年ほどの間に私も関わって進めていた次の2つの施策についてかんたんに紹介したいと思います。

  • プロジェクトの健康状態の可視化と予防(dev-vital)
  • 自動テストの適用範囲の拡大

今回紹介するこれらの施策は、SWETメンバーの今までの経験などを元に議論した中で出てきた課題から決めています。

プロジェクトの健康状態の可視化と予防(dev-vital)

私がSWETに所属してある程度の期間がたちますが、いろいろなプロジェクトに関わってきました。 その中で感じたのは、あるプロジェクトで出会った課題は他のプロジェクトでも起きていたりするということです。

今までのSWETの取り組みはプロジェクトですでに起きた課題に対してアプローチをとることが一般的でした。 たとえば、すでにテスタビリティがよくないプロダクトをリファクタしてテストをかけるようにするとか、CI/CDサービスが動いていない状態を直すといったようなところからです。

プロジェクトの早い段階からSWETのメンバーが関わっていれば、上記のようなことをあとから行わずにすんでいたかもしれません。 しかし、弊社のプロジェクトの数・規模感・スピードなどを考えるとすべてに関わるのは無理があります。

そこでどうすれば、このようなことが起きないかについて検討をしました。 その中で出てきたのが、次のような仮説です。

  • プロジェクトにおけるある指標が他の指標に影響をあたえて、最終的にユーザーに見えるレベルでの問題となるのではないか?
  • 影響をあたえる指標の関係性がわかれば、将来起こりうる問題の予兆がわかりその結果として予防ができるのではないか?
  • そのようなプロジェクトの健康状態といえるような指標を可視化できればよいのではないか?

この仮説をもとに「プロジェクトの健康状態の可視化と予防」という施策を2021年度からはじめました。 この施策を「dev-vital」と呼んでいます。

この施策では、まずプロジェクトの健康状態に必要と考えられる指標がどういったものかについてメンバーで検討を何度もおこないました。 そして決めた指標をプロジェクトからとれるようにプロセスを整え、集めた情報を保存できるようにして可視化をする必要があります。

そこで、開発プロセスの中で蓄積される次のような3種類の情報をまず可視化できるように整備を進めました。 すべてのプロジェクトで一度におこなうのは困難であるため、まずは特定の1つのプロジェクトをターゲットとして進めました。

  • (1)CI/CDサービスから得られる「ビルド時間」や「ビルドの成功・失敗率」「テストケース数」
  • (2)GitHubから得られる「エンジニア数」や「PRなどの情報」
  • (3)不具合チケット(JIRAチケット)から得られる「不具合数」や「クローズされるまでの時間」

なお取得するための情報元として上記に紐づくのもありますが「アプリ、サーバのリリース頻度」なども取得しています。

CI/CDサービスからの情報

(1)の情報を可視化するためにメンバーが開発しているCI Analyzerを活用しています。

このツールについては「CI/CD Conference 2021」で登壇をしていますので次を見ていただければと思います。

データを得るためにもCI/CDサービスを活用してPR時などをふくめビルドを継続的におこなえるような環境であることが望ましいです。 そもそもCI/CDサービスが動いてなければ、コードに問題があるのかどうかすらわかりません。

その一環も含めて、弊社の「Pococha」というサービスでCI/CDサービスを活用できるようにいかに開発に組み込んでいくかという次のような改善を進めました。

また、CI Analyzerでテストケース結果を確認できるようにCI/CDサービスの成果物としてJUnit.xml形式のファイルが保存されている必要があります。 それらの対応も併せておこなっています。

これにより、CI/CDサービスからいろいろな情報が得られるようになっています。

GitHubからの情報

(2)の情報はGitHubのGraphQL APIを活用することで得ることができます。

データを得るためにはPR時に記載する情報の整理ができていることが望ましいです。 PRには「誰が担当したのか(assignees)」「どのマイルストーンでリリースするのか(milestone)」「どのような対応なのか(label)」などいろいろな情報を含めることができます。

これらの情報はPRを出した人自身で入力することが望ましいもののどうしても人によって入力内容に差がでてしまいます。 そこで次を活用しました。

  • PR Templateの活用
  • Dangerの利用による自動設定
  • Dangerの利用による未入力箇所の指摘

これらにより、PRからの情報はある程度統一されるようになり、データの取得ができるようになりました。

JIRAからの情報

(3)は不具合チケット(JIRAチケット)の活用です。

検証を担当する品質管理部のメンバーがいる場合、検証時に不具合があればJIRAに不具合チケットを起票するようになっています。

今までも、一部のプロジェクトではこの不具合チケットのデータはまとめてBigQueryに保存してデータポータルを使って可視化していました。 今回ターゲットとしたプロジェクトにおいては、その整備ができていなかったこともあり、保存し可視化できるようにしました。

dev-vitalの現時点のまとめ

このようにいろいろと情報を取れるようにするにはプロセス含め整えることが必要でした。 その上で、データを保存できるようにし可視化をすすめました。

今は上述したようないくつかの情報について可視化できるようになっています。 この可視化された情報からだけでもある程度の情報が得られる状態ではあります。

しかし、当初かかげている仮説は次のようにそれぞれの指標が影響を与えているというものです。 「プロジェクトにおける指標が他の指標に影響をあたえる」

一歩目をあるき出したとはいえ、この仮説に対して検証ができるフェーズについてはこれからというところになります。

今まで話したようなこの1年間でおこなえたことについては次の勉強会でメンバーが登壇をしますので是非聴講してもらえればと思います。

自動テストの適用範囲の拡大

SWETは今までいろいろな自動テストを実装し、運用してきました。 ユニットテストといったフィードバックの早い自動テストは重要ですし、全社的な教育や事業部との連携などは次のブログにあるように進めてきました。

これらは今後も続けていきますが、ユニットテストよりも結合度の高い自動テストも重要です。 SWETが取り組んできた自動テストとしては、次のようなものがあります。

  • (1)パフォーマンステスト
  • (2)スクリーンショットテスト

パフォーマンステスト

1つ目のパフォーマンステストは「Pococha」のiOSアプリで取り組んできました。

Pocochaはライブ配信サービスであり「パフォーマンス」という観点は非常に重要です。 たんに機能が動けばいいわけではなく、高負荷時においてもアプリがユーザーにとって問題なく使えることが求められます。

取り組みをはじめた時点では、パフォーマンス計測自体が実施できていませんでした。 そこでまず、パフォーマンスを計測できるようにして、それを継続的に確認できる状態にする必要がありました。

そのために、パフォーマンス計測を自動でおこなえるようにしました。 このパフォーマンス計測周りについてはPocochaでおこなった事例として次のような記事を書いています。 この記事時点ではおおがかりな基盤にはなっていますが、この時点ではこのぐらいの基盤を作って計測する必要性がありました。

そして、そこから年月がたって必要なことはかわってきてこの基盤についても見直しをすすめています。

パフォーマンス計測がどのレベルで必要かはプロダクトの性質やそのときの状況によって異なります。 しかし一切関係ないというプロダクトはないともいえます。

今では、パフォーマンスに関してはiOS、Androidともにプラットフォーム側がいろいろと機能を提供してくれるようになってきています。 たとえばiOS側では、MetricKitXcode OrganizerなどがWWDCで発表されています。 Android側では、Android Vitalsといったものもあります。

これらのようなプラットフォームが提供する機能を活用することも重要です。

iOSアプリにおけるパフォーマンス計測についての話は2022/3/24に開催した「iOS Test TeaTime #4」でメンバーが登壇しました。

毎年のように変化があるので是非まとまったこの資料を見てもらえると嬉しいかぎりです。

スクリーンショットテスト

2つ目がスクリーンショットテストです。 ここでいうスクリーンショットテストは、ある時点での画面をキャプチャしたものを検証に利用することを指しています。

スクリーンショットはいろいろなプロダクトで活用されています。 すぐにスクリーンショットがとれる状態になっていればよいですが、必ずしもそういうコードになっていないことはよくあります。 そこで、まずはスクリーンショットをとれるようにする必要があります。

Androidにおいて、それらについておこなったことについては2021年の「DeNA TechCon 2021」でメンバーが登壇しました。

この登壇から1年がたち、スクリーンショットの活用方法は広がってきました。 今、Pocochaでおこなっている活用方法については先日おこなわれた「DeNA TechCon 2022」でメンバーが登壇しました。

このようにいろいろな自動テストを活用できる場所で使えるようにしています。

おわりに

こんかいは2021年度におこなった施策についてかんたんに説明をしました。 こうして振り返ると、おこなったことについてはある程度アウトプットしていることがわかります。 今後もアウトプットしてくと思うので、是非ウォッチしてもらえればと思います。

さいごになりますが、私はこのたびSWETを卒業します。

入社してから、ずっとSWET(SWETがグループになる前から)としていろいろなことをおこなってきました。 そのすべてをブログにまとめようかと思いましたが、あまりにも長くなるので2021年度に私がかかわった施策についてかんたんに説明しました。

長い間、働けたのは今回説明した施策みたいな非常に魅力的な仕事に取り組めたことや関わってきたメンバーの魅力によると思っています。

SWETでは上述した施策以外にもいろいろなことをおこなっています。 次のサイトに本件以外の情報についても載せているので見てもらえればと思います。

非常に魅力的で面白い仕事が待っているので、是非とも興味がある方は次から応募していただければと思います。

DeNA TechCon 2022開催決定!今年もSWETメンバー登壇します!

SWETグループの井口(@hisa9chi)です。本記事では2022年3月に開催予定の弊社イベント DeNA TechCon 2022 に関してSWETグループ含め所属する品質管理部から5件の登壇が予定されております。

品質管理部ではDeNAのものづくりを支え、品質管理のスペシャリストとして、事業部とともにプロダクトの品質を作り込んでいっています。そのためにプロダクト開発の現場に深く入り込み、品質の担保とその改善活動を引っ張っていくことで、信頼性の高いプロダクトを素早く提供し続けます。さらに、日本のテスト業界を牽引していく存在となるために、標準化活動や高度なテスト技術を追求しており、得た技術やノウハウは内外問わず積極的に発信、共有しています。

そんな品質管理部の取り組みの一部を本イベントで発信しますので、是非とも皆様に聴講していただきたい思いで今回5件の登壇の見所を紹介させていただきます。

DeNA TechCon 2022 イベント情報

紹介セッション

今回、ご紹介するのは次の5つとなります。

Track 時間 タイトル 部門 登壇者
Talk C 16:00-16:30 CS×QAシナジー発揮!ユーザ体験向上ハンドブックのススメ QCグループ
CSチーム
柏倉 直樹(Naoki Kashiwagura)
小澤 直美(Naomi Ozawa)
Talk C 16:30-17:00 受身から攻めのQAへ!事業を成功に導くQAへの変革 QCグループ 前川 健二(Kenji Maekawa)
Talk B 17:30-18:30 Unity開発でのミスを未然に防ぐRoslynアナライザーのすゝめ SWETグループ 稲垣 和真(Kazuma Inagaki)
LT A 14:30-15:00 ゲーム開発のCI/CDを支えるJenkins運用 ~MacStadium 活用とディスク容量との闘い~ SWETグループ 井口 恒志(Hisashi Iguchi)
LT A 14:30-15:00 Pocochaにおけるスクリーンショットテストのレポート活用術 SWETグループ 外山 純生(Sumio Toyama)

CS×QAシナジー発揮!ユーザ体験向上ハンドブックのススメ

DeNAの品管は当たり前品質の担保だけではなく、サービスの価値を高めるための活動を積極的に実施しています。 たとえば、開発実装前に「本当にこの仕様でお客様が喜ぶのか?」の視点でレビューを実施するだけでなく、実装後の動作確認においても「この動きでお客様は満足するか?」を常に考えながらテストを実施し、企画・開発チームへ意見を伝えます。 この際に重要となるのが「QAメンバーのお客様目線をいかに強化するか」です。 今回のTechConでは、部門の枠を超えてCSチームと一緒に「お客様目線を強化する仕組み」を作った事例をお話しします。

受身から攻めのQAへ!事業を成功に導くQAへの変革

DeNAのヘルスケア事業領域では、開発上流での品質担保に課題があり、品質管理(テスト)にかかるコストが増加し、QCDバランスを保ったリリースが難しい領域でした。 そこで、サービスの企画が始まる上流工程からQAメンバーを参画させ、QA目線でのモノづくり強化策を実行してきました。 その結果、開発プロセス改善や開発品質向上だけでなくそのほかにも多くの成功を収めました。 今回はこれら成功事例をもとにテスト中心の受け身から、事業を成功に導くQA部隊への変革の歴史とともにリリースまでにQAがどのような活動を行なっているか紹介いたします。

Unity開発でのミスを未然に防ぐRoslynアナライザーのすゝめ

コードをビルドすることなく、ミスを検知できたらゲーム開発が楽になると思いませんか? 本セッションでは、.NETの静的解析器であるRoslynアナライザーのUnityプロジェクトへの導入と、カスタムルール実装のハードルを下げるためのノウハウを紹介いたします。 Roslynアナライザー活用のヒントとして役立ててくだされば幸いです。

ゲーム開発のCI/CDを支えるJenkins運用 ~MacStadium 活用とディスク容量との闘い~

オンプレmacやMacStadiumをビルドに活用して運用していると、どうしてもディスク容量不足という問題が発生します。 この問題は深刻であり、容量不足が発生するとビルドが途中で失敗してしまいます。 そのため、再ビルドや、ディスクの空き容量を増やすための調査と掃除という面倒な作業が増えてしまいます。 そこで我々がこのディスク容量不足問題に対して、どのように立ち向ったかをご紹介させていたきます。

Pocochaにおけるスクリーンショットテストのレポート活用術

ライブ配信サービスのPocochaでは、アプリのUIに関する問題の早期発見を目的として、自動でスクリーンショットを取得するテストを導入しています(参考:「Android スクリーンショットテスト 3つのプロダクトに導入する中で倒してきた課題」)。 その成果物となるスクリーンショットレポートの提供が始まると、当初想定していなかった活用アイデアが出てくるようなりました。

本LTでは、デザイナーやQAメンバーの課題解決にスクリーンショットレポートが活用された例をいくつか紹介します。スクリーンショットレポート活用のヒントとして役立てて下されば幸いです。

まとめ

今回は、SWETの所属する品質管理部による発表をご紹介させていただきましたが、他にもTechConではDeNAのさまざまなサービスに関する登壇があります。実際のプロダクト開発の現場で培ったノウハウが多く凝縮された内容となっております。色々と聴講していただいて、ノウハウの吸収や今抱えている問題などの解決の糸口となると幸いです。また、イベント当日はDiscordサーバーを立ち上げる予定ですので、登壇者・聴講者間でのコミュニケーションも可能となっております。ぜひとも、2022年3月17日はDeNA TechCon 2022で盛り上がりましょう。

参加登録はこちらからお願いいたします。

「テスタビリティの高いGoのAPIサーバを開発しよう」というハンズオンを公開しました

はじめに

SWETグループのGoチームの伊藤(@akito0107)です。 「テスタビリティの高いGoのAPIサーバを開発しよう」というタイトルでGoを用いてWeb APIを書くエンジニア向けのハンズオンを公開しました。 この記事ではハンズオンの内容と補足を紹介しようと思います。

ハンズオンのねらい

このハンズオンでは、APIサーバーを題材としてテスタビリティを担保した設計をするためには何が必要なのかを学んでもらうことを目的にしています。 特に、私自身がテスタビリティを考える上で重要だと考える、Dependency InversionやDependency Injection (DI), Test Doubleについて詳しく説明し、さらには自分で実装してもらう形をとっています。

Goでは他の言語と違い、デファクトのWeb Frameworkなどはありません(強いて言うなら標準がデファクトですが)。 そのため、Goで現実のプロダクトを開発する際には、必要に応じてライブラリ同士を組み合わせる、または自分たちで開発する必要があります。 その際の指標として役立つようなものを狙ってハンズオンを作成しました。

ハンズオンの内容

今回のハンズオンでは、Go言語を書いたことがある人や(言語問わずに)APIサーバーを実装したことがある人向けに、より「テスタビリティ」が高い設計にするためにはどうすればよいのか、といった内容を紹介しました。

以下がハンズオンとあわせて公開した資料となります。

speakerdeck.com

なお、Codelabの回答もrepository上に公開してありますので、もしわからないところがありましたら参照してください。

ハンズオンは[スライドを用いた講義]→[codelabを用いた実習]の流れを1Chapterとし、全部で3つのChapterから構成されております。 講義による知識のインプット + codelabで実際に手を動かすことによるアウトプットを体験することで、より実践的な技術を身に着けてもらうことを想定しております。

Codelabでは簡単なAPIサーバのサンプルアプリケーションを題材とし、テスタビリティが低い設計から高い設計へのリファクタを行っていきます。 その過程でテスタビリティの高い設計や、テストを書く際のテクニックなどが学べる構成になっています。

各チャプターは以下のような内容になっています。

  • Chapter1 テスタビリティについて
  • Chapter2 アーキテクチャについて
  • Chapter3 Test Doubleについて

以下にそれぞれのChapterの内容を簡単に説明します。

Chapter1 テスタビリティについて

最初のChapterでは、今回のハンズオンで一貫して対象とするテスタビリティの定義をしました。 テスタビリティには様々な定義があります。今回はソフトウェアテスト293の鉄則*1

テスト容易性とは可視性と操作性である

という定義を採用し、テスト対象システム(今回の場合はAPIサーバー)の可視性と操作性を上げるための設計について考えていきます。

f:id:swet-blog:20210909123728p:plain
Testabilityの定義について

Codelabではサンプルアプリケーションのビルドおよび次のChapter以降で行うリファクタの準備をしました。

Chapter2 アーキテクチャについて

このChapterではテスタビリティが高いアーキテクチャについて解説しました。 いくつか例はあるものの、APIサーバを実装する上で最も古典的な設計である3層アーキテクチャについて解説し、テスタビリティを担保する上で特に重要な概念であるDependency Inversion(依存関係逆転)について解説をしました。

f:id:swet-blog:20210909123918p:plain
3層アーキテクチャについて

CodelabではChapter1でビルドしたサンプルアプリケーションをリファクタし、3層アーキテクチャに直した上で、依存関係の整理ができるような設計にすることをゴールにしています。

Chapter3 Test Doubleについて

最後のChapterではTest Doubleについての紹介しました。mockやstubという言葉は聞いたことがあるかもしれませんが、それらの総称がTest Doubleです。 Test Doubleのメリットや分類を紹介し、最後にGoでの実装例を話しました。

f:id:swet-blog:20210909124059p:plain
Test Doubleについて

CodelabではChapter2でリファクタしたサンプルアプリケーションに対し、Test Doubleを用いたテストを追加していくということを行いました。

時間の都合上、Codelabを最後までやりきれなかった方もいらっしゃるかとは思いますが上記の通り資料は公開されており、また、Codelabの回答もrepository上に公開されていますので、時間のあるときに最後までチャレンジできるようになっています。

補足

以下にスライド・Codelabには盛り込めなかった話題について補足をしておきたいと思います。

結合度の高いテストについて

今回紹介したアーキテクチャは各レイヤーの結合度を下げ、各レイヤー単体でテストを書くことが簡単になるような点を目的として設計しています。 一方で、コードの記述量が増え、コードの見通しが悪くなる、といったデメリットもあげられるかもしれません。

講義の内部でも触れましたが、最近はミドルウェア(特にDocker)やライブラリの進化で、手元で簡単にDB等のインフラ環境が整えられるようになってきました。 Cloud Providerが提供しているコンポーネントも、以前であれば、手元に環境を再現するのが難しくテストを書こうとすると必然的にコード上でTest Doubleを用意する必要がありました。 しかし、現在ではAWSであればlocalstackや、GCPであれば公式が提供しているemulatorなどが出てきて、手元で環境を再現することが容易になってきました。 そのため、以前よりも結合度の高いテストを書くことは容易になってきており、相対的に単体のレイヤーで行うテストの重要性や必然性は薄れてきているかもしれません。

それでもあえて今回このある意味レガシーな設計を紹介したのはいくつかの意図があります。代表的なものを紹介します。

まず第一に、(これは当たり前ですが)いくらlocalで使えるコンポーネントが出てきたといっても全てがそろっているわけではないので、ある程度複雑なシステムを対象にする場合は、何かしらのTest Doubleを使う必要があるということ。

そして第二に、再現テストを書くための土台を作っておくということがあげられます。

再現テストとは、何かしらのバグが発生した際に、そのバグが発生するような状況を再現するために書くテストです。 再現テストを実装した上で、バグを修正し、テストが通ることを確認するというような使い方をします。 バグの発生原因を特定し、確実に修正されていることを確認できる手法で、これをやるかやらないかでバグ修正の際の安心感や効率が変わってくると感じています。

再現テストを実装する上では、例えばDBからエラーが帰ってきた場合など、特殊な状況・状態に依存することが多く、この状況・状態の再現は仮にエミュレータを使っていたとしても非常に難しくなる場合があります。 そういった場合でもTest Doubleなどを使い、システムの状態を簡単にいじれるようにしておくと簡単に再現テストを書くことができます。

実運用中にバグが発覚し、いざ再現テストを書こうと思ったときには外部環境に密結合でテストが書けないといった状況を防ぐためにも、やや冗長に感じられるかもしれませんが、外部から依存を注入できるような設計は今でも有効だと感じています。

より良いテストを書くために

Codelabで記述したテストコードは、繰り返しが多く冗長な実装になっていると感じるかもしれません。 本来は、テストコードを記述した後にテストコード自体のメンテナンス性を上げる必要があります。 GoであればTable Driven Testsなどを使うと良いかもしれないです。

以下の例はCodelabのChapter3のuserUsecaseのtestをTable Driven Testingを使うようにリファクタした例です。

func TestUser_Create2(t *testing.T) {
    cases := []struct {
        name      string
        mockFn    func(t *testing.T) *userRepositoryMock
        mock      *userRepositoryMock
        expectErr error
        in        *model.User
    }{
        {
            name: "success",
            mockFn: func(t *testing.T) *userRepositoryMock { // testing.Tをt.Run内で受け取る必要があるため関数でwrapしている
                return &userRepositoryMock{
                    findByEmailFn: func(ctx context.Context, queryer sqlx.QueryerContext, email string) (user *model.User, err error) {
                        if email != "test@dena.com" {
                            t.Errorf("email must be test@dena.com but %s", email)
                        }
                        return nil, apierr.ErrUserNotExists
                    },
                    createFn: func(ctx context.Context, execer sqlx.ExecerContext, m *model.User) error {
                        return nil
                    },
                }
            },
            in: &model.User{
                FirstName:    "test_first_name",
                LastName:     "test_last_name",
                Email:        "test@dena.com",
                PasswordHash: "aaa",
            },
        },
        {
            name: "return user from repository",
            mockFn: func(t *testing.T) *userRepositoryMock {
                return &userRepositoryMock{
                    findByEmailFn: func(ctx context.Context, queryer sqlx.QueryerContext, email string) (user *model.User, err error) {
                        return &model.User{}, nil
                    },
                    createFn: func(ctx context.Context, execer sqlx.ExecerContext, m *model.User) error {
                        return nil
                    },
                }
            },
            in: &model.User{
                FirstName:    "test_first_name",
                LastName:     "test_last_name",
                Email:        "test@dena.com",
                PasswordHash: "aaa",
            },
            expectErr: apierr.ErrEmailAlreadyExists,
        },
    }

    for _, c := range cases {
        t.Run(c.name, func(t *testing.T) {
            userUsecase := NewUser(c.mockFn(t), nil)
            err := userUsecase.Create(context.Background(), c.in)

            if err != c.expectErr {
                t.Errorf("expectErr is: %v but actual: %v", c.expectErr, err)
            }
        })
    }
}

まとめ

Goのハンズオンを公開し、その内容の紹介と補足をしました。 講義を受けていなくても、スライド、Codelabそれぞれで完結して内容を把握できると思いますので、興味がありましたらぜひご覧になってみてください。

DeNAではGo以外にもAndroidのTestに関するハンズオンを公開しております。 せひそちらも覗いてみていただければと思います。

最後に、SWETでは一緒に働いてくれる人を募集しています。下記職種で募集しているのでぜひご応募ください。

career.dena.jp

*1:セム ケイナー,ジャームズ バック,ブレット ペティコード. ソフトウェアテスト293の鉄則

治安維持のためにCI/CDサービスを活用しておこなったこと

SWETグループの平田(@tarappo)です。

10/21(木)にiOS Test TeaTime #3を開催しました。

その時の私の登壇資料は次のとおりです。

資料では不足しているであろう情報もあるので、本稿ではその点も補いつつ説明していきたいと思います。

はじめに

SWETメンバーとして、プロジェクトに関わるときは「CI/CDサービス」が問題なく動いているかどうかを確認することはよくあります。

CI/CDサービスが文字通り継続的に動いているのであれば「プロジェクトのコードが手元でも動かせる」「コードの状態がある程度わかる」という状態ともいえます。

そのため、これはプロジェクトの状況を把握してなにをするべきかを判断するためには重要な指標の1つともいえます。

その中で最初にチェックする箇所の例としては、次のようなものがあります。

  • どのようなことを実行しているか
    • 自動テストがあって実行されているか
  • どのようなタイミングで実行しているか
    • 継続的に動かせているか
  • ビルドの成功率はどういった状態か
    • 仮に失敗をある程度していてもすぐ直せているか

本稿では、私が今回関わったプロジェクトの「関わった頃の状況」とそれを元にどのような理由からどのようなことをおこなっていったかについて次から説明をしていきます。

関わった頃の状況

本稿で話す関わったプロジェクトではiOS周りをメインで関わりました。 そのプロジェクトで利用しているCI/CDサービスはBitriseになります。

Bitriseの詳細は本稿では説明をしませんが、Bitriseでは実行するものを「ワークフロー」という単位で用意します。 その「ワークフロー」は特定の機能をおこなうことができる「ステップ」と呼ばれるものを組み合わせて作ります。

このBitriseの関わった頃の利用状況としては次のようなものでした。

  • Q:どのようなことを実行しているか
    • 一般的なワークフロー(ビルド、テスト、App Store Connectへのアップロード)は揃っている
    • コードにテストケースは一定あるが「テスト」のワークフローはNightly実行のみ
  • Q:どのようなタイミングで実行しているか
    • 継続的に動かせているのは一部であり、次のような感じになっていた
      • PR単位:Dangerのみ実行しており、他は実行していない
        • Dangerは本プロジェクトの他リポジトリと併せてCircleCIに移行しました
      • Push時(特定ブランチ):アプリのビルドと配布
      • Nightly(1日1回):テスト
  • Q:ワークフローの実行結果の成功率はどのぐらいか
    • アプリのビルドにおいて、マージ後に直せているがテストにおいては失敗したまま放置傾向になっている

上記のように、PR単位で何かしらのワークフローは動かしておらずコードのチェックは行えていない状態ではありました。

この状態の理由

関わった頃はまだ「開発者がそこまで多くなく、みんながある程度プロダクトについて理解している状態」という前提がありました。

そのような前提がある中なので、

  • Bitriseの運用に対してコストをあまりかけられない
  • 「ビルド」や「テスト」が失敗するようなコードであっても、原因に気づきやすくコードを直すコストはそこまで高くない

結果として、この時点ではBitriseは「アプリの配布用」の用途ぐらいになっていました。

今後もこのような体制であれば一定問題がおきないかもしれませんが、関わった時点で人が増えつつある時期でもありました。 このまま進んでいくと、次のような問題が起こるであろうと思われました。

今後起こるであろう問題

関わる人が増えてくると、次のような問題が発生していきます。

  • 手元で「ビルドができない」「テストが通らない」といったケースが増える
    • 自分が関係していないコードのことが多くなり、原因の特定コストが一定かかる
  • 誰もがマージできるためビルドできないコードが一定発生し「検証をするためのアプリ」のビルドが検証当日になっても出来ていないといったことが起こる

この手のことがある程度増えて、結果として「コミュニケーションコスト」「失敗の原因の特定コスト」「修正コスト」といったいくつものコストが必要になってしまうということが起こりえます。

この状態が今後起こることを想定し、Bitriseをもう少し活用し「コードの品質」をある程度担保できるようにするのが良いだろうと判断しました。

そこで、「今後に向けた対応」として次を検討しました。

  • Step1:PR時点で「ビルド」「テスト」のワークフローを動かす
  • Step2:「ビルド」「テスト」のワークフローが失敗したものはマージできないようにする

これらをBitriseやGitHubなどで設定することはかんたんです。 しかし、実際に利用され続けなければ意味がありません。

そこで、この「Step1」と「Step2」をおこなった場合の課題などについて関係者にヒアリングをおこないました。 その結果として次の課題がわかりました。

  • 課題1:実行時間がかかりすぎて待っていることができない
    • 特に「テスト」の実行時間が長い
  • 課題2:必要性を強くは感じていない
    • 現時点ではそこまで問題が起きていないと考えているため

実行時間がかかりすぎてもこれらを行おうとするかはプロダクトをとりまく状況にも依存します。 この時点では、スピード優先してリリースしていくという形とも言えました。

しかし「課題1」「課題2」において何かしらのアプローチをおこなうことは今後を考えると必要だろうと判断しました。 それらについて次に説明をしていきます。

課題1:実行時間について

Bitrise Insightsという機能を利用して実際に「テスト」にかかっていた時間を出すと次のとおりです。

f:id:swet-blog:20211104113334p:plain

この画像をみたら分かるとおりですが、

  • テストの実行時間は40分以上
  • 成功率は高くない

PR時において40分以上も結果を待つのは厳しく、1度失敗して修正するとすぐに1時間以上の時間が発生するためPR時に設定したくない理由も分かります。

また成功率の低さは手元で全テストを実行するのが面倒という側面もあったと考えられます。 本稿では詳細は割愛しますが、このプロジェクトではモジュール単位で分割しており、モジュール単位でテスト実行できるものの、それら全てのテスト実行がXcodeからできるようになっていませんでした。 ※なお、この課題については全実行できるような仕組みを別途チームメンバーが用意してくれました。

このような状況下では、テストが壊れやすくその上に壊れたテストが乗ってしまい、さらに直しづらいというループが出来てしまいます。

これらを解消するには「実行時間」をできる限り短くする必要があるといえます。

課題2:必要性の認識

「定期的にコードのチェックをしてなくても問題があまり起きていない」状況ではありますが、将来的には炎上リスクがあると言えます。

必要性を認識してもらうためには「治安の良い状態に慣れてもらう」「現状を認識できるようにする」といった状態にして、このような環境が当たり前と思ってもらうことです。

  • 「Bitriseの治安の良い状態(グリーンが当たり前)」を作って、その環境に慣れてもらう
  • 「Bitriseの状態の可視化」をおこないコードの状態を常にわかるようにする

このような状況を当たり前にすることで、なにか問題が発生したときに以前と比べて「問題が起きた」というのを強く感じてもらえるようにするというのがあります。

おこなったことの内容

上述した「課題1」「課題2」を解決するために行なったことについて次のとおりです。

  • 前提としてやる必要のあること(まずはキレイにすること)
    • (1)Bitriseのワークフローの現状把握と整理
  • 「課題1」へのアプローチ
    • (1)Bitriseのワークフローの現状把握と整理(一部)
    • (2)実行時間の短縮
  • 「課題2」へのアプローチ
    • (3)情報を追えるようにするための可視化
    • (4)安定運用するための対応

(1)Bitriseのワークフローの現状把握と整理

既存のワークフローの把握と整理

BitriseはWebからワークフローを簡単に作れるため、ワークフローやワークフロー内のステップは増えやすく誰が作ったか分かりづらいという問題があります*1

結果として次のようなことがしばしば起きますし、実際に起きていました。

  • 「使っていないワークフロー、ステップ」が残されたままになることがある
    • 例えば、調査用に作った一時的なものは放置されやすい
  • ワークフロー名に統一性がなかったり、行なっていることとワークフロー名があっていない

そこで、次の方針をもとに整理整頓を進めました

  • 必要のないものは削除
    • これでステップが削除されると実行時間の削減にもつながる
  • ドキュメント化
    • ルール化したほうが良いものについてはルールについて明記
    • ワークフローやトリガーなどBirise利用に関する情報をドキュメントにまとめ、適宜更新

この方針のもとにおこなったこととしては、次のようなものです。

  • (1)メンバーにヒアリング
    • 使っていないワークフロー、ステップの削除
  • (2)ワークフローで行なっていることをチェック
    • 別名のワークフローだけど中身は同じというのもあったため整理
    • 使っているステップをチェックし必要なければ削除
  • (3)ワークフローの作り方についてルール化
    • ワークフロー名の命名規則
    • ユーティリティワークフローの用意と利用
  • (4)今後のために整理した内容は全てドキュメント化

ヒアリングして削除できたものはそこまで多くなく、実際はワークフローならびにステップ、そしてステップがよんでいる先(たとえばfastlaneのlane)をチェックして、本当に必要なのかを確認しつつ進めました。

次図は実際のドキュメントの目次ですが、最終的にはこのようなドキュメントを用意することで、これから利用する人も現状把握がある程度しやすいようにしています。

これらによって現状のBitriseのワークフローなどの状態がわかるようになりました。 ここからがスタートとも言えます。

また、これらは地道な作業ですがこれだけでも多少の実行時間の削減に繋がっています。

既存のワークフローの失敗を直す

あまり定期的に動かしてないこともあり「テスト」のワークフローは失敗していました。

壊れた状態のままにしておくと、他の壊れたコードも追加されていってさらに直すのが大変になります。 結果として、放置されてしまうということはよくあることです。

あまり動かしていなかったこの「テスト」では次のような問題が起きていました。

  • 例1)Compile errorになっている
  • 例2)期待値が少し変更されていた(例えば表示文言が変わったとか)
  • 例3)手元では動くがBitriseの環境依存で落ちる
  • 例4)Xcode12 x XcodeGenで起きたCycle Inside App問題

例1)や例2)では落ちている箇所を伝えて、直してもらえれば大丈夫です。 しかし例3)や例4)は調査、対応コストが一定かかるためどうしても放置されてしまいがちです。

そこで例1)や例2)はSWETメンバーで対応はしつつも、少したったらプロジェクトメンバーに伝えて直してもらうように依頼しました。 例3)や例4)についてはSWETメンバー側でおもに関わって対応をしました。

対応をした結果、1度All Greenになりましたが再度テストが失敗することは何度も起きました。 しかし、例3)や例4)のようなコストが一定かかるものは何度も起きるわけではありません。

何度も対応を続けることで、All Greenにするための修正コストもそこまでかからないようになってきました。

(2)実行時間の短縮

「課題1」のアプローチは分かりやすく、実行時間をどれだけ減らせるかになります。

PR時にワークフローが動いている状態を当たり前にするためには、この状態が良いなと思ってもらわなくてはなりません。 そのためには、実行時間は大きな課題でした。

そこで、多少ワークフローでおこなっていることがわかりづらくなっても、実行時間を減らすことを最優先としました。

実行時間の短縮

まずは実行時間がかかっているテストをどうにかするところからはじめました。

当初のテストのワークフローの例としては次のような感じでした。 ※実際のワークフローはもう少しステップが多いです。 f:id:swet-blog:20211104113459p:plain

この状態における実行時間短縮に向けた対応として次をおこないました。

  • (1)並列化ができる箇所(ステップC)は並列化
  • (2)並列化しても共通して利用されるステップ(C以外のステップ)で時間を減らせる箇所がないかの調査と対応

ワークフローの並列化

Bitriseでのワークフローの並列化は公式のステップを利用します。 実際に用意した並列化したワークフローの図を元に説明をしていきます。

f:id:swet-blog:20211104113346p:plain

上の図を元に説明をしていきます。 ワークフロー1がメインのワークフローとなります。 ワークフロー2〜4までがワークフロー1きっかけで動く他のワークフローです。

ワークフロー1では「Bitrise Start Build」ステップを利用して他に動かすワークフローを指定します。 また、他のワークフローが動いている間にワークフロー1でもテストを実行します。

そして、ワークフロー1にある「Bitrise Wait for Buildステップ」で他のワークフローが終わるのを待機します。 また、ここで他のワークフローの成果物をまとめてテストケース数などを計算するようにしています。

ステップの実行時間のチェック

上記の並列化は「テスト」においては効果がありますが「ビルド」は並列化できません。 そこで、共通で利用するようなステップがどれぐらい時間がかかっているかをチェックしました。

ここの実行時間が削減できれば、ワークフロー全体に効果があります。

調べた結果として次の3つのステップで多少時間がかかっていました。

  • (1)git cloneステップ
  • (2)fastlaneステップ
  • (3)Cacheステップ

1つ目はgit cloneステップで、Repositoryの肥大化によってcloneをすると2〜3分もかかっている状態でした。

公式のステップはPR時にdepthの設定が解除される作りだったため、独自にステップを作るかこの公式ステップにPRを出すかを検討しましたが、4.0.27バージョン頃から対応されるようになったため、depthを指定するようにしました。

これはgit cloneステップの「Checkout options」にある「Limit fetching to the specified number of commits」で指定できます。 これにより2〜3分かかっていたのが20秒前後にまで削減されました。

2つ目はfastlaneステップです。

fastlaneを使ってビルドやテストをするためのセットアップをおこなうlaneを用意していて、それをBitriseでよんでいました。 Pluginfileに指定してある独自のプラグインも含めgem install時のdependencyが多くインストールに多少時間がかかっている状態でした。

これらはキャッシュに乗せておくことで、その時間は緩和されますがfastlaneはバージョンアップ頻度も多く、ある程度バージョンアップする必要もあります。

しかし、このセットアップ処理自体は別にfastlaneを使わないといけないわけではありませんでした。 そこで、今回はfastlaneを使わない形に変更しました。

最後はCacheステップ(Pull、Push)です。

ビルドを早くするためになんでもCacheしたいという気持ちは出がちですが、Cacheするものが増えてくると、どうしてもCacheのPullとPushに時間がかかってしまいます。

そこで、必要なもののみキャッシュをするようにしました。

また、プロジェクトで利用しているGemfileに記載されていたgemについて整理整頓をおこない使っていないものは全て削除しました。

ビルドマシンのスペックの変更

上述したように色々と実行時間を短縮させるための行為をおこないました。

しかし、最後はある意味「金の弾丸」です。

BitriseはGen2を今年(2021年)から提供しはじめました。 Gen2についての情報については次のブログを参考にしてください。

BitriseがGen2を提供したことにより、利用するビルドマシンのスペックがさらによくなりました。 現在、利用しているGen2のビルドマシン(Elite XL)と今まで利用していたGen1のEliteを比べると次のとおりです。

  • Gen1 Elite:4vCPU@3.5GHz、8GB RAM
  • Gen2 Elite XL:12vCPU@3.2GHz、54GB RAM

このスペックの変更により、アプリのビルドは40%程度実行時間が削減されました。 また、テストにおいてはビルドほどではありませんがある程度の実行時間が削減されました。

テストの実行時間の短縮結果

これらの対応の結果、どの程度実行時間が短縮したかについて説明します。

直近4週間の結果(Bitrise Insightsより)は次のとおりです。 これは全てのテスト実行(PR時、PUSH時)での結果になります。

f:id:swet-blog:20211104113308p:plain

以前の結果(41分22秒)と比べて30%ほどになっています。 当初予定していた時間は10分程度ですが、一応達成できたと言えます。

実行時間を削減する案は他にもあるのですが、まずは慣れる場を用意することを優先するためこの時間で一旦はOKとしました。

(3)情報の可視化

Bitriseでいろいろと実行するようになったこともあり、Bitriseの実行履歴から情報を追えます。 しかし、Bitriseの実行履歴を見て情報を追うのはなかなか面倒です。

情報をあとからおいやすくすることで、問題が起きたときに分かりやすくしました。 そこで次のような情報の可視化をおこないました。

  • GitHub Commit Statusの利用による可視化
  • ビルド結果の可視化

GitHub Commit Statusの活用

デフォルトのBitriseの機能ではCommit Statusに対する情報は成功、失敗といった情報ぐらいです。 そこであとからCommit Statusを見たときにどのような状態だったかが分かるようにしたいというのがありました。

そこでワークフローの種類によって、Commit Statusにそれぞれ結果を反映させるようにしました。 f:id:swet-blog:20211104113427p:plain

上記の図にあるように単にワークフローの成否だけでなく、テストケース数やカバレッジ率も設定するようにしています。 これにより、例えばある時点から特定のワークフローが失敗しているとか、そのときに特定のテストケースが失敗しているのか、Compile Errorなのかといったことまで分かるようになっています。

情報の可視化

Bitriseで実行したワークフローの結果を、SWETメンバーが開発し導入しているCIAnalyzerを利用して下図のように可視化をおこなっています。

f:id:swet-blog:20211104113300p:plain

これにより、ビルドの状態(ワークフローやステップ単位での実行時間やビルドの成功率など)を後からでも簡単に確認できるようになりました。 ただし、この手の可視化をおこなったとしても見てくれるとは限りません。

そこで、定期的に関係者に状況をシェアするようにしていました。 例えば「xx月と比べてテストの実行時間がyy%削減されました」とか「最近xxというワークフローの実行時間が少しずつ上昇しているようです」などです。

また、すでに紹介していますがBitriseでもInsightsという機能を提供しています。 この機能でも同様に履歴がわかるようになっていますし、ステップ単位での実行時間も分かります。 最近リリースしたばかりで、どのプランでも30日間の無料アクセスを提供しているとのことなので、1度見てみると良いと思います。

(4)安定運用するための対応

CI/CDを安定的に運用するためには上記の対応も重要なことです。

しかし、実行時間が短くなってPRのたびに動いたとしても、ビルドが失敗した場合に対応をせずに放置してしまうという状態はよくありません。

失敗した際に放置してしまう理由はいくつかあります。

  • 失敗していることに気づいていない
    • 単にSlackの通知チャンネルに通知しているだけでは通知数の多さでスルーされてしまうことはあります
  • 失敗した原因の対応コストが高い
    • 上述したとおり環境起因で落ちる場合は対応コストがどうしてもある程度かかってしまいます

そこで安定運用するためにおこなったことについて次に説明をしていきます。

期間限定の門番

最初に述べた「Step2」の対応が出来ない間(システム的に守れない間)の一定期間はBitriseに対する門番となりました。

門番としておこなったこととしては、おもに次のとおりです。

  • ワークフローが失敗したときの初期調査
  • Bitrise周り全般の相談窓口

失敗したときの初期調査においては、より失敗を見つけやすいようにSlackの通知周りの整理整頓もおこないました。 これについては後述します。

失敗を見つけたらBitriseのログを調査し、原因を追求した上で「対応をおこなう」「対応方法を伝える」のどちらかを行いました。

これを繰り返しおこないつつ、少しずつ自分で対応をするという範囲を狭めていきました。

失敗を知ってもらい対応してもらうための工夫

失敗した場合に、それに気づいてもらう必要があります。 そこで、何かしら問題が起きたときにすぐに気づけるような状態にしておきます。

  • Slack通知の整理整頓
  • 失敗時におけるSlack通知先の変更

Slack通知は多くなりがちです。 通知する箇所の整理や通知内容について整理整頓をおこないました。 表示する内容も変更し、テスト結果や実行時間なども表示するようにしました。

次に失敗時におけるSlackでのメンションです。 当初は失敗した場合にSlackのユーザーグループにメンションをするというのもおこないましたが、PR単位も含めると失敗することはよくあることです。

結果、メンションをしすぎてしまい逆に放置されてしまう原因になってしまいます。

そこで、ワークフローが失敗してはいけないような「デフォルトブランチ」や「リリースブランチ」においてだけ、失敗した時点で開発メンバーがメインで利用しているチャンネルにSlack通知をするようにしました。

これで、問題に気づきやすくなりました。

最終的には問題が起こらないようにする必要があります。 通知をおこなうのではなく、そもそも「失敗したものがマージされないようにする」のが望ましいです。 しかし、この段階でその設定までおこなうのはまだ時期尚早なため、まずは知ってもらうというのを優先しました。

おこなった結果

行いたかった「Step1」と「Step2」は次のとおりです。

  • Step1:PR時点で「ビルド」「テスト」のワークフローを動かす
  • Step2:「ビルド」「テスト」のワークフローが失敗したものはマージできないようにする

これらがどうなったかについて次に説明していきます。

Step1に対する結果

実行時間の短縮により「課題1」は解決しました。 そこで、PRに対してもワークフローを実行するようにしました。

ただし、一度に「ビルド」と「テスト」を設定するのではなく、まずは「ビルド」を動くようにしました。 PR時に「ビルド」を動かすようになってある程度たってから、追加で「テスト」についても動くように設定しました。

Step2の対応と結果

「Step1」が達成した段階時点では、まだ「Step2」はそこまで必要と認識されていない状況でした。 ある程度、今の状態が慣れるのを待ちつつタイミングを待つこととしました。

ブランチに失敗したままのコードがマージされるケースは、この時点でもたまに起きていました。 それでも、まだStep2についてはおこなえていない状態でした。

しかし、実際に検証前にビルドができないといったような「誰もがこの状態は良くない」と思える事件が発生した時点で、今後の予防として「Branch Protection Rule」の導入を提案しました*2

この時点ではPR時に「ビルド」「テスト」が動くようになってからある程度たっている状態でした。 これらのワークフローの実行時間があまりかかってないことを共有し、導入することになりました。

Branch Protection Ruleの導入

一度に導入するとなにかあったときの対応コストは多くなります。 その結果として導入をやめようという流れにもなりかねないため、少しずつ設定を追加するようにしました。

まずは次の設定を入れました。

  • PRの向き先のブランチ:「リリースブランチ」
    • 「Require status checks to pass before merging」をオン
      • ビルドを必須
        • GitHub Commit Statusをそれぞれ独自におこなっているので設定はかんたん

設定を入れた状態にし、なにかしら問題が起きたら即時対応をするようにしました。 例えば、Bitrise起因でビルドが出来ないなどの対応があります。

上記の設定を導入し、ある程度たったのちに次のように拡大をしていきました。

  • PRの向き先のブランチを拡大:「リリースブランチ」「デフォルトブランチ」
    • 「Require status checks to pass before merging」
      • ビルドを必須
      • テストを必須

この設定を入れて、慣れてくるようになるとエンジニアメンバーからも提案が出てくるようになりました。 そこで、次の設定を入れることにしました。

  • Require pull request reviews before merging
    • レビュー必須(approve 1件以上)

これにより「ビルド」「テスト」ができないもの、コードレビューがされていないものは基本マージができなくなり、事故が起こることは減りました。

これで当初予定していた「Step1」と「Step2」のそれぞれの対応が終わりました。

おわりに

求める姿を想定し、そこに進めていくのは必ずしも一直線とは限りません。

その状態自体が本当に自分たちにとって良いのかも分からないこともしばしばありますし、その状態にすることによる大変さがあるかもしれないと思うかもしれません。

今回は、上述したように少しずつ対応を進めていきました。 そして、少しずつメンバーに慣れていってもらうようにしました。

その結果として、当初予定していた「求める姿」になりました。

この状態がベストではなくまだ先の「求める姿」があると思っています。 また、プロジェクトの状態が変わっていけばまた変えるべき箇所も出てくるとは思います。

今後も現在の状況を見ながら、「求める姿」を考えて対応を進めていければと思います。

最後に、上記のような活動に興味を持ってくれた方、一緒に働いてみたいなと思ってくれた方。 現在、SWETはメンバーを募集中ですのでぜひ応募ください。

*1:Bitriseではリポジトリにあるbitrise.ymlを正として利用する設定がありますが、弊社の場合はGitHub:eを利用しており本機能をそのまま利用できません。

*2:Branch Protection Ruleを用いることで、PRのコードがCI/CDサービスで成功していない場合は特定ブランチにマージさせないといった設定できます。

8年ぶりの新卒としてSWETにjoinした話

はじめまして、2021年に新卒としてSWETに加わったIKです。 今回はSWETに入って感じたこと、思ったことなどを新卒目線で書いていきたいと思います。

IKってどんな新卒?

学生時代にVim scriptばかり書いていた新卒です。 主に、ソフトウェアのメンテナンスに興味をもち、OSSにパッチを送る活動をしていました。

パッチを送った主なリポジトリ

また、VimConf2019に登壇し、以下の発表をしました。

しかし、一般的なWebシステムやアプリ開発には疎い、そんな状態で入社しました。

どうしてSWETへの配属を志望したのか

私は学生時代、1からソフトウェアを作ることよりも、ある程度形になったソフトウェアのメンテナンスに参加することの方が多かった傾向にあります。 そこで行ったメンテナンスの内容を以下に示します。

  • 具体的なメンテナンスの内容
    • バグの修正
    • feature requestに応える
    • パフォーマンスの改善
    • Pull Requestのレビュー
    • 既存のドキュメントの改善

ソフトウェアのメンテナンスに参加することで、メンテナンスのフェーズは1からソフトウェアを作成するフェーズよりも遥かに長いこと、メンテナンスを継続することはとても大変であることを思い知りました。 その経験から、ソフトウェアのメンテナンスの負担を減らすこと、開発時に発生する手戻りを減らすことはソフトウェア開発が行われる限り常に要求されることだと考え、それらに興味を持つようになります。 その私の考えがSWETのミッションとマッチしていると考えたため、私はSWETへの参加を志望しました。

SWETのミッション

ソフトウェアテストを起点とし、DeNAのサービス全般の品質向上とエンジニアの開発生産性向上に対して貢献していくことをミッションとする

SWETに配属されてどんな仕事をしているのか

Unityプロジェクトに導入するための静的解析器の開発をしています。 C#向けの既存の静的解析器はいくつかOSSとして公開されていますが、以下の理由により自作する必要があります。

  • 社内独自フレームワークに起因する問題を検査できない
  • 既存の静的解析器のルールでは、検出したい問題を見逃してしまうことがある

では、自作の静的解析器を作成するために行った/行っているフローを以下に示します。

  1. 診断の対象を決める
    • ECMA334やMicrosoft社のドキュメントを参考に、C#の構文を調べながら、診断の対象になるものを決めて、洗い出す(ここが一番大変)
  2. 仕様をデシジョンテーブルにまとめ、テストケースを洗い出す
  3. 作成したデシジョンテーブルをもとにテストコードを1件ずつ追加
  4. 静的解析器を実装し、テストを通す
  5. 3に戻る

静的解析器を開発プロジェクトで使ってもらうためには、適用する開発プロジェクトの人の信頼、すなわち静的解析器に対して好印象を持って貰う必要があります。 そのため、正確に静的解析ができているかを検証し、証明するためにテストを書くことは重要です。

要約すると、テストのノウハウを学びつつ、静的解析器を実装する仕事をしています。

SWETの人たちが、新卒目線でどう見えるか

ミスを未然に防ぐことや手戻りを防ぐことに意識をおき、そういった可能性がある箇所に対しての改善を妥協しない人が多い印象です。 特にメンターから教わったことの中から印象に残ったものを以下に示します。

テストメソッドの命名をしっかり意識する

SWETにjoinする前の自分は、テストメソッドの命名が雑でした。極端な例を出すと、goodcaseとかbadcaseなどと言った命名をしていました。

FizzBuzzを使った例

[TestMethod]
public void GoodCase()
{
    var actual = FizzBuzz(3);
    Assert.Equal("Fizz", actual);
}

このように、テストメソッドの命名が雑だと以下のようなデメリットがあります。

  • テストが失敗したとき、失敗したメソッド名やメッセージを見ただけでは失敗箇所の特定が行えず、深くコードを追う必要がある

そこで、テスト対象メソッド名_テスト対象に与える条件_期待する結果のような命名を意識しました。

[TestMethod]
public void FizzBuzz_入力値が3_Fizzを返す()
{
    var actual = FizzBuzz(3);
    Assert.Equal("Fizz", actual);
}

これにより、上記のデメリットを解消できます。

テストの無いリファクタリングはリファクタリングとはいわない

この言葉は自分にとても刺さりました。 リファクタリングとは、プログラムの外部から見た振る舞いを変えずに、内部構造を変えることだとした上で話しを進めます。

テストを書き、実行することで、リファクタリングを試みた際に発生したリグレッションを検知できます。 しかし、当時の自分は自動テストを実施せずに目視のみの確認でリファクタリングを完了としていました。 そのため、抜け漏れが発生し、リグレッションを起こしてしまったことが多々ありました。

リグレッションを放置していると、ボディーブローのように後々効いて、気づいたときには巨大なバグになっていることも少なくありません。 こういった状態を作らない、妥協しない姿勢をSWETの方から見習い、吸収していきたいと思いました。

新卒でSWETに入って良かったと感じたこと

テストケースの考え方や、テスト自動化の導入方法は、どんなソフトウェア開発にも必要です。 また、テストを導入するためには、テストを導入しやすい環境を作らなければなりません。

これらのテクニックは、これからも陳腐化することはないスキルだと自分は確信しています。 そういった潰しの効くスキルを基盤として身につけながら業務に携わることができてとても良かったと感じています。

また、こういったテスト技術を基盤として様々なソフトウェアに関わることで、様々なソフトウェアの知識を吸収ができるのも良さだと考えています。

終わりに

新卒でSWETに入って、うまく仕事を遂行できるか不安ではありましたが、毎日新しい発見がありとても学びになっています。 また自分の施策が、ソフトウェア開発を促進するための施策に関われていることを実感でき、今の仕事にとてもやりがいを感じています。 新卒でSWETに入ったことを自分は正解だと感じており、今後もSWETで学んだことを様々なソフトウェア開発の促進のために生かしていきたいと考えています。

SWETは中途しか採用していないイメージを持たれている方が多いのかも知れませんが、新卒で入ることも可能です。SWETは学びが多い部署だと思っているので、新卒でSWETに入る選択もありだと思います。 もしSWETに興味を持った方がいれば、是非お話しましょう!