QUO CARD Digital Innovation Lab Tech Blog

クオカード デジタルイノベーションラボの技術ブログです

QUOカードPay オンラインストアをリライトしました

こんにちは、日本のテトリスプレイヤーの上位 0.5% に入るデジタルイノベーションラボの飯島です。 今回は Java のプログラムをリライトした話を書きます。

背景

2020年6月にQUOカードPayオンラインストアの内製化を行いました。 その時は EC2 で動いていた Java プログラムを ECS に乗せるために必要な修正を加えましたが、ソースコードの大半には手を加えていませんでした。 内製化から1年と少し経ちますが、バグの修正や新規機能追加の際に複雑なソースコードの解読に時間を取られることが多く、これはまずいということで2ヶ月間他の機能追加無しでリライトをすることにしました。

方針

プロダクトの全体的な設計から見直すのが理想でしたが、そこまでやると果てしない時間がかかってしまうため、少ない時間で効果が得られそうな箇所や、今後よく触りそうな箇所を重点的にリライトするようにしました。

実施した内容

Kotlin 化

QUOカードではサーバーサイド実装の言語としてKotlinを採用しています。 今回リライト対象となるコードはJavaで書かれていたため、まずKotlin化をすることにしました。 Kotlin化により nullとmutableの扱いが限定されるので、可読性の向上とバグの減少が見込まれます。

null まわりの修正

元々のソースコードでは @Nullable や @NotNull が使われていなかったため、引数に null が入ってくるのか、戻り値として null が考えられるのかがぱっと見でわからないようになっていました。 そのため不要な null チェックが蔓延していたり、また必要な箇所に null チェックが足りなかったりという状態でした。

Kotlin では基本的に null は許容されず、型の後に ? をつけることで nullable になります。

val a: String? = null
val b: String = null // コンパイルエラー

Java から Kotlin に変換する際には型に ? をつけておき、可能なら ? を削除するという方針でリライトしました。 ただ、この修正は当初考えていたよりもずっと難しく、まだかなりの量の ? が残ってしまっています。 理由としては、元々のソースコードを読んでも使われている値が null を許容するのかしないのかの判断が難しかった為です。 例えばメソッドの引数で言うと、そのメソッドを呼んでいるメソッド、そのメソッドの引数がそのまま使われているなら更にその呼び出し元、というように追いかけて虱潰しに調べないといけません。 クラスのフィールドで言えば、それが使われている全ての箇所を調べないと判断ができません。

変数の不変化

Kotlin では、val で宣言した変数は不変 (immutable) になり、後から変数に値を代入しようとすることができません。これにより、一度初期化した変数がソースコード中のどこかで変更されないことがコンパイラレベルで保証されるので、変数に何が入っているのかを把握するためにソースコードを深く潜っていかなくてもよくなります。(詳しくは「副作用」の箇所に書きます) リライトにあたり、ローカル変数でもクラスのフィールドでもとにかく可能な限り val で宣言することを意識しました。

綴りミス(タイポ)を修正

当たり前ですが綴りミスは無い方が良いです。 コードリーディングの妨げになりますし、正しい綴りで検索をかけた時に引っかからなくなります。 例えば、Downloadで検索したのに、Donloadと綴りミスしていたら一生引っかかりません。 (DonloadはもしかしたらDonald なのでは?と社内で議論になりました) 今回は一つのクラス内に2, 3個綴りミスがあるというレベルだったので、他の項目のリライト中に直せる箇所だけ直す、という対応をとりました。

修正時の注意点

thymeleafなど実行時に名前を解決する処理がある場合、Java側の綴りミスを直してコンパイルが通っても実行時にエラーが発生します。 この部分のリライトは影響範囲の調査に時間がかかるため、今回は後回しにしています。

String の配列をクラスに変更

例えば名前と年齢を持つ犬を表現したい時、一般的にはクラスを作ると思います。

data class Dog(val name: String, val age: Int)

これをStringの配列で表している箇所がありました。

// 0番目の要素に名前、1番目の要素に年齢を入れる
String[] dog;

年齢を取り出す場合は Integer.parseInt(dog[1]) となっています。 さらに、犬をたくさん扱う場合はこうなっていました。

String[][] dogs;

