Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

「マインドマップを使ったテスト分析設計勉強会」参加レポート〜前編〜

こんにちは、テストチームのあべです。
テストエンジニア歴はもうすぐ1年のひよっこです。

今回は、発表者が先輩のとののさん(@tonono2587)、会場が自社ということもあり、「マインドマップを使ったテスト分析設計勉強会」に参加してきました!

マインドマップを使ったテスト分析設計勉強会」

■日時
2018/05/16 19:30~21:30
■場所
株式会社ガラパゴス
■登壇者
セッション1:とののさん(@tonono2587)
セッション2:鈴木三紀夫さん

nagasaki-it-engineers.connpass.com

NaITE(長崎IT技術者会)のマインドマップ本読書会SIGメンバーさんの活動報告会を兼ねた勉強会、という形でした。

今回の勉強会はセッションが2つありました。ので、一緒に参加した先輩の文豪さんと2回に分けてレポートします!

今回は、セッション1「【使ってみた】テストにマインドマップ」です(^^)
主な内容はこちら

ちなみに

あべは、テスト設計、分析、テストケース作成の経験がありません。
1年間、テスト実施者として経験を積んできました。

また、「マインドマップから始めるソフトウェアテスト」は未読で参加しています。
マインドマップについても、勉強会当日にネットで20分ほど学習しただけの、全くの素人です。もちろん、書いたこともありませんでした。

そんな、ソフトウェアテストの経験もマインドマップの知識も浅い、
初心者のレポートであることをご承知おきくださいm( )m

SIG活動報告

まずはSIGメンバーの活動報告がありました。

メンバーの参加動機

■テスト業務の改善

  • 1個の改修でどこに影響があるのかすぐにわからない
  • テスト技法自体はなんとなくわかってきたが、設計にもっと活かしたい
  • 要求分析的な部分も考えたい
  • バグ分析もできたらいいな

活動内容

マインドマップから始めるソフトウェアテスト」の読破&実践を通して、
日々の活動内容の改善を目指す

マインドマップを描いているうちにわかったこと

  • 内容を俯瞰してみられる
  • 自分の考える癖が見える
  • 考えられることが増える

マインドマップからテストケースに落としてみてわかったこと

マインドマップを描いてみる

描いてみましょう!ということで、以下のお題でマインドマップを描いてみることになりました。

■お題

