読者です 読者をやめる 読者になる 読者になる

そのメールアドレス、現在も使っていますか?

こんにちは。ユーザーファースト推進室ディレクターの大黒です。

ありがたいことにクックパッドは今年で20年目をむかえ、数多くのユーザーに利用されるまでに成長しました。それ故に発生する課題もあり、今回はその中でもユーザー登録に使われているメールアドレスの課題と対策をご紹介したいと思います。

ユーザー登録の仕組み

クックパッドのユーザー登録では、下記の項目が必要となります。

  • メールアドレス
  • パスワード
  • 郵便番号
  • 生年月日

※iOSアプリでは郵便番号と生年月日は任意入力となります

f:id:kotsuru0812:20160628234150p:plain

SNSアカウント認証や認証コードでのアクティベートを採用するサービスが今では主流ですが、20年続くサービスであれば一般的なユーザー登録フローではないでしょうか。しかしながら最近のスマートフォンユーザーの多くはメールを使わないという実態も分かっているため、ユーザー登録にメールアドレスを使い続けるかどうかは、別途議論を進めているところです。

メールアドレスの課題

一度取得したメールアドレスを生涯にわたって使い続けるという人は多くありません。SNSやメッセージアプリが普及した今では、メールでやり取りをすることが少なくなり、ナンバーポータビリティが可能になったことも相まって、キャリアのメールアドレスが変わることへの抵抗感はほぼありません。また、登録した時のメールアドレスとパスワードさえ覚えていれば、実際にそのメールアドレスを使っていなくてもログインできるので、メールアドレスを変更せずに使い続けるという人も出てきます。

例えば私たちが把握しているメールアドレスの課題としては、次のようなものを挙げることができます。

3〜6ヶ月でメールアドレスは再利用される

キャリアやプロバイダーにもよりますが、使われなくなったメールアドレスは3〜6ヶ月程度の保留期間を経て、再利用が可能になります。メールが普及してからの年月を考えると、多くのメールアドレスが再利用されていると予想されます。

登録できない人がでてくる

クックパッドでは、1つのメールアドレスで作れるアカウントの数は1つだけです。もしユーザーAさんが取得したメールアドレスが、過去にクックパッドのユーザー登録に使われていて、誰かがメールアドレスを変更せずに使い続けているとしたら、その人がメールアドレスを変更するか、ユーザーAさんが別のメールアドレスを取得しないとユーザー登録ができないことになります。

以上のことをふまえると、フィーチャーフォン全盛期からサービスを続けているクックパッドとしては、ユーザー登録、もしくはメールアドレス変更から一定期間経過しているユーザーに対して、登録しているメールアドレスを現在も使っているかどうかを確認することが大切だと考えました。

最小の実装で確認施策を行う

ユーザーにメールアドレスが最新であるかどうかを確認するような施策は、他社ではあまり事例がなく、社内でも知見が全くない状態から始まりました。このような場合はなるべく既存の仕組みを使い、対象者をできるだけ絞り込んでコンパクトなサイズでまず試してみることが大切です。

重要なお知らせという仕組み

クックパッドには返金などが必要になったユーザーに対して、案内を通知する仕組みを持っています。これはアプリで使われている仕組みですが、PUSH通知とアプリのトップスクリーンに導線が表示されるため、ユーザーに内容を確認してもらえる確率が非常に高くなります。今回はこの仕組みの一部を使い、何度かテストしてみることにしました。

f:id:kotsuru0812:20160628234542p:plain

対象者を定義する

メールアドレスの課題を考慮すると、キャリアのメールアドレスで登録、またはキャリアのメールアドレスに変更してから一定期間経過しているユーザーの場合、機種変更などによりメールアドレスが変わっている可能性が高いという仮説がたてられます。あとは一定期間をどうするかですが、メールアドレスの保留期間が3〜6ヶ月程度なのを考慮すると、6ヶ月以上経過している人たちを対象とした方がよさそうということになりました。

施策の実施

f:id:kotsuru0812:20160628234637p:plain