実際にはdogsのような体を表す変数名はつけられておらず、result のような名前だったため、変数宣言から何を意味するのか読み取れず苦戦しました。 こちらは Dog クラスを用意することで対応しました。

フラグ保持用の Map<String, String> を List に修正

飼い主に対して複数の猫が紐づくモデルを考えます。 単純に DB から猫を取ってくると、このようなクラスにマッピングできます。

class Cat {
    private Person person; // 飼い主
    private String name;
}

DB から取ってきた List に対してループを回す中で、既に登場した飼い主を保持しておく Map を作っている箇所がありました。

Map<Person, String> map;
for (Cat cat: cats) {
    if (!map.get(cat.person).equals(“”)) {
        // キーが存在しない場合の処理
    } else {
        // キーが存在する場合の処理
        map.put(cat.person, “”);
    }
}

最初はこの map が何をしているのかを理解するのに時間がかかりましたが、単純に処理済みの飼い主を保持しているだけということがわかりました。

val processedPersons: List<Person>;

としてリストに持たせて変数名を分かりやすくすることで解決しました。

private メソッドを別クラスの public メソッドにする

他のクラスが持つべきメソッドが private メソッドとしてコントローラーやサービスに実装されていたので、そういうものはあるべき場所に実装を移動しました。

private String getDogInfo(Dog dog) {
    return dog.getName() + “ ” + dog.getAge().toString();
}

⬇️

data class Dog (val name: String, val age: Int) {
    val info: String
        get() = “$name $age”
}

String クラスや各種ライブラリが提供するクラスなど、自分で修正することができない型に対するメソッドに関しては、拡張関数を作成して対応しました。 型をラップしたクラスを作って必要なメソッドのみを生やすという方法も考えられますが、今回は手間も考慮して拡張関数方式にしました。 拡張関数を使うと、メソッドのネストをチェーンで記述できるようになるので、単純な可読性も向上しました。

private String addExclamationMark(String s) {
    return s + “!”;
}

public void a() {
    addExclamationMark(
        addExclamationMark(
            addExclamationMark(“hello”))) 
}

⬇︎

fun String.addExclamationMark(): String = “$this!fun a() {
    “hello”
        .addExclamationMark()
        .addExclamationMark()
        .addExclamationMark()
}

メソッドから副作用を取り除く

副作用のあるメソッドはコードリーディングの妨げになります。

public class Fox {
    private String name;
    private int age;

    // setter & getter
}

public void a() {
    Fox fox = new Fox();

    setName(fox, “taro”);
    fox.setAge(3);

    System.out.println(fox);
}

private void setName(Fox fox, String name) {
    fox.setName(name + “-chan”);
}

このコードでは、最初に宣言された時の fox と println した時の fox では状態が異なります。println 時点での fox の状態を知るには、fox が宣言された場所から println される場所までの全ての行を読まないといけません。もし fox が宣言された時点から変更されないことが保障されるなら、println がどれだけ離れていても fox の状態はすぐ把握できます(そもそも変数の宣言と使用箇所が離れていること自体がよくないですが)。

data class Fox (val name: String, val age: String)

fun a() {
    val fox = Fox(getName(“taro”), 3)
    println(fox)
}

private fun getName(name: String): String = “$name-chan”

Kotlin だとこのように書けば、宣言時から変更されることはありません。

副作用を持つメソッドの見分け方として、まず戻り値のない(voidの)メソッドは怪しいです。setXXX という名前のメソッドもほぼ副作用があります(中には BigDecimal のように新しいオブジェクトを返すものもありますが)。あとはリライト中のメソッドから呼び出しているメソッドの中を逐一確認していきます。副作用を持つメソッドを呼び出しているメソッドも副作用があることになるので、呼び出し階層を全て辿る必要があります。

修正方法ですが、副作用としてセットする値を返すようにします。上の例でいうと、setName の中で fox に値をセットしていたところを、getName として値を返し、呼び出し元の方で使用するようにしています。 何重にもネストしたプライベートメソッドの中に副作用がある場合は、こうやって少しずつ副作用を上のメソッドに押し出していき、最後に呼び出し元で使用するようにします。 副作用の無いメソッドが完成したら、副作用のある元のメソッドは Deprecated でマークし、「副作用があるため、極力 XXX を使用すること」とコメントしておきます。 こうすると Intellij などの IDE では呼び出し箇所に取り消し線が引かれるため、今後リライトするときの目印となります。