数取器( 数取器|ブラウザ・スマホですぐ使えるWEBアプリ
使い方
「+」ボタン:クリックする度に数字が+1される
「-」ボタン:クリックする度に数字が-1される
「Clear」ボタン:クリックすると数字が0にもどる

■作成時間:10分

参加者のみなさんがもくもくと書き進めて行くなか、私はいきなりつまずきました。

・・・なにをかいていいかわからない!

テーマはもちろん「数取器」です。
しかしその先に伸ばすべきメインブランチに何を書けばいいかわかりません。
前述のとおり、私はテスト分析も設計も、テストケース作成の経験もありません。
メインブランチにはなにをかくべき?ボタン?品質特性?技法の名前??なにが正解なんだ???
完全に、パニックです。
ですが、さすがに何も描かないのは良くないので、とりあえずメインブランチをボタンにして書いてみました。

そうなるまでのあべの頭の中の流れは、
品質特性では書ける気がしない。。
→「表示」「機能」みたいな要素でも多分うまくまとめられないな〜
→とりあえずボタンごとにどんなテストがしたいか書き出してみよう!
という感じでした。

それがこちら↓ f:id:glpgsinc:20180517150925j:plain 最初の5分くらいは完全にパニックになっていたので、ブランチもかなり少ないしまとまっていません(笑)

マインドマップを共有する

各自で書いた後、周りの人と各々のマインドマップを共有しました。
そのとき、参加者の方の意見で「なるほど!」と思ったのは、

  • 「+」ボタンの表示範囲が大きかった→ボタンのどこでも反応する?
  • 「-」ボタンを押し続けるとマイナスになるのか
  • 更新をかけたときは値がリセットされるのか
  • ボタンを長押しするとどうなるのか

またメインブランチも、私のようにボタンだったり、「機能」「表示」などの要素だったり、人によってかなり違いがありました。

マインドマップをテストに使うコツ

作成→共有後、とののさんマインドマップをテストに使うときに、感じたこと、それを解決するコツを紹介してくださいました。

■メインブランチを出すのが難しい
  • メインブランチをいろんな方法で出してみる。
    メインブランチの出し方
    • 品質特性
    • 利用者の立場:実装/ユーザー
    • 意地悪漢字
    • 業種/職種/経験
    • モデルを基に発想:入力>変換>出力
■ブランチを広げるのが難しい
  • 気にしすぎないで思いついたことはどんどん描く(階層/構造/見た目/内容...)
  • ブランチのそばにメモを書く
■「手描き」自体難しい(後でメインブランチを入れ替えたくなる/書ききれない)
  • ツールをつかってみる (WEB/アプリ/付箋)
  • 描ききれないところはメインブランチごと別紙へ

まとめ

  • マインドマップを書くのにはコツがいる
    メインブランチの出し方、マインドマップ全体の整理の仕方
  • 始めやすい、楽しくできる
  • 繰り返し描くうちに、マインドマップの使い方自体上手くなる、慣れてくる
  • テスト観点が多くげられるようになってくる
    描きながらどう整理するか、考える順番をどうするか
  • 視覚的にまとまるので、他人と共有しやすい 共有することで、別の考えも取り入れられる

感想など

  • はじめやすい
    マインドマップを描いたことがなくても、なんとかそれっぽい形に描くことができました。
    必要なのは紙とペンだけなので、思いついたときにすぐはじめられていいなと思いました。
  • 活用できそう
    ソフトウェアテストの分析以外にも、自己分析や旅の計画など、いろいろなことに活用できそうだなと思いました。
  • 自分がどんなふうに考えているかわかる
    頭のなかでぐるぐる考えるより、自分がどんな順番で考えてるかが目に見えてわかると、自分の考え方や癖(とののさんも仰っていましたね)が見えてくる感じがありました。自分の考え方の良いところ、悪いところを直すきっかけにもなりそうです。
  • 知識がある程度必要
    これはマインドマップを使わない場合でも当然なのですが、ソフトウェアテストの知識はある程度必要だなと感じました。知識がないと、今回の私のように、前に進めないままパニックということになりかねません。 ソフトウェアテスト以外でマインドマップを使うときも、テーマについてはある程度知っておいたほうがいいなと感じました。

さいごに

マインドマップ」とはなにか、テスト分析に使うとどうなるのか、を学ぶことができました。
今回の勉強会をきっかけに、マインドマップについて、テスト分析についてを学びます! ゆくゆくはマインドマップを使ってテスト分析できるよう頑張ります(^^)

ここまで読んでいただき、ありがとうございました!

文豪さんの後編をお楽しみに!

ところで

ガラパゴスではエンジニアを募集しています。興味を持たれた方はぜひ弊社の採用ページをご覧ください。

www.glpgs.com

サーバーレスなAtomリーダーを作ってみた

こんにちは。iOSチームの高橋です。好きな天気記号は煙霧です。

最近仕事の空き時間に簡単なAtomフィードのリーダーを作ったので、そのお話をします。

構成

図にするとこういう感じです:

f:id:glpgsinc:20180516150140p:plain

サーバーレスということで、フィードを保存するストレージにはDynamoDBを使っています。 DynamoDBに登録されたフィードを定期的にLambdaでクロールして解析したエントリ情報をDynamoDBに保存するようにしています。 保存されたエントリはAPI Gateway経由で取得できるようにして、S3に置かれたhtmlからajaxで叩いてVue.jsで描画する形です。

筆者はDynamoDBやらVue.jsやらの経験がなかったので、この記事はそのあたりの奮闘記になります。

DynamoDBとの闘い

DynamoDBはいわゆるNoSQLなので、検索の柔軟性をいささか犠牲にしている部分があります。 具体的にはあらかじめ指定されたキーでしかフィルタやソートができません。

筆者はこのあたりで心が折れかけたのですが(RDBの世界に帰りたい!)、とりあえずテーブルを二つ作って、一方をフィード格納用、もう一方をエントリ格納用としました。

キーの設定

フィード格納用テーブルはシンプルにフィードのIDとURL(とタイトルなどのメタデータ)を保存するものです。 こちらは特にフィルタしないのでscanで全件取得する方針にしました。なのでIDをハッシュキーにしておきます。

問題はエントリ格納用テーブルです。フィードIDでフィルタして、更新時刻でソートしたいので、 フィードIDをハッシュキーに、更新時刻をレンジキーにしたいのですが、 フィードIDと更新時刻の組が一意になるとは限らない(レアケースではあると思いますが)ので、これらをキーとして使うことはできません。

かわりに、エントリIDをレンジキーにして、ローカルセカンダリインデックスとして更新時刻をソートキーにすることで解決しました。

Pythonからの呼び出し

こうしてできたDBへはAPI GatewayからLambdaでアクセスします。言語はPython3.6を選んだので、boto3を使ってこういう風にクエリできます:

dynamodb = boto3.resource('dynamodb')
entries_table = dynamodb.Table(os.environ['ENTRIES_TABLE_NAME'])
entries = entries_table.query(
    IndexName='LSI', # ローカルセカンダリインデックス
    ScanIndexForward=False, # 降順
    Limit=10,
    KeyConditionExpression=Key('feed_id').eq(feed_id))

ペジネーション

ここまで作ったところでふたたびNoSQLの壁が立ちはだかります。DynamoDBではSQLのようにOFFSETを使うことができず、 かわりに「前回のクエリで返ってきた最後の値」を引数に渡すことでそこから先のエントリだけを取得するようになっています。 今回のように順番にペジネーションする場合はフロントエンドで値を持ち回る必要があるだけですが、途中のページに飛びたい場合などには問題になるでしょう。

Serverless Framework

テーブル設計とLambdaに載せるコードが準備できたところで、Serverless Frameworkを使ってズドン!と作ってしまいます。 この子はYAMLにLambda関数の設定や作りたいリソースを書いてやると自動でLambda関数やAPI Gatewayのエンドポイントやその他リソースを作ってくれるいい子です。

今回の設定ファイルはこういう感じになります(だいぶ端折っていますが):

service: feed-reader
provider:
  name: aws
  runtime: python3.6
  region: ap-northeast-1
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource:
        - "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.FEEDS_TABLE_NAME}"
        - "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.ENTRIES_TABLE_NAME}"
        - "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.ENTRIES_TABLE_NAME}/*"
  environment:
    FEEDS_TABLE_NAME: ${self:service}-feeds-${opt:stage, self:provider.stage}
    ENTRIES_TABLE_NAME: ${self:service}-entries-${opt:stage, self:provider.stage}
functions:
  crawler:
    handler: handler.crawl
    events:
      - schedule: rate(6 hours)
  createFeed:
    handler: handler.create_feed
    events:
      - http:
          path: feeds/create
          method: post
          cors: true
resources:
  Resources:
    feedsTable:
      Type: "AWS::DynamoDB::Table"
      Properties:
        AttributeDefinitions:
          -
            AttributeName: id
            AttributeType: S
        KeySchema:
          -
            AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: ${self:provider.environment.FEEDS_TABLE_NAME}

functionseventsのところでscheduleを指定するとCloudWatchから定期的にLambda関数を起動することができます。 また、httpを指定するとAPI Gatewayのエンドポイントを作ってくれます。便利!

pipでパッケージを入れたい場合

ところで、Lambdaで実行する関数の中でサードパーティのパッケージを使いたいときがあります。 こういう場合、パッケージをダウンロードしてきてzipに含めて一緒にアップロードするなどの手間が発生するのですが(しかも場合によってはLambdaと同等の環境を再現する必要があったりもする)、 Serverlessにはそれを解決する便利なプラグインがあります。

GitHub - UnitedIncome/serverless-python-requirements: ⚡️🐍📦 Serverless plugin to bundle Python packages

このプラグインを入れて、YAMLファイルにちょっと追記して、requirements.txtに必要なパッケージを書いておくと自動でLambda関数に含めてくれます。 また、今回はネイティブコンパイルの必要なパッケージは使っていないのですが、必要ならばDockerコンテナを立ち上げて環境を用意してくれるようです。便利!

Vue.jsとなかよくなる

さて、バックエンドの準備が整ったので、フロントエンドの作成に入ります。 APIを叩くのにはaxiosを使うことにしたので、たとえばこういう感じでフィード一覧を取得することができます:

axios.get('/feeds').then(function (response) {
  console.log(response.data.feeds);
});

こうして得られたフィードやエントリをVue.jsを使って描画していきます。

Vue.jsというのは、コンポーネントシステムやデータバインディングを用いてシンプルな記述から動的にWebページを生成するフレームワークです。 公式のチュートリアルがとても充実していて特にハマったところもないので特筆すべきことはないのですが、たとえば、

<div id="app">
  <ul>
    <li v-for="feed in feeds">{{ feed.title }}</li>
  </ul>
</div>
var vm = new Vue({
  el: '#app',
  data: {
    feeds: []
  }
});

axios.get('/feeds').then(function (response) {
  vm.feeds = response.data.feeds;
});

のように記述すると、GET /feeds APIから返ってきたフィード情報がリスト表示されるようになっています。 ここではvm.feedsがHTMLのfeedsにバインドされており、変更が検知されるとDOMを書き替えて表示を更新します。 v-forというのはVue.jsの独自属性で、その名の通り配列のfor文のように要素を繰り返すことを表しています。

さらにコンポーネントを活用することで、ひとまとまりのHTML片を独自タグに変換し、HTMLの記述をシンプルにすることができます。たとえば、

components: {
  'entry': {
    props: ['entry'],
    template: '<article><h1>{{ entry.title }}</h1><div v-html="entry.content"></div></article>'
  }
}

のようにコンポーネントを定義しておくと、HTML側では

<entry :entry="entry"></entry>

と書くだけで、templateで指定したHTMLを生成してくれます。いい子ですね。

S3にアップロード

あとはこうしてできたWebページ一式をS3の適当なバケットに上げて、Webホスティングを有効にすれば完成です。簡単ですね!

認証をかける

こうしてできたAtomリーダーですが、自分以外からのアクセスを制限したいと思います。 このとき可能な方法はいくつかあると思うのですが、今回はAPI GatewayAPIキーを設定する方法をとりました。 そういう場合もServerless Frameworkに任せることができて、YAMLファイルに

provider:
  # ...
  apiKeys:
    - feedReaderAPIKey
# ...
functions:
  # ...
  createFeed:
    # ...
    events:
      - http:
          # ...
          private: true

と書くだけで自動的にAPIキーを生成してAPI Gatewayに設定してくれます。

生成されたAPIキーはデプロイ時にコンソールに表示されるので、axiosのほうで

axios.defaults.headers = { 'x-api-key': 'XXXXXXXXX' };

などと書いてやればaxiosからのリクエストに自動的にAPIキーが付加されます(実際には直書きするとまずいので、ローカルストレージに格納したものを読み出しています)。 便利ですね。

ハマったことなど

基本的には便利なフレームワークに助けられてスッと作ることができたのですが、いくらかハマったところがあったので書いておきます:

DynamoDB

上にも書きましたが、DynamoDBのキー概念に馴染むのにけっこう手間が掛かりました。いまでもこれが正解なのかよくわかっていません(でもこれ以外にどうすれば?)。 結局詳細はよくわからなかったので、限りなくシンプルなテーブル構造を保つためにいくつかの機能(未読件数表示とか)を落とすことになりました。今後の課題です。

XHTMLとHTML

Atomフィードのエントリのcontent属性が'xhtml'の場合(そんなことをしている例は筆者自身のblog以外に見つからなかったのですが)、その部分はXMLとして解釈され、XHTMLとして独自の名前空間を持ちます。 これが問題で、Python標準ライブラリのXML解析器でtostringを呼ぶと、その部分のXHTMLタグすべてに名前空間接頭辞がつくことになります。 これをそのままWebページに流し込んでも、未知のタグとして扱われるのでマークアップが機能しません。

これを解決するために、Webページ自体をXHTMLにしてしまう、という案も考えたのですが、そうすると今度はHTMLで書かれたフィードが有効なXMLではないケースに対応できなくなります。

最終的には、XML解析の段階でタグをイテレートして、タグ名から接頭辞を消去することで解決しました。

tree = ET.ElementTree(element=list(content)[0])
for el in tree.iter():
    el.tag = el.tag.replace('{http://www.w3.org/1999/xhtml}', '')
content_text = ET.tostring(tree.getroot(), encoding='unicode', method='html')

まとめなど

ということで、DynamoDBとServerless FrameworkとVue.jsを使ったサーバーレスAtomフィードリーダー作成について書きました。 普段はiOS開発をやっているので、もっと手こずるかな〜と思っていたのですが、案外簡単に(実質三日くらい?)作ることができたのでよかったです。 新しい技術を覚えてゆくのはやっぱり楽しいですね。

ところで

ガラパゴスではエンジニアを募集しています。興味を持たれたかたはぜひ採用ページをご覧ください。

www.glpgs.com

FoundationDBを試してみる

ご機嫌よう、ガラパゴスのおとめです。

先日のことですが、AppleがFoundationDBをオープンソースにしたというニュースが目に止まりました。そこでFoundationDB、て何かしらん、と調べてみたら、次のように魅力的なリストができました。

  • ACIDなNoSQL
  • SQLで操作可能
  • SSDを使う場合で、1コアで20,000書き込み/秒のスループット
  • 500コアまでリニアにスケール(1400万書き込み/秒までスケールするという記事もあり)
  • 読み込みは1ms、書き込みは5ms
  • キーはソートされている

そこで、まずは試してみることにしました。

インストールしてみる

とりあえず適当なUbuntu Server 16.04のVMに入れてみます(システム要件を満たしていないことは置いておきます)。まあ手順は公式に書いてある通りなのですが……

$ sudo dpkg -i foundationdb-server_5.1.5-1_amd64.deb
dpkg: 依存関係の問題により foundationdb-server の設定ができません:
 foundationdb-server は以下に依存 (depends) します: python (>= 2.6) ...しかし:
  パッケージ python はまだインストールされていません。

ここでガラパゴスのおとめはちょっとハマってしまいました。Ubuntu 16.04ではPython3.5が入っていて、コマンドもpython3だからなのかな? とか、python2も入れておいて、update-alternativesしないとダメなのかしらん? とか……

結論としては、sudo apt-get install pythonするだけでした。

インストールが済んだら確認してみます。

$ fdbcli
Using cluster file `/etc/foundationdb/fdb.cluster'.

The database is available.

Welcome to the fdbcli. For help, type `help'.
fdb>

次にbindingをインストールしてみましょう。この記事を書いている時点では、以下の言語が用意されています。

使ってみる

今回はRubyから使ってみます。公式のガイドでは何故かgemを直接ダウンロードするみたいに書かれていますが、gem install fdbで入ります。ダウンロードしたgemを使う場合は、ffiに依存していますので一緒に入れると良いでしょう。

では早速公式のAPIリファレンスとにらめっこしつつREPLから使ってみます(Ruby 2.5.1から使っているので時々ワーニングが出ています)。

$ irb
irb(main):001:0> require 'fdb'
=> true
irb(main):002:0> FDB.api_version 510
/home/his/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/fdb-5.1.5/lib/fdbimpl.rb:173: warning: constant ::Fixnum is deprecated
/home/his/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/fdb-5.1.5/lib/fdbimpl.rb:173: warning: constant ::Fixnum is deprecated
/home/his/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/fdb-5.1.5/lib/fdbimpl.rb:173: warning: constant ::Fixnum is deprecated
=> nil
irb(main):003:0> db = FDB.open
=> #<FDB::Database:0x000056493f1fb808 @dpointer=#<FFI::Pointer address=0x00007f4ddc001050>, @options=#<FDB::DatabaseOptions:0x000056493f1fb790 @setfunc=#<Proc:0x000056493f1fb7b8@/home/his/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/fdb-5.1.5/lib/fdbimpl.rb:553 (lambda)>>>

……ここいらへんから既に公式のリファレンスが何を言っているのかわかりません。というかAPIの説明が何もないので、ソースコードの方を見てみると、どうやらFDB.openの戻りを使ってget/set/clearすることでKVSのように使えるようです。

irb(main):004:0> db.set(1, 'foo')
Traceback (most recent call last):
        6: from /home/his/.rbenv/versions/2.5.1/bin/irb:11:in `<main>'
        5: from (irb):8
        4: from /home/his/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/fdb-5.1.5/lib/fdbimpl.rb:581:in `set'
        3: from /home/his/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/fdb-5.1.5/lib/fdbimpl.rb:569:in `transact'
        2: from /home/his/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/fdb-5.1.5/lib/fdbimpl.rb:582:in `block in set'
        1: from /home/his/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/fdb-5.1.5/lib/fdbimpl.rb:877:in `set'
NoMethodError (undefined method `bytesize' for 1:Integer)

何か怒られました。Integer#bytesizeが存在しないと言われています。bytesize? じゃ文字列を入れてみたらいいのかしらん?

irb(main):005:0> db.set('1', 'foo')
=> "foo"
irb(main):006:0> db.get('1')
=> "foo"

何か入ったようです。試しにFDBコンソールから確認してみましょう。するとこのようにちゃんと登録されているようです。

$ fdbcli
Using cluster file `/etc/foundationdb/fdb.cluster'.

The database is available.

Welcome to the fdbcli. For help, type `help'.
fdb> get 1
`1' is `foo'

では何件か登録してみます。

irb(main):007:0> db.set('100', 'bar')
=> "bar"
irb(main):008:0> db.set('30', 'baz')
=> "baz"

既存のキーを書き換えてみます。

irb(main):009:0> db.set('1', 'hmm')
=> "hmm"

ところでキーは必ずソートされていると書かれていました。確認してみましょう。キーを複数取得するには、get_rangeを使うと良さそうです。

irb(main):010:0> db.get_range('1','100')
=> [#<FDB::KeyValue:0x000056493ee77970 @key="1", @value="hmm">]
irb(main):011:0> db.get_range('1','999')
=> [#<FDB::KeyValue:0x000056493eb7e520 @key="1", @value="hmm">, #<FDB::KeyValue:0x000056493eb7e160 @key="100", @value="bar">, #<FDB::KeyValue:0x000056493eb7d788 @key="30", @value="baz">]

ふむ。(とりあえずデフォルトでは)ソート順はCなのかしらん? そして、get_rangeの引数に与えるキーの範囲ですが、始点は含んで終点は含まないことが分かりました。ためしにFDBコンソールからも見てみましょう。

fdb> getrange 1 999

Range limited to 25 keys
`1' is `hmm'
`100' is `bar'
`30' is `baz'

ソートされているようです。

さて、今回はFoundationDBのさわりとして、RubyからかあるくKVS風に触ってみました。SQLで操作とかトランザクションとかスループットとかも多いに気になるところですが、今回はここまでにしましょう。スループットとかはErlangで10,000プロセスくらいで一気に書いてみたらいいのかしらん……?


ところでガラパゴスではエンジニアを募集しています。ご興味をお持ちの方はぜひ弊社の採用ページをご覧ください。

www.glpgs.com

では、ご機嫌よう。

--

この記事は業務の一環として業務時間中に書きました