施策の評価は下記の数値を元に行いました。

  • 変更率
    メールアドレスをいまは使っていないと判断し、新しいメールアドレスの登録が完了したユーザーの割合

  • 変更手続き中
    メールアドレスをいまは使っていないと判断し、新しいメールアドレスの登録が完了していないユーザーの割合

  • アクション率
    上記の変更率と変更手続き中に加え、「使っているメールアドレスです」と明示的に回答してくれたユーザーの割合

第1回テスト

PUSH通知を大量に配信すると、サイトに思わぬ負荷がかかる場合があるので、インフラ面でも問題がないことを確認する必要があります。なので最初のテストでは、無事にPUSH通知が配信できるか、ユーザーがPUSH通知を開いてくれるかという点を中心に検証しました。

配信数 変更率 変更手続き中 アクション率
300 1% 1% 35%

結果を見てみると、多くのユーザーがPUSH通知に反応してくれたことが分かります。インフラ面でも問題がないことが分かったため、今度はメールアドレスを変更したい人たちが無事に変更することができるかを検証することにしました。

第2回テスト

このテストではある程度配信数を増やし、メールアドレスを変更するユーザーが多くなるよう抽出条件を変更し、メールアドレスの更新が古いユーザーを対象としました。

配信数 変更率 変更手続き中 アクション率
1,000 5.1% 1.8% 40.3%

結果としては変更手続き中で止まっているユーザーが、前回と同じくらいの割合でいることが分かります。このことからメールアドレス変更フローを見直してみたところ、ユーザーが躓くポイントがあることが判明したため、既存のメールアドレス変更フローを改善することにしました。

メールアドレスを変更するには、パスワードの入力が必要になります。登録しているメールアドレスを現在使っておらず、パスワードを忘れてしまうと、パスワードの再設定を行うことができないため、メールアドレスの変更ができずフローの途中で止まってしまいます。

改善前 f:id:kotsuru0812:20160628234726p:plain

別件でメールアドレス変更フローの改善を行った際に、パスワードを忘れてしまった場合の考慮が漏れていました。この場合、サポートにお問い合わせをして本人確認を行うことで、パスワードの再設定をすることができるので、パスワード入力画面にお問い合わせへのリンクを設置することにしました。

改善後 f:id:kotsuru0812:20160628234814p:plain

この改善を行った結果、この導線から1日平均約3件のお問い合わせがありました。言い換えると、メールアドレスを変更しようと思ったがパスワードが分からず、どうしていいか困っていた人が1日平均約3人ほどいたということになります。

第2回のテスト結果としてはアクション率は前回と同じくらい高く、PUSH通知に対してのネガティブなご意見もなかったため、メールアドレス変更フローの改善を行い、施策としてはこのまま進めるという判断になりました。

まとめ

実際に登録しているメールアドレスを現在も使っているかどうか確認してみると、「使ってるメールアドレスです」というアクションをしてくれたユーザーもたくさんいて、ユーザーにとっては非常に関心が高いことだと分かりました。提供するサービスにもよりますが、サービス側から定期的に「登録してるメールアドレス使ってる?」と聞いてあげることは、とても大事なことです。しかしながら冒頭にも書いた通り、いつまでユーザー登録にメールアドレスを必須にするかについては引き続き議論を重ね、今後の改善に繋げていきたいと思っています。

実践 Pact:マイクロサービス時代のテストツール

技術部の taiki45 です。

以前「サービス分割時の複雑性に対処する: テスト戦略の話」という記事で、サービス間のインテグレーションテストにおける問題について紹介しました。現在のクックパッドではこの問題の解決のために Pact というツールを導入して運用しています。この記事では、その運用の知見を紹介できればと思います。

Pact

Pact は Consumer-Driven Contract testing (CDC testing) を実現するためのツールです。"Consumer"、"Provider" という見慣れない単語が出てきますが、この記事ではだいたい「Consumer = Web API クライアント」、「Provider = Web API サーバー」と対応ができます。この記事では具体的な Pact の利用例を通じて CDC testing がどういうものなのかについても紹介します。

必要になった背景

クックパッドでは、今までサービス間連携(他サービスの呼び出し結果を使うこと)の部分のテストには主に WebMock を使って書いていました。しかし、WebMock だと Provider が更新されても Consumer のスタブデータは更新されないので Provider 側の変更に Consumer のテストは追随できません。定期的にスタブデータを更新したり、RackVCR を使って Provider 側が変更されたら Consumer のスタブデータを更新することもできますが、それだとあくまで Consumer でのテストであるため、Provider が意図しない破壊的変更をリリースすることは防げません。それを防ぐには Provider のテストで Consumer に対する破壊的変更を検知できる必要があります。