終わりに

リライトをすればするほど「これは一から書き直した方が早いのでは。。」という気持ちに見舞われましたが、実際はソースコードの奥深くに隠された仕様を発見するための時間が必要になるので、やっぱり時間が取れたタイミングでリライトしていくのが現実的かな、とも思いました。

また、技術的負債は放っておくと返済にかかるコストが上がっていくと言うのを身を以て体験しました。クリーンアーキテクチャの最初の方に、「時間がある時に綺麗にすればいいと思うかもしれないが、そんな時間は一生来ない」のようなことが書いてありましたが、自分も実装するときはそれを意識しようと思いました。今回は何をするにもソースコードの解読に多大なコストがかかってしまう状態になっていたのでリライトの時間を設けざるを得ませんでしたが、最初から綺麗なコードだったらもっと生産的なことに時間を割けたのに、、と考えると、常にその時点で最も読みやすいコードを書く努力は大事だと思います。(ただ、リライトしてコードを綺麗にしていく作業自体は楽しかったです。)

java.time.LocalDateTime と時差について

こんにちは、日本のテトリスプレイヤーの上位 1% に入る飯島です(デジタルイノベーションラボ所属)。 日付を扱う時によく使用される java.time.LocalDateTime ですが、意外と時差のことを気にせず使っている方も多いのではないのでしょうか。今回は LocalDateTime と時差周りの話、その他 java.time に含まれるクラスの話をしていきます。

LocalDateTime について

LocalDateTime は時差情報を持っていません。 つまり、2020-11-24T12:00:00 を表す LocalDateTime があったとして、それがいったいどの地域に置ける時刻なのかは不明だということです。 日本における 11/24 12:00 はイギリスでは 11/24 3:00 なのに、LocalDateTime ではそれを表すことができません。 日本のシステムと海外のシステムを API 連携するとき等に LocalDateTime でやりとりしていると不整合が出てきてしまいます。 クラウドのリソースを使っていて、アプリケーションサーバーのタイムゾーンと DB サーバーのタイムゾーンが異なっている時も不整合が出てきてしまいます。

不整合が発生する例

弊社では ORM に jOOQ を使うことが多く、jOOQ の java コード生成機能でテーブルに対応するクラスを作って DB を操作するようにしています。 テーブルが timestamp 型のカラムを持つ場合、生成されたコードでは LocalDateTime が対応づけられます。 この場合、アプリケーションサーバーが UTC+9:00, DB サーバーが UTC+0:00 などオフセットが異なっていても、アプリケーションサーバーで作成した日時が DB に保存されます。 例えばアプリケーション側で LocalDateTime.now() をして 2020-12-21 11:44:49.288105 が生成されると、DB にもそのままの形で保存されます。 この値を UTC+0:00 などの別アプリケーションから読み込むと、元々の時差分がずれた日時が取得されてしまいます。

※ timestamp with time zone 型を使うことで時差に対応することは可能です。ただ、自分たちで設計した DB ではない場合など、timestamp 型が前提となっている場面は多々あるかと思います。

じゃあ何を使えば良いのか

基本的には java.time.Instant か java.time.OffsetDateTime を使えば良いかと思います。 LocalDateTime が求められる場面では、LocalDateTime.ofInstant(instant, ZoneOffset.UTC) や LocalDateTime.now(ZoneOffset.UTC) などで 変換するのが良いでしょう。

Instant

時系列上のある一点を表すクラスです。 時差は持っていませんが、LocalDateTime が時系列上の点を表わせないのに対して、こちらは UTC に固定された一点を返します。 試しに手元で現在時刻を表示させてみると、こうなります。

> Instant.now()

2020-11-30T08:35:16.088031Z

画面に Asia/Tokyo の時刻を表示したい場合など、ユーザーが Asia/Tokyo を望んでいる場合は、アプリケーション側で変換して出力すれば良いです。

OffsetDateTime

標準時からの時差情報を含んだデータです。 UTC+8:00 の 13:00 や UTC+9:00 の 15:00 といったデータを持っています。

> OffsetDateTime.now()

2020-11-30T17:35:16.101819+09:00

※ OffsetDateTime#now はシステムのデフォルトタイムゾーンを使用して計算されるため日本の日時が表示されていますが、情報として UTC+9:00 が含まれているため時系列上の一点を表せています。

ZonedDateTime

国・地域ごとの時差情報を持った時刻データです。 1 年を通して常にオフセットが固定の地域もありますが、サマータイムにより 1 年のうちでも標準時を 進める/戻す などオフセットが変わる地域も存在します。 ZonedDateTime ではそういった地域の時差情報を表すことができます。

現在の日本は一年を通してオフセットは一定(UTC+9:00)ですが、実は過去にサマータイムを導入していた時期がありました。 下記コード (kotlin で書いてます) でも確認できます。 1948 年 5 月 2 日の 0:00 ~ 1:00 が存在しないことがわかります。 オフセットも 9 時間から 10 時間になっていますね。

val zonedDateTime = ZonedDateTime.of(1948, 5, 1, 20, 0, 0, 0, ZoneId.of("Asia/Tokyo"))
(1..10).fold(zonedDateTime) { dt, i ->
  dt.plusHours(1).also { println(it) }
}

1948-05-01T21:00+09:00[Asia/Tokyo]
1948-05-01T22:00+09:00[Asia/Tokyo]
1948-05-01T23:00+09:00[Asia/Tokyo]
1948-05-02T01:00+10:00[Asia/Tokyo]
1948-05-02T02:00+10:00[Asia/Tokyo]
1948-05-02T03:00+10:00[Asia/Tokyo]
1948-05-02T04:00+10:00[Asia/Tokyo]
1948-05-02T05:00+10:00[Asia/Tokyo]
1948-05-02T06:00+10:00[Asia/Tokyo]
1948-05-02T07:00+10:00[Asia/Tokyo]

国・地域ごとの時差情報は、デフォルトでは tz database から取得するようになっています。


以上、java.time.LocalDateTimeは時差情報を持っていないので使うときは気をつけましょうという話でした。 それでは楽しい時差ライフを!!

CIのテストを修正した資産によって分けて実施する

こんにちは、クオカード デジタルイノベーションラボの鳥海です。

QUOカードPayオンラインストアは内製化に切り替えてからもう少しで半年ほど経ちます。
内製化に切り替えた後は、新たな機能追加とリファクタリングを進めており、それぞれで unit test が作成できるものは作成し、CI による自動テストを実施するようにしています。
CI には GitHub Actions を使用しています。

自動テストを修正のたびに都度実施することにより、安心して既存の機能が改修ができたり、リファクタリングが行えるようになったのですが、テストソースが増えるにつれて以下のような問題が発生してきました。

  1. 一部のモジュールの修正しかしていないのに、全てのテストを実施しており無駄な待ち時間や 余計な GithubActions のリソース消費が発生していた
  2. たまに実行中のまま止まってしまうテストがでてきた

1 に関しては、静的資産(HTML、CSS、JS など)のみ修正した場合など、不要な場合もテストを実施していたので、こういう場合は実施しないようにすべきでした。
2 に関しては本来、事象を解析して直したいところでしたが、作業の優先度も考慮し該当のテストを無効にして対応していました。ただ今後その機能が修正された場合はテストを実施すべきであるのと、担当者内で無効にしていることを共有できているうちはよいのですが、新しいメンバーが増えた際に伝達漏れなどが発生し得ると思いました。

上記の点を解決するために、毎回テストを一律実施するのではなく、修正した資産によってテスト絞って実施できないか調べてみました。

GitHub Actions のテストワークフローの実施を GitHub に push したパスによって絞る

GitHub Actions のpathsを使うと、修正したリソースパスを指定してワークフローが実行できるようになります。

下記のようにパスを指定すると push した際に差分 が発生した資産がそのパスに含まれていれば、ワークフローが実施されます。
また、指定したパスの中で除外したいものがあれば!を付与したパスを下行に追加することにより除外できます。
パスの指定方法にはワイルドカードが指定可能です。 指定方法の詳細はここを参照ください。

GitHub Actions のワークフローファイル