そこで、Consumer の期待する振る舞いをプログラムから扱えるデータとして表現して、Provider に渡し、Provider 側でその「Consumer が期待する振る舞い」を満たしているか検証する手法で、Provider の変更がサービス間の連携を壊さないことを確かめることができます。この「Consumer が期待する振る舞い」を "Consumer-Driven Contract (CDC)" と呼んでいます。そして、この手法を実装に落とし込んだツールが Pact です。

Pact の仕組み

Pact では CDC は "pact file" と呼ばれるファイルへと JSON フォーマットで出力されて、Consumer の CI 時に生成されます。Consumer は CI 時に毎回 pact_broker と呼ばれるアプリケーションに pact file を "publish" し、pact_broker がバージョンや publish 日時といったメタデータと共に保管します。Provider は CI 時に pact_broker から自身が関連する Consumer が publish した pact file をチェックアウトして、新しい変更が Contract を壊していないか検証します。このテスト自体は pact file から RSpec の example を生成して実行することで実現されています。それぞれの example をパスさせるためには Provider のデータのセットアップが必要になることが多く、それは "Provider State" と呼ばれる機能で実現されています。Provider State は、Consumer が状態に一意な名前を付けて、Provider はその状態を再現するためのコールバックを登録できる仕組みです。

実際のフローは以下のようになります:

  1. Consumer プロジェクトがテスト内で Contract を記述する。Contract はリクエストと、そのリクエストに対応するレスポンスと期待する副作用が含まれる。
  2. Consumer のテスト実行時にモックサーバーを起動させる。Consumer は HTTP クライアントの向き先を起動したモックサーバーに向けてリクエストを発行する。モックサーバーは登録された Contract を元にレスポンスを返す。
  3. テスト成功後に Consumer は定義した Contract を JSON 形式でファイルに書き出し、pact_broker と呼ばれるサーバーにアップロードする。
  4. Provider は CI で pact_broker から関係する Consumer(s) がアップロードした Contract をダウンロードして、その Contract 通りに自身が振る舞うかテストを実行する。

work_flow_of_pact

https://github.com/realestate-com-au/pact/blob/v1.9.2/README.md より

実際のコードを見たほうがより理解しやすいと思うので、ここで示します。

次のような Consumer の期待が存在するケースを例にします:

「レシピが2つ存在している前提で、Consumer が Provider の /v1/recipes に対して GET リクエストを送信した時に、Provider は [recipe_a, recipe_b] となる構造や値のデータを Content-Type: application/json のようなヘッダー値とともにレスポンスする」

このケースでは Consumer 側のテストで以下のように Pact を使って HTTP リクエストをモックするとともに、Contract を記述できます:

RSpec.describe Recipe, pact: true do
  before do
    allow(described_class.client).to receive(:endpoint).and_return(provider_app.mock_service_base_url)
  end

  describe 'get_all' do
    let(:recipe_a) { { id: Pact.like(1), name: Pact.like('Curry') } }
    let(:recipe_b) { { id: Pact.like(2), name: Pact.like('Salada') } }

    before do
      provider_app.given('there are 2 recipes').
        upon_receiving('a request for recipes').
        with(method: :get, path: '/v1/recipes').
        will_respond_with(
          status: 200,
          headers: {
            'Content-Type' => Pact.term(generate: 'application/json', matcher: %r{application/json}),
          },
          body: [recipe_a, recipe_b]
        )
    end

    it 'returns recipes' do
      recipes = described_class.get_all
      expect(recipes.size).to eq(2)
      expect(recipes.first.name).to eq('Curry')
    end
  end
end

上記コードでは、まず、HTTP クライアントの API call 先をモックサーバーに向けています。その後、Pact のモックサーバーに対して Contract の登録をしています。テストケース内の described_class.get_all が評価されると、実際に HTTP リクエストがモックサーバーに送信され、セットアップしたレスポンスが返却されます。以上の流れでこのテストは成功します。