on:
  push:
    paths:
    - 'module1/**'
    - '!module1/function1/**'

以下のようにワークフローを実施しない資産のパスを指定することもできます。

on:
  push:
    paths-ignore:
    - 'module1/**'

これで push した際にワークフローを実施する資産を特定できるようになったのですが、これだけだとテストごとにワークフローファイル作らなくてはいません。

ワークフローでテスト以外は Github リポジトリのチェックアウトや Java のセットアップなどがありますが、テストごとにファイルを分けると、これらは各ワークフローで重複することになり、これはよろしくないなと思いました。

そこで一つのワークフローファイルの中で修正された資産を判定し、テストを分けて実施できないか調べてみました。

テストワークフロー内で、修正した資産を判定できるようにする

現時点で GithubActions に用意されている機能ではワークフロー内で修正資産を判断できるものがないため、以下のプラグインを使用してどの資産に修正が入ったか判定できるようにしました。

dorny/paths-filter

このプラグインを使用して資産ごとにテストを分けたワークフローのソースが以下となります。

jobs:
  test:
    name: Run tests
    runs-on: ubuntu-latest

    -- 省略 --

    steps:
      # 修正された資産を判定する※
      - name: paths-filter
      uses: dorny/paths-filter@v2
      id: filter
      with:
        base: ${{ github.ref }}
        filters: |
          module1:
            - 'module1/**'
          module2:
            - 'module2/**'
          module_common:
            - 'module_common/**'

      # モジュール1のテスト
      - name: module1 test
        id: module1-test
        if: steps.filter.outputs.module1 == 'true' || steps.filter.outputs.module_common == 'true'
        run: ./gradlew module1Test

      # モジュール2のテスト
      - name: module2 test
        id: module2-test
        if: steps.filter.outputs.module2 == 'true' || steps.filter.outputs.module_common == 'true'
        run: ./gradlew module2Test

      # 共通モジュールのテスト
      - name: module_common test
        id: module_common-test
        if: steps.filter.outputs.module_common == 'true'
        run: ./gradlew moduleCommonTest

      # モジュール1のテスト結果をアップロード
      - name: store reports of module1
        uses: actions/upload-artifact@v2
        # テストが実行された場合だけアップロードする
        if: always() && steps.module1-test.outcome != 'skipped'
        with:
        name: module1-report
        path: module1/build/reports

      # モジュール2のテスト結果をアップロード
      - name: store reports of module2
        uses: actions/upload-artifact@v2
        # テストが実行された場合だけアップロードする
        if: always() && steps.module2-test.outcome != 'skipped'
        with:
        name: module2-report
        path: module2/build/report

      # 共通モジュールのテスト結果をアップロード
      - name: store reports of module_common
        uses: actions/upload-artifact@v2
        # テストが実行された場合だけアップロードする
        if: always() && steps.module_common-test.outcome != 'skipped'
        with:
        name: module_common-report
        path: module_common/build/report

ソースの※の箇所がプラグインを使用しているところです。 base: ${{ github.ref }}を指定すると、そのブランチのHEADとの差分を比較することになります。(指定しないと分岐元のブランチとの差分比較となります) フィルターを通すと、steps.filter.outputs.<フィルタ名>で修正した資産を判定して、それに応じてテストを実施できるようになります。

また、複数条件を指定することで判定文が長くなるため、テスト結果のアップロードでは同じ条件を指定せず、テストステップに id を付与し、その id のテストが実施されたかを steps コンテキストのoutcome を使用して判定しています。

これでテストをワークフロー内で呼び分けることができました。
次は実施するテストを分けます。

Gradle でテストを実施したい単位に分ける

クオカードペイオンラインストアではビルドツールに Gradle を使用しています。
Gradle では、以下のようにincludeexcludeを使用して、タスクの対象資産を特定することができます。

module1/build.gradle

task module1Test(group: 'verification', type: Test, dependsOn: testClasses) {
  useJUnitPlatform()
  # function1配下は除いたテスト
  exclude '**/module1/function1/**'
}

task function1Test(group: 'verification', type: Test, dependsOn: testClasses) {
  useJUnitPlatform()
  # module1のfunction1のみのテスト
  include '**/module1/function1/**'
}

上記で示した方法を柔軟にテストを分けて実施することができるようになります。
これで修正した資産に影響するモジュールのみテストを行うようにしたので、問題点を解消できました。

テストの並列化

テストは一つのジョブでなく、複数のジョブに分けて定義し並列で実施するようにもできます。
GithubActions では優先度が同じジョブは並列で実施されます。
ただし、各ジョブ間ではセットアップしたものなどのリソースを共有できません。
共通処理をそれぞれ行わなくてはいけなく、今回はそれに伴う時間と並列化により効率化される時間を比較して、結果的に並列化は実施しませんでした。

ただ dorny/paths-filter プラグインの判定結果をジョブを起動する条件にも使えるので、今後テストが増え、テストに大幅に時間がかかるようになった際は並列化をすることで時間の短縮が可能になると思われます。

以下のようにすると module1 と module2 のテストが並列で実施されます。

jobs:
  filter:
    name: Filter
    runs-on: ubuntu-latest
    outputs:
      # 変更した資産の判定結果を他のjobに渡す
      module1: ${{ steps.filter.outputs.module1 }}
      module2: ${{ steps.filter.outputs.module2 }}
      module_common: ${{ steps.filter.outputs.module_common }}
    steps:

      -- 省略 --

      - name: paths-filter
      uses: dorny/paths-filter@v2
      id: filter
      with:
        base: ${{ github.ref }}
        filters: |
          module1:
            - 'module1/**'
          module2:
            - 'module2/**'
          module_common:
            - 'module_common/**'

  test-module1:
    name: Run tests module1
    runs-on: ubuntu-latest
    needs: filter
    if: needs.filter.outputs.module1 == 'true' || needs.filter.outputs.module_common == 'true'

    -- 省略 --

  test-module2:
    name: Run tests module2
    runs-on: ubuntu-latest
    needs: filter
    if: needs.filter.outputs.module2 == 'true' || needs.filter.outputs.module_common == 'true'

    -- 省略 -

色々無駄なところを省くことによって、各メンバーがより効率的に作業ができますし、GitHub Actions の使用リソースも抑えられます。
最初は問題となっていなくてもプロジェクトの規模が大きくなっていくにつれて問題となってくるものもあるので、見直すことで最適化をはかり、非効率となっているものを取り除いていくことも開発を進めるうえで大事だと思いました。

QUOカードPay オンラインストアのリライトはじめました

クオカード デジタルイノベーションラボの齋藤です。 2020年6月にQUOカードPay オンラインストアの内製化を行いましたが、そのタイミングでは修正しきれなかった問題が残っていました。 今後の機能追加や改修を行う上での障害になる為、方針を決めた上でリライトを進める事にしました。

方針

  • 修正することが多いWebのバックエンドから着手する。

  • できるだけ部分的に修正する

Pull Requestが巨大になってしまうとコードレビューが難しくなる為、またリリース時にトラブルが発生するリスクも大きくなるため、できるだけ局所的な改修になるように進めています。

  • 細かい部分に拘りすぎないようにし、まずは全体的に改善する

一旦全体的に厳しい部分を修正したい為、まずは修正内容で記載した内容のみを修正し、それ以外の問題については後回しにすることにしました。

今回導入するライブラリ

以下のライブラリを今回新たに導入しました。

通常は kotest + MockK という組み合わせになると思いますが、Kotest のクラス群でコンストラクターにわたす関数ないしは、 init で書く処理は、関数の見た目とは反してクラスボディにかなり近い性質を持つという特性があります。 そのため、テストを書いているときに値を関数の見た目に騙されて変数等の状態と誤解してテストを書いてしまうケースがあり、今回のようにベンダーから引き継いだ状態のよくわからないコードをリファクタリングしていく際にテストが悪いのかプロダクションコードが悪いのか混乱する可能性が考えられたので、書いている状態を勘違いしづらい JUnit を使った方がよいと判断しました。

  • MockK