先ほどのテストを実行すると pact file が生成されます。pact file には Consumer/Provider の情報、Consumer が期待するリクエスト/レスポンスが含まれています:

{
  "consumer": {
    "name": "ConsumerApp"
  },
  "provider": {
    "name": "ProviderApp"
  },
  "interactions": [
    {
      "description": "a request for recipes",
      "provider_state": "there are 2 recipes",
      "request": {
        "method": "get",
        "path": "/v1/recipes",
        "query": "fields=media%5Bcustom%5D&image_size[recipe]=280"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": {
            "json_class": "Pact::Term",
            "data": {
              "generate": "application/json",
              "matcher": {
                "json_class": "Regexp",
                "o": 0,
                "s": "application/json"
              }
            }
          }
        },
        "body": [
          {
            "id": {
              "json_class": "Pact::SomethingLike",
              "contents": 1
            },
            "name": {
              "json_class": "Pact::SomethingLike",
              "contents": "Curry"
            }
          },
          {
            "id": {
              "json_class": "Pact::SomethingLike",
              "contents": 2
            },
            "name": {
              "json_class": "Pact::SomethingLike",
              "contents": "Salada"
            }
          }
        ]
      }
    }
  ],
  "metadata": {
    "pactSpecificationVersion": "1.0.0"
  }
}

Provider 側では、この pact file を使って自身が振る舞うかテストを実行します。 Pact を使った CDC testing では、単体テストと同じように、Provider の検証テスト実行毎に環境を初期化します。そのため、Pact が "Provider States" という前提条件を記述する仕組みを提供していて、Provider の検証時にはその仕組みを使ってテスト時にデータのセットアップをします。上記の例では、there are 2 recipes という文字列が Consumer から指定され、Provider はその前提条件をセットアップするためのロジックを記述します:

Pact.provider_states_for 'ConsumerApp' do
  provider_state "there are 2 recipes" do
    set_up do
      %w[Curry Salada].each {|name| Recipe.create!(name: name) }
    end
  end
end

データのセットアップロジックが揃うと、後は Pact が提供する Rake タスクを使って Provider の振る舞いを検証するテストを実行します。テストが成功することを確認して一連のワークフローが完了します。

ここでは一部の Pact を使った CDC testing の一部のコード例を示しましたが、より詳細なコード例は Pact の README にあります。

Consumer のテストで WebMock などを使って HTTP リクエストを単にスタブするのに似ていて、Pact はさらにそこに Consumer が暗黙に期待していた内容をやりとりできるように pact file に落としこむ点が加わっています。

Pact というツールは、コアとなる機能を複数の言語実装から利用できるように、いくつかのコンポーネントに分割されています。ここでは主に Ruby 実装である pact gem を中心に、その周辺のコンポーネントと合わせて紹介します。

pact gem

https://github.com/realestate-com-au/pact

Ruby 用の Pact ライブラリです。各種設定機能や、Consumer のテストで Contract を定義するための DSL が実装されています。また、Contract を読み込んで、それをもとに RSpec のテストを自動で行うことによって Provider を検証する機能が実装されています。

pact file の生成やモックの登録や検証といった主要な機能は後述の pact-mock_service gem で実装されています。

pact-mock_service gem

https://github.com/bethesque/pact-mock_service

上述のように Pact のコアとなるような機能が実装されています。

  • Consumer が期待するリクエスト、レスポンスの組の登録を受け付ける
  • Web サーバーとして動作し、リクエストを受け付けて登録されたレスポンスを返却する
  • 登録されたリクエストが期待するパラメータで呼ばれたどうかを検証する
  • pact file を生成する

pact-mock_service はコマンドライン経由で起動できます。また、Ruby のプロセス内から実行することもできます。

pact-mock_service の役割

典型的なリクエスト/レスポンスを見てみると pact-mock_service の役割と Pact の仕組みについてわかりが得やすいのでここで試します。

まず、モックサーバーを起動します:

$ pact-mock-service service --pact-dir=. --port=3000

とりあえずヘルスチェックをします:

$ curl -H 'X-PACT-MOCK_SERVICE: 1' -v http://localhost:3000
* Rebuilt URL to: http://localhost:3000/
*   Trying ::1...
* Connected to localhost (::1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.43.0
> Accept: */*
> X-PACT-MOCK_SERVICE: 1
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Access-Control-Allow-Origin: *
< X-Pact-Mock-Service-Location: http://localhost:3000
< Server: WEBrick/1.3.1 (Ruby/2.3.0/2015-12-25) OpenSSL/1.0.2e
< Date: Tue, 21 Jun 2016 15:20:02 GMT
< Content-Length: 20
< Connection: Keep-Alive
<
* Connection #0 to host localhost left intact
Mock service running

Pact では Provider/Consumer 間で期待されるリクエスト/レスポンスの組を Interaction と呼びます。Pact の Contract は、Provider や Consumer に関する情報の他に、複数の Interaction から構成されています。

ここでその Interaction を1つ登録します:

$ curl -X POST -H 'X-PACT-MOCK_SERVICE: 1' -H 'Content-Type: application/json' \
  http://localhost:3000/interactions \
  -d '{"description": "a request for recipes", "request": {"method": "get", "path": "/recipes"}, "response": {"status": 200, "headers": {"Content-Type": "application/json"}, "body": "[]"} }'

Set interactions

登録した Interaction が呼び出されたかどうか検証します。ここではまだ呼び出してないのでエラーが返ります:

$ curl -H 'X-PACT-MOCK_SERVICE: 1' -v 'http://localhost:3000/interactions/verification?example_description=a%20request%20for%20recipes'
*   Trying ::1...
* Connected to localhost (::1) port 3000 (#0)
> GET /interactions/verification?example_description=a%20request%20for%20recipes HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.43.0
> Accept: */*
> X-PACT-MOCK_SERVICE: 1
>
< HTTP/1.1 500 Internal Server Error
< Content-Type: text/plain
< Access-Control-Allow-Origin: *
< X-Pact-Mock-Service-Location: http://localhost:3000
< Server: WEBrick/1.3.1 (Ruby/2.3.0/2015-12-25) OpenSSL/1.0.2e
< Date: Tue, 21 Jun 2016 15:36:26 GMT
< Content-Length: 144
< Connection: Keep-Alive
<
Actual interactions do not match expected interactions for mock MockService.

Missing requests:
    GET /recipes

* Connection #0 to host localhost left intact
See standard out/err for details.

登録した Interaction を呼び出します:

curl http://localhost:3000/recipes

[]

もう一度 Interaction が呼び出されたか検証します。すでに呼び出しているので今回は成功レスポンスになります:

curl -H 'X-PACT-MOCK_SERVICE: 1' -v 'http://localhost:3000/interactions/verification?example_description=a%20request%20for%20recipes'
*   Trying ::1...
* Connected to localhost (::1) port 3000 (#0)
> GET /interactions/verification?example_description=a%20request%20for%20recipes HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.43.0
> Accept: */*
> X-PACT-MOCK_SERVICE: 1
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Access-Control-Allow-Origin: *
< X-Pact-Mock-Service-Location: http://localhost:3000
< Server: WEBrick/1.3.1 (Ruby/2.3.0/2015-12-25) OpenSSL/1.0.2e
< Date: Tue, 21 Jun 2016 15:36:54 GMT
< Content-Length: 20
< Connection: Keep-Alive
<
* Connection #0 to host localhost left intact
Interactions matched

pact file の生成リクエストを送ります:

$ curl -X POST -H 'X-PACT-MOCK_SERVICE: 1' -H 'Content-Type: application/json' http://localhost:3000/pact -d '{"consumer": {"name": "A Consumer"}, "provider": {"name": "A Provider"}}'

{
  "consumer": {
    "name": "A Consumer"
  },
  "provider": {
    "name": "A Provider"
  },
  "interactions": [
    {
      "description": "a request for recipes",
      "request": {
        "method": "get",
        "path": "/recipes"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json"
        },
        "body": "[]"
      }
    }
  ],
  "metadata": {
    "pactSpecificationVersion": "1.0.0"
  }
* Connection #0 to host localhost left intact
}

pact file が ${consumer_name}-${provider_name}.json という命名規則で書き出されています:

$ cat a_consumer-a_provider.json
{
  "consumer": {
    "name": "A Consumer"
  },
  "provider": {
    "name": "A Provider"
  },
  "interactions": [
    {
      "description": "a request for recipes",
      "request": {
        "method": "get",
        "path": "/recipes"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json"
        },
        "body": "[]"
      }
    }
  ],
  "metadata": {
    "pactSpecificationVersion": "1.0.0"
  }
}

pact gem は Consumer のテスト実行時に DSL で登録された内容を元に、このようなリクエストをモックサーバーに対して実行することで Pact の機能を実現しています。

pact-mock_service gem は pact 以外からも利用できるように設計されているので、Ruby 以外の言語で Pact の Consumer を実装する時に pact-mock_service が利用できます。例として pact-consumer-swift という Swift 向けの Pact Consumer ライブラリがあります。 クックパッドでも pact-mock_service を利用した Swift 向けの Pact Consumer ライブラリを作成しました。そのうち公開されると思います。

また pact-mock_service 相当の機能を独自に実装しているような Pact ライブラリも存在します。例として pact-jvm があります。

pact_broker

https://github.com/bethesque/pact_broker

Consumer と Provider が pact file をやりとりするための仲介サーバーです。 Pact を使って CDC testing を実行するのに pact_broker は必須でありません。例えば Consumer と Provider を同じチームが開発している場合、手元での開発中は pact file をローカルのファイルシステムのどこかに置き、pact verification 時にそのファイルパスを指定する、ということもできます。しかし、複数の CI サーバーで各 Consumer/Provider の CI が実行されるような環境では、ストレージは独立していたほうが使い勝手が良く、かつ pact file のバージョンや更新日時や差分などのメタデータも合わせて管理できたほうが便利です。なので pact_broker という仲介サーバーが提供されています。

上述した機能の他に、各 Consumer/Provider 間の関係を可視化する機能が用意されていて、意外と便利です:

network_diagram

https://github.com/bethesque/pact_broker/blob/v1.9.2/README.md より

pact_broker は Rack アプリケーションとして実装され gem として提供されています。公式に Docker イメージも提供されています。

また Ruby 向けクライアントライブラリとして pact_broker-client が提供されています。

後述しますが、クックパッドでも pact_broker サーバーを用意して Provider/Consumer 間のやりとりに利用しています。

pact-specification

https://github.com/pact-foundation/pact-specification

Pact における Contract のデータ構造とマッチングの仕様に関するテストケース群が集まっているリポジトリです。 Ruby 実装の pact gem 以外に、pact-jvm など、他言語による実装がありますが、仕様に沿って実装されている限り Consumer と Provider の言語が異なっても互換性があるようになっています。

クックパッドでの Pact の運用

基本的には Pact が想定する使い方やワークフローを採用しています。Consumer の CI で pact file を生成し pact_broker へ publish、Provider の CI で pact verification を実行しています。

手元ではデフォルトで pact file を生成しない

開発者の手元でテストを実行する時には pact file を生成する必要がないので、pact file を生成しないようにしています。CI ジョブでテストを実行する時、もしくは明示的に指定された時のみ生成するようにしています。

pact file を生成しないオプションを追加するパッチを送り、環境変数でそのオプションを制御しています。

pact verification の結果を見やすくする

規模が大きいアプリケーションでは、pact verification が失敗した時に出力が多く、Jenkins のコンソール画面だと内容が把握しにくい問題がありました。そこで JUnit format で検証結果を出力する gem を作り verification 失敗時に素早く内容を把握できるようにしています。

pact file のバージョニング

Pact では pact file を pact_broker にアップロードする際にバージョンをつけることができます。pact_broker では x.y.z のようなよくあるバージョニング以外に、独自にバージョニングロジックを定義することができます。 クックパッドでは頻繁にリリースを行うため、バージョンを表すのに単に git revision hash が良く使われています。git revision hash 単体ではバージョン番号として順序を表すのに不適当なのと、日時を得るまでに1ステップ必要になってしまうので、日時と git revision hash 値を連結した値を pact_broker におけるバージョンとして採用しています。

pact_broker の運用

公式に Docker イメージが公開されているのですが、上記のバージョニングロジックや、ヘルスチェックエンドポイントの追加など、いくつか変更する箇所があったのと、社内にすでに Web アプリケーション用のベースイメージが存在しているので、独自にイメージを作成してそれを利用しています。

pact_broker には内部からのみアクセスできるようにアクセス制御をかけています。データベースには RDS 上の PostgreSQL インスタンスを利用しています。また、CI 環境から publish する本番環境と、チュートリアルやテスト用途に利用できるサンドボックス環境を用意しています。

動的なポート確保

Pact のチュートリアルでは Consumer がモックサービスを設定する際に固定なポート番号を指定しています。ポート番号を手で管理するのは面倒なので、動的なポート確保機能を使うようにしています。

以前の pact-mock_service でも find_a_port gem を使用して動的に空いてるポートを確保する実装が存在していたのですが、社内にある RRRSpec を利用して分散テスト実行しているアプリケーションのテストで webkit-server が起動しないなどの不具合があったため、モックサーバーを起動する時に port 0 に bind する実装に修正した上で pact gem からも使えるようにしました。

Remote Facade パターンと pact-expectations

サービス間通信を利用するようなアプリケーションでは設計のガイドラインを設けていて、Remote Facade パターンを推奨しています。特にテストで Pact を使う際は Contract の数や種類を極力減らしたいので、様々な層のテストで個別に Pact を使って HTTP リクエストをスタブするのではなく、Remote Facade のテストでのみ Pact を使い、その他の層では Remote Facade をモックするようにしています。

Remote Facade パターンを単純に適用するだけだと、Pact を使わずにモックしている箇所でその Expectation が検証されない問題がありました。Pact で記述する Expectation をそのまま Remote Facade をモックする際にレスポンスとして利用することでその問題を解決し、人事部長氏 の手によって pact-expectations gem という実装が作られ、利用しています。

集約的ライブラリの作成

上記のようなバージョニングロジックや、Pact を利用するのに必要なヘルパーの初期生成や、Railtie を使った Zero-Configuration 化などをするために、集約的ライブラリを作成しています。主に社内向けの設定を出力するので、このライブラリは公開していません。

まとめ

Consumer-Driven Contract testing を実現する Pact というツールについてと、クックパッドにおける Pact の運用について紹介しました。

今回 Pact を導入するにあたり、遭遇した不具合を修正したり機能を追加しました。例えば、UTF-8 への対応や、Expectation 内の query をネストできるようにすることや、pact verification の際に pact file の取得をリトライするようにするなど。みなさんが Pact を導入する際に、この記事及び Pact への貢献がお役に立てると幸いです。

クックパッドでは自由に OSS に貢献することのできる環境や文化があります。共にワイワイ OSS に貢献していく仲間を募集してます。応募をお待ちしています。

http://recruit.cookpad.com/jobs/career_recruitment


One more thing: 8月の iOS Developers Conference Japan で、トークが採択されれば Pact の話をするようなので、興味のある方は是非。

https://iosdc.jp/2016/c/node/62

より良いデザインにするために大切にしたいと思っていること

Design

こんにちは。デザイナーの遠藤です。 私は今クックパッドiOS/Androidアプリのデザインを担当しています。

みなさんは、既存の機能を別のプラットフォームに追加する際に、あまり考えずにそのまま追加してしまい、後で後悔したことはないでしょうか?今回は、web版にある機能「料理の基本」をクックパッドアプリへ追加した時のことを交えて、より良いデザインにするために大切にしたいと思っていることをご紹介します。

料理の基本について

レシピをみて料理を作っている際に、「あれ、半月切りって何だっけ?」「水にさらすってどれだけやればいいんだろう」のような、基本的なことさえわからず手が止まってしまったことはありませんか?クックパッドでは、そんな方のために料理の基本を提供しています。今回私は、この機能をiOS/Androidアプリのレシピページに追加する際、デザインを担当しました。

この機能の使い方は、レシピ中の手順欄にある言葉から利用できます。 例えば、スマートフォンwebの場合、レシピ中にところどころ薄く下線が引いてあります。 その下線の部分をクリックすると、詳しく内容が見れるようになっています。 (↓下記の例では、下線「みじん切り」をクリックした場合)

デザイン検討のプロセス

上記のスマートフォンwebの機能をアプリに追加する際に、下記のようにデザインを検討しました。

(1)現状把握(ユーザーのシーンを理解、今まで出来ていないことの洗い出し)

PCやスマートフォンweb版では、料理の基本のキーワードをタップした際、レシピページから料理の基本ページへ遷移します。ユーザーは料理中にレシピを見ていることが多いため、料理中に画面の行き来が発生することで、本来の目的から少し脱線し料理の進行を妨げてしまっているのではないか、ということを課題にしていました。 それを踏まえて、アプリではなるべく本来の目的の「料理をする」ということを妨げずに、知りたい情報を簡単に知れることを重視することになりました。

(2)そのシーンでは、「ユーザーはどんな情報が必要で、何をしたいのか?」アイデアを考える

料理中にわからない言葉に直面して困ってしまったというシーンでは、どういった情報が必要で、何ができると嬉しいのか?というアイデアを出しました。以下は、そのアイデアの一部です。

  • 3Dtouchの機能を使えばすぐに内容が知れて嬉しいのではないか?
  • 画面遷移して見に行くのは料理を妨げることにならないか?
  • 「料理の基本」を全文見せる必要はないのでは?

こういったアイデアをプロジェクトの担当者同士で意見し合い、ブラッシュアップしていきます。 また、アイデアだけでは実際に操作感がわからないため、すぐにUIを具体化していきます。(3)

(3)アイデアをプロトタイプしてデザインの方向性を決める

アイデアが浮かんだ段階で、どんどんプロトタイプを作っていきます。 また、今回iOSとAndroid両方での実装が必要だったため、両方でどういったデザインが必要かも都度検討していきます。 以下、プロトタイプの一例です。

プロトタイプを作っていくとたくさんの気づきがあり、方針がとても固めやすいです。 例えば、上記の一番左のプロトタイプでは、モーダルを表示している時に背景を暗くしてモーダルを目立たせていましたが、「暗くすると、本来の料理をすることを妨げることになるのでは?」という意見のもと、背景を暗くするのをやめました。

(4)デザインが決定したら、周りのデザイナーやエンジニアにレビューしてもらう

プロトタイプである程度方針を固めたら、次に細部のデザインをつめ、決定したら周りのデザイナーやエンジニアに触ってもらいレビューをもらいます。 ここでは、

  • 他の機能の挙動との齟齬がないか
  • 情報が適切か、見せ方が適切か
  • 実装が現実的にできるものかどうか

の観点でアドバイスをもらいます。

決定したデザイン

プロトタイプ作成を重ね、周りの人にレビューしてもらった結果、このようになりました。

iOS / Android

リリース後の評判はどうか?

リリース後、一部のブログなどで「嬉しいアップデートだった」というご意見をいただいているのを発見し、とても嬉しく感じています。とはいえ、まだまだ課題もたくさんありますので、引き続き今後も改善していく予定です。

まとめ

今回、下記の3点を行ったことにより、ただの機能追加にとどまらず、より良いデザインは何か?ということを追求することが出来たと思っています。

★現状把握をする

既存の機能の追加であれば、現在できていることと、出来ていないことをきちんと把握することが大事です。もし何も考えずにそのまま追加した場合、ユーザーが現状で感じている疑問や不安を無視してしまう可能性があります。現状把握をして振り返ることで、デザインする際に大事にしたいことが浮き彫りになってくると思います。

★「ユーザーがしたいことに対してどうアプローチするのか」という方針をある程度定める

今回で言えば、「ユーザーが料理中にわからない言葉に出会った時に、その時していることをなるべく妨げずに、知りたい情報を簡単に知れるようにする」という方針があったので、それを軸にデザインがスムーズに出来たと思います。

★アイデアを出した段階でなるべく早くプロトタイプを作る

アイデアを出しているときに、頭のなかで想像するものでは議論が進まず、悩みのポイントもどんどんずれてきてしまいます。実際に触れるものになって気づく点や、想像と違ったものになってしまったときどうするかという方向転換も早い段階でできたのがとても良かったと思います。

これらは、今後もデザインをする際に大切にしたいと思っていることです。 既存の機能を他のプラットフォームへ追加する際にどう見せるべきか迷っている方は、ぜひ試してみてください。


クックパッドでは、より良いユーザー体験を届けていきたい!というデザイナーやエンジニアを募集しています。

クックパッド株式会社 採用情報

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/