Kotlin で作ったクラスでもモックできるというのが特性(Mockito だとプロダクションコードを open にしないといけない)なので採用することが多いですが、 Kotlin で使うようにする目的で作られているので Kotlin の用語にあわせた API になっているというのもポイントであると考えています。 ( Mockitoの場合、動作を記録するのに、 Kotlin の予約後である when という API の使用が絶対で、その場合に

`when`

と書かないといけないため、 Kotlin では使いづらいというのがあります ) ただし、 spring-test の MockBean ではMockitoが使われているので、そのままMockito を使っています。

  • spring-test

優先する修正内容

  • nullをOptionalで書き換える

  • フィールドインジェクションをコンストラクタインジェクションに変更する

  • デッドコードを消す

  • できるだけテストを書く

全てテストを書こうとすると非常に時間がかかってしまう可能性がある為、今回は難しい手動でテストする形も許容する事にしました。

  • 依存を減らす

テスト作成を容易にするため、クラスを分割するなどし依存しているモジュールの数を減らそうとしています。

2巡目以降のリファクタリングで対応する予定のもの

今回はサーバーサイドを優先に進める事にしましたが、以下についても今後対応していこうと思います。

  • フロントエンド

  • バッチ

  • Kotlin化

  • できるだけ参照透過に書き換える

リモートでのユーザビリティテスト実施

こんにちは。

今まで対面で実施していたユーザビリティテストを初めてリモート環境で実施したので、ブログでご紹介します。


実施の背景

現状では対面での実施が難しく、リモートでの実施を検討することとなりました。


実施までの準備

スクリーニングアンケートの作成配信、評価項目、テストタスク、全体スケジュールなどの準備は通常通り進めましたが、操作してもらうUIをどのように用意するかと進行方法については悩みました。弊社はデザインにFigmaを利用しているので、Figmaでプロトタイプを作って操作してもらうのも検討したのですが、被験者の方にアプリをインストールしていただいたり、スマートフォンの環境によってはうまく動作しなかったりといったことが想定され、実施や実施準備への影響が懸念されたので、今回は採用しませんでした。(別のプロトツールですが、過去に在籍した企業でそのような経験もしたので。。。)代わりに画面キャプチャを貼り付けたスライドを用意し、被験者の方に口頭で操作する場所を発話してもらい、こちらで画面を切り替えて擬似的に操作していただくこととしました。普段のユーザビリティテストで行う脳内で考えていることを出来るだけ声に出して操作してもらう延長のイメージです。 調査は準備が重要と言われていますが、今回はリハーサルが本当に重要でした。もし今までと同じ感じでリハーサルを軽くしていたら、、、1人分の結果を無駄にしていた可能性が大きかったです。


実施

Zoomを利用して既存UIと新規UIの評価を上記のとおり紙芝居形式で実施しました。


対面との違いで感じたこと

今回の形式は、実際にご自身で操作できないため、普段よりも考えて操作することになるので、操作に行き詰まった時に色々と探して操作する部分を拾いづらい場合がありました。また、同じ空間に居ないので、感情を感じ取るのが難しかったです。なので、色々と聞き出すために普段よりもこちらが話すことが多かったように感じます。もちろんペーパープロトなので説明なども多いので意外と喉が乾きました。こまめに喉を潤わす準備も忘れずにしたいです。


実施して良かったこと

初めての試みだったので、少し不安を感じていましたが、課題を抽出でき、改善案の検討に繋がった活動になりました。絶賛開発中ですので、お楽しみにしていただけると嬉しいです。また、過去に実施した際は都内で開催していたのですが、今回はお住いの場所に限らず、より幅広くご参加いただけたと思います。 インタビュー調査はすでにリモートで実施ていましたが、ユーザービリティテストもリモート実施の実績ができたので、現在の対面が難しい状況がいつまで続くか分からないですが、生成的調査、検証的調査を状況に応じて使い分けながらプロダクト開発を進めていけるということが確認できました。見学する人数が多くても被験者の方に圧迫感を与えずに済むのもメリットとしてありそうでした。


最後までお付き合いありがとうございました。以上となります。

GitHub Actions の composite run steps action を 試してみた

今回はちょっと前(2020 年 8 月 7 日)に公開された GitHub Actions の composite run steps action を 試してみました。これは Dockerfile や node js のコードを書かなくても(書けなくても) 通常の GitHub Actions のワークフロー YAML が書ければ、 action を作成・公開できるという仕組みです。

続きを読む