ContractS開発者ブログ

契約マネジメントシステム「ContractS CLM」の開発者ブログです。株式会社HolmesはContractS株式会社に社名変更しました。

Spring BootでWebSocketを試す

この記事は Holmes Advent Calendar 2020 - Qiita 5 日目の記事です。


こんにちは、Holmesでサーバサイドエンジニアをしているid:c-terashimaです
12月は年末ということもありより忙しい月かと思いますが、いかがお過ごしですか?
私は技術書典10の執筆にPHPカンファレンスの当日スタッフ、子供(二人)の誕生日と慌ただしい日々を送っています。。

今回は spring-boot-starter-websocket を使ってWebSocket(STOMP)を試してみたいと思います

WebSocketとは

Web上において双方向通信を低コストで行う仕組みのことです
Webアプリケーションサーバから任意のタイミングでクライアントに情報を送信することが可能で、チャットアプリみたいに多数のクライアントにメッセージを通知する場合に双方向通信が必要になります

環境

次の環境で動作を確認しております

  • Java11
  • Gradle
  • SpringBoot 2.4.0

依存関係

build.gradleのdependenciesに spring-boot-starter-webspring-boot-starter-websocketを追加します
それ以外は必要に応じて追加してください

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
}

WebSocketConfig

アプリケーションでWebSocketを有効にするための構成クラスを追加します

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/websocket")    // ①
                .setAllowedOrigins("chrome-extension://ggnhohnkfcpcanfekomdkjffnfcjnjam")  // ③
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");    // ②
    }
}

①で指定している /websocket はクライアントがコネクションを貼るためのエンドポイントで、②は通知を購読する(subscribe)ためのエンドポイントになります
クライアントは /topic を購読しておくことで、サーバからの通知を受け取ることが可能になります
今回クライアントはChrome拡張機能Apic - Complete API solution - Chrome ウェブストアを利用します
クロスオリジン対策として拡張機能のホストを③に登録します

メッセージ通知

/hello APIをコールすると購読しているユーザにメッセージを通知します

@RestController
@RequiredArgsConstructor
public class Controller {
    private final SimpMessagingTemplate simpMessagingTemplate;

    @PutMapping("/hello")
    public String hello(@RequestBody Parameter parameter) {
        String message = "hello " + parameter.name;
        simpMessagingTemplate.convertAndSend("/topic", message);   // ①
        return message;
    }

    @Data
    static class Parameter {
        private String name;
    }
}

SimpMessagingTemplate#convertAndSendメソッドで /topicを購読しているユーザにメッセージを送信しています

f:id:c-terashima:20201204194719g:plain

curlを実行するとMessageがリアルタイムで表示することができました
かんたんに双方向通信を実装することができましたね!


明日はスクラムマスターをしているid:misudaさんです

IntelliJでVueコンポーネントを書くときに知っておきたい機能3選

この記事は Holmes Advent Calendar 2020 - Qiita 4 日目の記事です。


ここ数日、アンプ購入をきっかけにギター熱が再燃している田中です。

JetBrains 社の提供する IntelliJ IDEA 等の製品は強力な補完を行うことができる高機能な IDE です。

言語ごとに細かなセッティングができ、痒いところに手が届くのでファンも多い IDE かと思います(私もその一人です)

一方で高機能ゆえに、「使いこなせていないなぁ」と感じる方も多いのではないでしょうか。

Holmes では Vue.js を利用しているので、IntelliJ IDEA で Vue.js を書くときに知っておくと良いことを紹介したいと思います。

前提

当たり前ですが公式の Vue プラグインは入れておきましょう!

f:id:s_tanaka:20201204094014p:plain
Preference > Pluginから入れておきましょう

Extract Vue Component を使う

あまり知られていないですが、IntelliJ の機能で単一ファイルコンポーネントの切り出しができます。

Vue.js—IntelliJ IDEA | Vue working with VueComponents extract component

  1. template 内を範囲選択
  2. Alt + Enter or 右クリック →Refactoring
  3. Extract Vue Component

とすることで選択した範囲の html、関連する要素を別ファイルに切り出してくれます。

f:id:s_tanaka:20201204094821g:plain
ListをVueComponentに切り出している

なんという神機能!

優秀なポイントとして、関連する script 内の data や computed を良い感じに props で受け取れるようなコンポーネントにしてくれます。

更に、style タグの中身が pure な css の場合、要素の class や id の style ごと移植してくれます(これだけのために SCSS での BEM 記法を辞めても良いと思えるほど…)

ロジックの実装に熱中していたり、リファクタリングの時間が無いと、ついついモノリシックなコンポーネントが出来上がりがちです。

コンポーネント指向のフレームワークにおいては

  • 責務
  • 再利用性
  • テスト容易性

の観点からコンポーネントは小さくしてなんぼです。ガンガンコンポーネントを切り出しましょう!

vcomputed 等の LiveTemplate を使う

LiveTemplate や Postfix completion をよく使う方はご存知かもしれません。

computed: { 
  someComputedValue() { }
}

や、

data() {
   return { }
}

等は、Vue において頻出イディオムと言えます。

IntelliJ ではこれらの入力を補助する LiveTemplate があります。

  1. vcomputed or vdata と入力する
  2. Tab or Enter

で上記のイディオムを挿入することができます。

f:id:s_tanaka:20201204095025g:plain
vdataでdata optionを挿入

他にもテンプレートや Vuex 用の LiveTemplate があるので眺めてみると良いと思います。

余談ですが、LiveTemplate は頻出イディオム集なので、初めて触る言語の LiveTemplate を眺めると言語のコツがつかみやすいです。

f:id:s_tanaka:20201204095123p:plain
LiveTemplateの編集画面

IntelliJ 上でフロントエンドのデバッグをする

通常フロントエンドのデバッグ、簡単なものであれば console.log 、入り組んだものになると ChromeDevToolVueDevTool で行うことが多いです。

ですが、可能であればエディタ上でブレークポイントを貼ることができれば直感的だと思います。

IntelliJ では npm script を Debug 実行することでフロントエンドのデバッグIntelliJ 上で行うことができます。

  1. 開発モードが有効になっている Vue の devserver 起動コマンド or それを登録した npm script を IntelliJ 上でデバッグ実行
  2. デバッグ実行でターミナル上に表示される localhost の URL を Shift + Cmd を押しながらクリック
  3. IntelliJ のデバッガーと接続している Chrome が立ち上がるので、IntelliJ 上でお好みの位置にブレークポイントを貼る

Vue.js—IntelliJ IDEA | Vue running and debugging

これで IntelliJ 上から出ることなくデバッグができますね。

ただし、Nuxt 固有の SSR でのみ実行されるライフサイクル(asyncData や fetch)ではブレークポイントに引っかかりませんでした。

SSR では設定にひと手間いるようです。

Nuxt.js Debugging with WebStorm. Get Nuxt.js debugging up and running… | by Fernando Alvarez | Medium

まとめ

これらの機能を使いこなせるようになると実装スピードがグンと上がると思います。

特に、 Extract Vue Component は億劫になりがちなリファクタリングを助けてくれる神機能だと思います。ガンガン使って効率化していきましょう!!

スクラムチームに振り返りbot導入してみた

こんにちは。 株式会社Holmesでスクラムマスターしてます、id:k_kubouchi です。

Holmesの開発はアジャイルでのスクラム開発で行っています。 スクラム開発のイベントには「スプリントレトロスペクティブ」というスプリントを振り返るイベントが存在します。 振り返りの種類は色々とあり、チームによってやり方は様々かと思います。 振り返りたい内容をレトロ内で出し切れたらいいのですが、スプリントを振り返る際に「何かあったんだけど思い出せない」みたいなシチュエーションも中にはあるのではないでしょうか。 そこの一助になればと思い、都度振り返りを投稿できる SlackBot を導入することにしました。 それでは、以下の項目に沿って導入手順を解説していきたいと思います。

※前提として、振り返りは KPT と呼ばれる振り返りのフレームワークを想定しています。

※2020年から SlackBot の設定手順が変更されたようなので、そちらをベースに解説していきます。

1. Slack App の設定(SlackBot)

まず、SlackBot を導入したいワークスペースに管理者権限アカウントでサインインします。 URLはこちらになります。 サインイン後、画面右上にある Create New App をクリックしてください。

f:id:k_kubouchi:20201110093623p:plain

設定ダイアログが表示されるので、 App Name に好きな bot 名を、 Development Slack Workspace に導入先の Slack ワークスペースを設定してください。

f:id:k_kubouchi:20201110093807p:plain

次に、権限の設定をする必要があるので、最初の画面の Basic Information クリックで表示されるメニューから、Add features and functionalityPermissions と遷移してください。

f:id:k_kubouchi:20201110093818p:plain

Scopes の真ん中ぐらいにある Add an OAuth Scope をクリックし、以下2つの権限を追加してください。(選択するだけで選んだことになってます)

  • chat:write
  • chat:write.public

f:id:k_kubouchi:20201110093834p:plain

続いて、 Basic Information から App Display Name に行き、 Slack 上で表示する App の内容を設定していきます。

f:id:k_kubouchi:20201110093848p:plain

設定項目はそれぞれ以下の通りです。

  • App name:Slack App の名前
  • Short description:Slack 上での bot の説明
  • App icon & Preview:Slack 上での bot アイコン
  • Background color:Slack 上での bot 背景

最後に Incoming Webhooks から、以降に登場する Google Apps Script 上で使用する URL を取得します。 左メニューの Incoming Webhooks をクリックし、 Webhook URLs for Your Workspace に表示されている URL をコピーしておいてください。

f:id:k_kubouchi:20201110093902p:plain

以上で Slac App の設定は完了です。

2. GASの設定(Google Apps Script)

Slack に投稿した内容を自動的に Googleスプレッドシートに溜めていくような仕組みにしたいです。 そのためには Google Apps Script を使う必要あるので、まずはそちらを準備していきます。

Google Drive+ NewGoogle Sheets を選択します。

f:id:k_kubouchi:20201110093913p:plain

新規スプレッドシートが表示されたら ToolsScript editor を選択します。

f:id:k_kubouchi:20201110093926p:plain

編集画面が表示されるので、適当なプロジェクト名を左上のタイトルをクリックし設定します。 編集エディタ上に今回は以下コードを挿入することとします。

var KEEP = 'KEEP';
var PROBLEM = 'PROBLEM';
var TRY = 'TRY';

function doPost(e) { // SlackからのPOSTリクエスト時に発火する
  switch(e.parameter.text) { // start、end、K: P: T: で場合分け
    case 'start':
      start();
      break;
    case 'end':
      end();
      break;
    default:
      registerKpt(e.parameter.text, e.parameter.user_name);
      break;
  }
}

function start() { // 開始宣言
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0]; // 先頭のシートを取得
  if ((sheet.getName().match(/^[\d]{4}\/[\d]{2}\/[\d]{2} [\d]{2}:[\d]{2}$/) !== null) || (sheet.getLastRow() !== 0)) { // 重複した開始宣言は排除
    postSlack('すでに今スプリントの `KPT` は始まってるなっしー!');
    return;    
  }
  var date = new Date(); 
  sheet.setName(Utilities.formatDate( date, 'Asia/Tokyo', 'yyyy/MM/dd hh:mm')); // シート名を現在時刻に変更
  postSlack('今スプリントの `KPT` 開始なっしー!');
}

function end() { // 終了宣言
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheets()[0];
  var lastRowNum = sheet.getLastRow();
  var keepArray = [];
  var problemArray = [];
  var tryArray = [];
  var reviewArray = [];
  if (lastRowNum === 0) { // 0行の場合は終了できない
    postSlack('登録された `KPT` がないなっしー...');
    return;
  }
  var rows = sheet.getRange(1, 1, lastRowNum, 3).getValues();
  rows.forEach(function(row) { // 行ごとにKPT4要素でまとめる
    switch(row[0]) {
      case KEEP:
        keepArray.push(row[1] + ': @' + row[2]);
        break;
      case PROBLEM:
        problemArray.push(row[1] + ': @' + row[2]);
        break;
      case TRY:
        tryArray.push(row[1] + ': @' + row[2]);
        break;
      default:
        break;
    }
  });
  
  var date = new Date();
  var now = Utilities.formatDate( date, 'Asia/Tokyo', 'yyyy/MM/dd hh:mm'); // 終了時点の時刻を取得
  postSlack( // まとめて投稿
    '今スプリントの KPT なっしー!みんなお疲れなっしー!\n' +
    '```\n' + 
    sheet.getName() + ' ~ ' + now + '\n\n' + 
    '# KEEP\n' + keepArray.join('\n') + '\n\n' +
    '# PROBLEM\n' + problemArray.join('\n') + '\n\n' +
    '# TRY\n' + tryArray.join('\n') +
    '\n```'
  );
  ss.insertSheet(0); // 新たな空シートを先頭に追加
}

function registerKpt(text, userName) { // KPT登録処理
  postSlack('登録してるなっしー....');
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
  var categoryCell = sheet.getRange(sheet.getLastRow() + 1, 1); // 挿入する行の1列目を取得
  var contentCell = sheet.getRange(sheet.getLastRow() + 1, 2); // 挿入する行の2列目を取得
  var userCell = sheet.getRange(sheet.getLastRow() + 1, 3); // 挿入する行の3列目を取得
  var message = processMessage(text); // 投稿を要素と内容に分ける
  if (message === null) { // 形式違いは排除
    postSlack('形式が違うなっしー。 `K:`、 `P:`、 `T:` から始めるなっしー!');
    return;
  }
  categoryCell.setValue(message.category); // 書き込み
  contentCell.setValue(message.content);
  userCell.setValue(userName);
  postSlack('スプシに書いといたなっしー!');
}

function processMessage(text) {  // 投稿を要素と内容に分ける
  var match = text.match(/^([K|P|T]):(.*)$/m);
  if (match === null) {
    return null;
  }
  
  var getText = text;
  switch(match[1]) {
    case 'K':
      getText = getText.replace('K:', '');
    case 'P':
      getText = getText.replace('P:', '');
    case 'T':
      getText = getText.replace('T:', '');
    default:
  }
  
  return {
    category: convertCategory(match[1]),
    content: getText,
  };
}

function convertCategory(category) { // プレフィックスから4要素を判断する
  switch(category) {
    case 'K':
      return KEEP;
    case 'P':
      return PROBLEM;
    case 'T':
      return TRY;
    default:
      return null;
  }
}

function postSlack(text){ // Slackへの投稿
  // ここに先程コピーしていた Slack Incoming WebHook の URL を指定してください
  var url = "https://hooks.slack.com/services/~~~~~~";
  var options = {
    "method" : "POST",
    "headers": {"Content-type": "application/json"},
    "payload" : '{"text":"' + text + '"}'
  };
  UrlFetchApp.fetch(url, options);
}

コードを設定できたら保存し、メニューの Publish を選択します。 設定ダイアログの中身を入力していきますが、この時以下のことに注意してください。

  • Project version: は毎回保存時は必ず New を選択してください
  • Who has access to the app: ですが、必ず Anyone, even anonymous で設定してください

以上で Google Apps Script の設定は完了です。

3. Slack App と GAS の連携(Slack Outgoing WebHook)

Slack の機能である Slack Outgoing WebHook を設定することにより、 GAS を発火させることができます。 その手順を説明していきます。

まず、 https://SlackのWorkspace名.slack.com/apps にアクセスし、検索フォームから Outgoing WebHooks を検索し、選択してください。

f:id:k_kubouchi:20201110105029p:plain

遷移したら Slack に追加 を選択し、 Outgoing Webhook インテグレーションの追加 をクリックしてください。 そうすると設定画面になるので、追加していきます。 今回は KPT 振り返りの内容を投稿できるようにしたいので、以下のようにそれぞれ設定していきます。

  • チャンネル : 投稿で使用するチャンネルを設定
  • 引き金となる言葉 : 投稿内容に含まれるどの文字を検知して発火させるか指定(今回だと KPT
    • K: : Keep 内容投稿用
    • P: : Problem 内容投稿用
    • T: : Try 内容投稿用
    • start : スプリント単位で振り返り内容をスプレッドシートにまとめたいので、その始まり用のトリガー
    • end : スプリント単位で振り返り内容をスプレッドシートにまとめたいので、その終わり用のトリガー
  • URL : GAS の URL を指定

f:id:k_kubouchi:20201110093958p:plain

以上で、 GAS と Slack の連携設定は完了です。

4. チームでの運用ルール

以下のルールを元に、チーム内で運用しています。

  • スプリントの始まりと終わりにスプレッドシートのシートをスプリント単位で分けたいので、スクラムマスターが start end を投稿
  • KEEP の内容は K: を頭につけて投稿
  • PROBLEM の内容は P: を頭につけて投稿
  • TRY の内容は T: を頭につけて投稿

上記ルールに沿って投稿すると、bot が返信してくれるので、地味に面白かったりします(笑)。 (参考サイトがふなっしーを使って運用されてたので、そこに引きずられる形でボットの画像や口調も合わせて設定してみてます)

f:id:k_kubouchi:20201110094011p:plain

5. まとめ

SlackBot での振り返りを導入したことにより、以下のような感想をチームメンバーからもらうことができました。

- 思いついたタイミングで気軽に投稿できる
- 気づきを忘れなくなった
- スプリント単位で手軽に管理できる

短い期間のスプリントだと、効率良く少しずつ改善を繰り返していく仕組みが大事なので 少しの自動化も開発の一助になるのならと思い、今回導入してみましたがそれなりの価値があったのではないかと思います。

今後もメンバーの手助けになるような仕組みをどんどん導入していき、 円滑にスプリントを回していけるよう精進したいと思います!

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

参考記事

Holmesはエンジニア・デザイナーを募集しています 興味がある方はぜひこちらからご連絡ください!

lab.holmescloud.com

lab.holmescloud.com

第1回 ファシリテーションを学ぶ理由と必要なスキル

Holmesでスクラムマスターの役割を担っているid:tomoya_misudaです。

ファシリテーション協会のセミナーで参加して学んだことや実践したことを数回に分けて書きたいと思います。 第1回は、ファシリテーションについてや学ぶキッカケなどまとめます。

www.faj.or.jp

学ぶキッカケ

スクラムマスターの役割を担う前は、開発メンバーとしてプログラムを実装しておりました。Holmesのプロダクト初期から関わっており、仕様や実装は誰よりも理解している自負がありました。しかし、メンバーが増えることでチームメンバーを支援する役割が必要になって来ました。その際にスクラムマスターの役割を担うことを組織から提案を頂きました。開発メンバーとして、スクラムマスター になったらと考えることもあり、やってみようと考えました。

スクラムマスターの役割を担っていく中で、「KPTをやっても特定のメンバーのみ意見を出す。」「プランニングとゴールの認識があっていない。」など数々の課題を抱えてました。その際に各イベントでやるべきことが出来ていないと感じてました。もっとメンバーにイベントの意味や方向性などを持ってもらうことを考えました。 会議やイベントの進め方を調べる中でファシリテーションの言葉に辿り着きました。

ファシリテーション

「人々の活動が容易にできるよう支援し、うまくことが運ぶよう舵取りすること。」をセミナーの冒頭で学びました。会議は、意見の対立やそれぞれの個性から言い出し にくいことが多いと思います。そこを引き出す、意見の対立をまとめる、決めたことが課題とあっているか参加者に確認すると私は捉えました。

ファシリテーションのスキル

f:id:tomoya_misuda:20200816131424p:plain
ファシリテーションのスキル

4つのスキルで構成されます。次回からは、各スキルについて実践を交えながら伝えたいと考えます。

まとめ

数回の記事を通して、ファシリテーションとは、実践の大変さ、良かったことを伝えることができたらと考えます。また、同じ悩みをみんなで共有できたらと考えます。ありがとうございました。

Holmesはエンジニア・デザイナーを募集しています 興味がある方はぜひこちらからご連絡ください!

lab.holmescloud.com

lab.holmescloud.com

Gaugeを使ってMarkdownで書いたテスト仕様を動かしてみる

こんにちは。Holmesでエンジニアをしている山本です。

社内でATDDの話題が出たことがありました。受け入れテストと言えばCucumber、というイメージがあったのですが、他にないか調べたところ、Gaugeという、Markdownを仕様としてテスト実行できるツールがあったので、試してみました。

実行環境

名前 バージョン
OS Windows 10 Pro 64bit バージョン2004
Gauge 1.1.5
Intellij IDEA Ultimate 2020.2.3
Visual Studio Code 1.51.1
JDK Amazon Corretto-11.0.9.1

Gaugeの概要

テスト仕様(CucumberにおけるFeatureファイル)をMarkdownで記述し、それと紐づけたテスト(CucumberにおけるStepファイル)を実行できる、テストフレームワークです。

概要は、Gaugeによるe2eテスト - Speaker Deckに詳しいです。

※スライド内でGaugeのURLが https://getgauge.io/ となっていますが、現在では https://gauge.org/ です。また、ライセンスがGPL3.0との記載がありますが、2020/4月末に変更され、v1.1.5の時点ではApache License 2.0になっています。

テストとして実行可能な言語は、v1.1.5の時点でC#, Java, JavaScript, Python, Rubyの5種類が選択です。また、Visual Studio Codeでのプロジェクト作成時には、TypeScriptも選択できました。

なぜGaugeを試そうと思ったのか

現状、ユニットテストとインテグレーションテストの区別なく、Spockを用いたテストを記述しています。

ユニットテストでは、依存クラスをモック化したテストを書けるのですが、インテグレーションテストを書こうとすると、データの事前準備など、テスト以外の記述がどうしても増えてしまいます。

前処理/後処理の共通化などを行っていますが、単一のテストクラス内では共通化できているが、複数のテストクラスを見比べると同じような処理が書かれていたり、あるメソッドでは共通処理を使っているが別のメソッドでは個別に初期化処理を行ったりと、標準化が難しい状態です。

こうした状態の改善のため、テストの仕様と実装を切り分け、テスト仕様を自然言語で記述できれば、データ作成などの共通化が、仕様と実装が混在している現在よりも容易に行えるのではないかと思います。

また、弊社ではSeleniumを利用したリグレッションテストをOPSチームの方々が記述してくれています。それらについても、テストの仕様と実装を切り分けていれば、共通のテスト仕様を元に、実装を切り替えることができるのではないかと考えました。

これらはCucumberでも達成可能だと思いますが、Cucumberのテスト仕様に用いるGherkin構文を覚えるよりも、ルールがあるとはいえMarkdownを利用できたほうが学習コストが低くなると思い、今回はGaugeを試してみようと思います。*1

環境構築

公式としては、Visual Studio CodeをEditorとして、開発元が同じブラウザ操作自動化ツールのTaikoを用いたJavaScriptによるテスト実装を推奨しているようです。

弊社では標準開発環境としてIntellij IDEAを使用しているため、今回はIntellij IDEAをIDEとして、Java+SeleniumによるE2Eテスト実装を試してみます。

こちらの記事を参考としました。

Gaugeのインストール

公式のインストールガイドにて、OS、プログラミング言語IDE/Editorを選択すると、それ以降のページの記述が変わる仕組みです。

v1.1.5時点では開発環境としてVisual Studio Codeしか選択できないようです。

ひとまず、Windowsインストーラーをダウンロードして、Gaugeをインストールします。

Intellij IDEAにGaugeプラグインを追加

インストールガイドでのIDEには表示されていませんが、Intellij IDEAにもGaugeプラグインがあるため、インストールします。*2

Intellij IDEAからGaugeプロジェクトの作成

Gaugeプラグインをインストールすると、Intellij IDEAの File > New > Project にて、Gaugeプロジェクトを作成可能となります。

  1. SQL Support にチェックせず Next
  2. Project name, Project location, Project SDK をそれぞれ設定し、Finish
    • SDKとしては、JDKしか選択できませんでした

作成されたプロジェクトは、MavenプロジェクトでもGradleプロジェクトでもない、素のJavaプロジェクトになっていました。また、依存性などにSeleniumが使われておらず、単純なユニットテストの実行のみとなっていました。

いくつか外部ライブラリが読み込まれていますが、どこで設定しているか確認すると、プロジェクトルートに プロジェクト名.iml が作成され、 jarDirectory として file://$USER_HOME$/AppData/Roaming/gauge/plugins/java/0.7.13/libs を読み込んでいました。

このファイルをVCSに追加して使い回す場合、Windowsでしか動かせなさそうなこと、またSeleniumがデフォルトの依存性に含まれていなかったことから、Intellij IDEAでのプロジェクト作成を断念し、Visual Studio CodeからMavenプロジェクトを作成することにしました。

Visual Studio CodeにGaugeプラグインを追加

Visual Studio CodeGaugeプラグインをインストールします。

Visual Studio CodeからGaugeプロジェクトの作成

公式のプロジェクト作成手順を元に、Javaを実装言語とした、Gaugeプロジェクトを作成します。

Javaでもプロジェクトの種類が複数ありますが、今回は java_maven_selenium プロジェクトを選択、作成しました。

作成されたプロジェクトの pom.xml を見ると、Seleniumが依存性に含まれていました。また、先ほどはディレクトリを参照していたライブラリ関連も、 com.thoughtworks.gauge:gauge-java:0.7.13 として依存性が追加されていました。

ただし、デフォルトでは文字コードの指定がされておらず、Windows環境ではファイルをMS932で読み込もうとするため、project 直下に、 以下の記述を追加しておきます。

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

Gaugeの実行確認

JavaでのGaugeの実行にはJDK11以上が必要ということで、環境変数 PATH にJDK11のbinを通し、 mvn test を実行するか、Visual Studio Code上で specs/example.spec を開いて Run Spec をクリックし、実行できることを確認します。

テストがパスし、 reports/html-report/index.html が作成されれば成功です。ファイルを開くと、以下のレポートが表示されます。

f:id:h-yamamoto_holmescloud:20201129162426p:plain
初期状態でのレポート

Intellij IDEAでのプロジェクトインポート

Visual Studio Codeを使って、プロジェクト作成はできました。改めてIntellij IDEAでプロジェクトを開いて、Mavenプロジェクトとしてインポートします。

インポートした後、 specs/example.spec を開くと、Gaugeプラグインをインストールしていれば、Markdownの見出し部分に、通常のテストクラスと同様の実行ボタンが表示されます。

これをクリックし、テストが実行されることを確認しました。

実装

いよいよ実装です。テスト仕様をMarkdownで、テスト実装をJavaで記述し、それぞれをマッピングしていきます。

記述方法の詳細は、公式ドキュメントを参照してください。

今回は、先ほどの記事こちらの記事を元に、GoogleまたはBingを開き、「Holmes開発者ブログ」で検索し、検索結果ページのタイトルを確認してみようと思います。

テスト仕様の記述

specs/ ディレクトリ配下に拡張子 .spec として、Markdownでテスト仕様を記述していきます。

他にも拡張子 .cpt として、再利用可能な仕様を「コンセプト」として記述できるようですが、今回は使用しません。

今回は、以下のようなMarkdownを記述し、 specs/search.spec として保存しました。*3

# 検索エンジンの検索結果ページのタイトルを確認する

Tags: search

   |url                    |inputSelector       |title                           |
   |-----------------------|--------------------|--------------------------------|
   |https://www.google.com/|#tsf input[name="q"]|Holmes開発者ブログ - Google 検索|
   |https://www.bing.com/  |#sb_form_q          |Holmes開発者ブログ - Bing       |

## 検索エンジンを開き、「Holmes開発者ブログ」で検索し、検索結果ページのタイトルを確認する

Tags: successful

* 検索エンジンのURL <url> を開く
* 検索文字列入力欄 <inputSelector> を取得する
* "Holmes開発者ブログ" で検索する
* 検索結果ページのタイトルが <title> であることを確認する

h1 タグが見出し、 h2 タグがシナリオとなります。

見出しとシナリオの間にテーブルを記述すると、データテーブルとして、パラメーター化テストが可能です。

シナリオ配下に順序なしリストを記述すると、それぞれがテストのステップとなります。注意点として、Markdownでは順序なしリストの記述に *|+|- のいずれかを使用できますが、Gaugeでは * のみ有効のようです。

ステップに " で囲んだ部分は静的パラメータ、 <> 囲んだ部分は動的パラメータとなります。動的パラメータには、データテーブルのヘッダ名を記述することで、その値が設定されます。このとき、値の末尾の空白は、自動でトリミングされます。

見出しと最初のシナリオの間にステップを記述すると、コンテキストステップとして、各シナリオの実行前の共通処理を記述できるようですが、今回は使用しません。

Intellij IDEA上でも、Gaugeプラグインをインストールしておくと、実装のないステップはエラーとなっていました。

テスト実装の記述

テスト仕様のステップに対応するテスト実装を記述します。

Javaの場合、テストメソッドに com.thoughtworks.gauge.Step アノテーションを付与し、ステップとマッピングします。

WebDriver インスタンスは、 driver.Driver.webDriver に設定されているので、明示的に生成する必要はありません。デフォルトでは、 ChromeDriver が設定されます。

以下のテストクラスを、 src/test/java/search/SearchSteps.java に記述しました。アサーションライブラリとしては、AssertJがデフォルトで依存性に含まれているため、それを使用しています。

package search;

import com.thoughtworks.gauge.Gauge;
import com.thoughtworks.gauge.Step;
import com.thoughtworks.gauge.datastore.ScenarioDataStore;
import driver.Driver;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

import static org.assertj.core.api.Assertions.assertThat;

public class SearchSteps {

    private final WebDriver driver = Driver.webDriver;

    @Step("検索エンジンのURL <url> を開く")
    public void openSearchEngine(String url) {
        driver.get(url);
    }

    @Step("検索文字列入力欄 <inputSelector> を取得する")
    public void getSearchTextInput(String inputSelector) {
        WebElement searchTextInput = driver
                .findElement(By.cssSelector(inputSelector));
        ScenarioDataStore.put("searchTextInput", searchTextInput);
    }

    @Step("<searchText> で検索する")
    public void searchTextInput(String searchText) {
        WebElement searchTextInput = (WebElement) ScenarioDataStore
                .get("searchTextInput");
        searchTextInput.sendKeys(searchText);
        searchTextInput.sendKeys(Keys.chord(Keys.ENTER));
    }

    @Step("検索結果ページのタイトルが <title> であることを確認する")
    public void checkSearchResultPageTitle(String title) {
        String pageTitle = driver.getTitle();
        Gauge.writeMessage("検索結果ページのタイトル: %s", pageTitle);
        assertThat(pageTitle).isEqualTo(title);
    }
}

JUnitのテストクラスのようですが、依存性にJUnitは含まれていないため、テストクラスへの @Test などのアノテーションは不要です。

各テストメソッド間での値の受け渡しには、それぞれライフサイクルの異なる DataStore が利用可能です。今回はシナリオ内での値の受け渡しに、 ScenarioDataStore を利用しています。

また、 Gauge.writeMessage メソッドで、レポートにメッセージを追加することができます。

@Step で記述したテキスト内のパラメータ数と、メソッドの引数の数に不一致があるとエラーとなるなど、テスト実装でもGaugeプラグインをインストールしておけば、Intellij IDEA上でチェックが行われるようになっていました。

実行と結果の確認

mvn test を実行すると、 example.spec に続いて、 search.spec の実行が行われます。実行順は、ファイル名の昇順のようです。

reports/html-report/index.html を開くと、追加したテストの結果を含んだレポートが出力されています。

f:id:h-yamamoto_holmescloud:20201129162120p:plain
テスト追加後のレポート

左フレームのスペック名を選択して、表示を切り替えることができます。タグ付けしておくことで、検索も可能です。

データテーブルを使用した場合、テーブル内容が表示され、行をクリックすることでテスト結果の表示を切り替えることができます。

f:id:h-yamamoto_holmescloud:20201129163306p:plain
Bingの行選択時

Gauge.writeMessage で出力したメッセージも確認できました。

感想

テスト仕様の記述については、単純なMarkdownのため、非常に書きやすいです。Cucumberなどをしっかりと使ったことがなく、概要だけ調べての感想になりますが、Gherkinに比べると、導入に際してのハードルは低いと思います。

いっぽう、テスト仕様の表現力という意味だと、「given/when/then」、日本語では「前提/もし/ならば」といった記述をキーワードとして行えるGherkinのほうが高いと感じました。今回の記述量程度であれば、IDE/Editorのコード補完やコンパイルエラーの有無で管理できますが、GaugeでもGherkinと同様、テストの準備では「前提」を使用したり、前処理の実装はこのファイルに記述するなどの取り決めをしておくと、テストが増えてもメンテナンスがしやすくなると思います。

実装については、公式でIDE/EditorとしてVisual Studio Codeしか選択できなかったため、やや不安がありましたが、Visual Studio Codeでプロジェクトさえ作ってしまえば、Intellij IDEA + Gaugeプラグインで問題なく行えました。今回はサンプルコードのため、テスト実装内でSeleniumを使っていろいろと操作を行いましたが、保守性を高めるためには、Page Object Patternに基づいたクラスを別途記述し、Gaugeはそのオーケストレーションに専念したほうがいいと思います。

また、テスト結果のレポートについては、エラーが起こった場合は自動でスクリーンショットが添付されるなど、非常に見やすいと思います。*4

※Cucumberでのレポート出力については、ちょっと調べた限り公式ではCucumber Reports Serviceが推奨されていたり、Allure Frameworkなどサードパーティ製ライブラリがあるものの、デフォルトでどんなレポートが出力されるかが分からず、比較はできませんでした。

Gauge自体はSeleniumに依存するものではないため、ブラウザ操作を伴うテスト以外でも、受け入れテストやリグレッションテストなど、繰り返し行うテストの自動化およびレポート出力に使えそうだと感じました。

最後に

Holmesではエンジニア・デザイナーを募集しております。ご興味がある方はこちらからご連絡ください。

lab.holmescloud.com

lab.holmescloud.com

*1:なお、私自身Cucumberをきちんと勉強したがないので、あくまでイメージになります

*2:以前のバージョンでは、IDE/EditorとしてIntellij IDEAを選択できたようです

*3:当初は検索ボタンのクリックまでCSSセレクターでやろうとしたのですが、Googleの検索ボタンにはIDが振られておらず、冗長なので断念しました

*4:ただし、Seleniumを使わないテストの場合、スクリーンショットを取っても意味がないため、調整する必要がありそうです

プロダクトにドメイン駆動設計を適用するためにはじめたこと

こんにちは。最近Slackのカスタム絵文字作りにハマっている友野です。Holmesでサーバーサイドエンジニアをしています。

Holmesが提供するホームズクラウドは、今年8月にサービスローンチ3周年を迎えました!

これまでの支持に感謝し、これからも長く使ってもらえるようにプロダクト改善に取り組んでいます。そのひとつとして、ドメイン駆動設計(以下、DDDと表記します)適用に関する取り組みについてご紹介します。似たような状況や同じ課題を持つ誰かの一助になれば幸いです。

背景と現状

ホームズクラウドPMF(Product Market Fit:プロダクトマーケットフィット)を経て、サービスシェア拡大のためにトレードオフスライダーをスピードに全振りしていました。Spring Bootを利用した三層+MVCアーキテクチャを採用し、ローンチ以来、機能追加・改善を繰り返し行ってきました。

日本ではCLM(Contract Lifecycle Management:契約ライフサイクルマネジメント)領域はまだ深く浸透しておらず、認知度向上のためにはとにかく市場評価する必要があり、スピード最優先にする戦略・アーキテクチャ採用は間違っていなかったと思います。

一方で、コアドメインである契約関連機能はその仕様の複雑さも相まって、コードの複雑性も増加の一途を辿り、メンテナンスコストが上がってきました。サービスローンチから3年をふりかえり、市場変化に合わせて「変更に強いプロダクト」を作ることがCTOはじめメンバー全員の共通認識になり、DDD採用を決める後押しになりました。

まずはじめたこと

採用を決めたものの、DDDとは何だろうか?というメンバーが多い状態から第一歩を踏み出すために、有識者の力が必要でした。

DDD Community JPを主宰している松岡さんに声をかけ、力を貸してくれるようお願いしたところ、快く引き受けてくれた上にライブモデリングとライブコーディングまでしてくれる運びとなりました(感謝しかありません)。それまでにメンバーは松岡さんのYouTubeで基礎知識をインプットし、モブワークに臨みます。

テキストベースのコミュニケーションをはさみながら、モブワークを2回ほど実施した後のメンバーの感想は以下の通りです。

  • モデルクラスにロジック集約してると動くドキュメント感あってよい
  • 正しい状態しか作れないと処理がシンプルになる!
  • 1クラスだけ見ればいいのは楽だし安心
  • ドメインやモデルが定義されていないソフトウェアからDDDを始めるには? 再設計/モデリング?

不安に思いながらも、多くのメンバーがその効果と威力に期待を膨らませ、モチベーションを高めていきました。

戦略的モデリング

先日の記事で触れたように、社内で勉強会を重ねた上で、仕様の共通理解と深掘りを行うためにドメインモデリングに取り組み始めました。具体的には、弁護士資格を持つ社員をドメインエキスパートとしてスクラムのリファインメントに招き、モデリングを実施しています。ドメインモデリング自体初めてのメンバーが多いため、以下の書籍を参考にモデリングの進め方ガイドを作り、適宜ふりかえりを交えながら進めています。

little-hands.booth.pm

現在リモートワーク中心での勤務体系のため、議論はオンラインホワイトボードmiro上で行い、モデリングをした後にバージョン管理のためにPlantUMLでまとめています。

そして、戦術的な設計

仕様の複雑性への対策として、ドメインモデリングだけでも一定の価値はあると考えています。これに加えて、戦術的な設計領域*1 まで踏み込むことでさらにDDD採用の効果を高め、変更に強い状態を作ることを目指しています。

しかし、既存の資産、かつ稼働しているサービスにおいて、どこから手をつけるか非常に悩ましい課題です。最初はドメインモデリングした成果を活かすために、集約をコードで表現することからスモールスタートすることに決めました。

採用するパターン2つ

集約をコードで表現するために以下の2つのプラクティスを採用します。

  • ドメインモデルを反映したオブジェクトを置くパッケージの作成
  • 既存テーブル構造に依存しないRepository+Adapterパターン

ドメインモデルを反映したオブジェクトを置くパッケージの作成

まず何より、ドメインモデルを反映したドメインオブジェクトを置く場所を決めます。既存のパッケージ構成では、Spring MVCやSpring Data JPAコンポーネントに合わせ、controllerserviceを中心に、entityrequestなどのモデルのパッケージを切っています。serviceパッケージのクラスには、データモデルが渡されて手続き的に処理されるコードがあるため、ドメインオブジェクトを配置するのは適切ではありません。ドメインオブジェクトを置くdomainパッケージを作ることにしました。serviceパッケージのクラスの修正は最小限に留め、ビジネスルールに関する操作をドメインオブジェクトに移譲する状態をゴールとしています。

  • serviceパッケージ内のクラスが担う責務
    • 全体の流れを構成する責務
    • データの取得、永続化(を依頼する)責務
  • domainパッケージ内のクラスが担う責務
    • ビジネスルールに基づく判断/加工/計算の責務

いわゆる三層アーキテクチャ+ドメインオブジェクトのパターンで、これは以下の書籍を参考にしています。

現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法:書籍案内|技術評論社 gihyo.jp

既存テーブル構造に依存しないRepository+Adapterパターン

一方で、すでに稼働しているサービスではDB構造を大きく変更することは難しく、リスクも伴います。ホームズクラウドでは、データ永続化のライブラリとしてSpring Data JPAを採用しており、Repositoryインタフェースはあるものの、直接JPARepositoryを実装している*2のが現状です。つまり、JPAエンティティ(DDDのエンティティではないのでJPAをつけています)を渡さないと永続化できません。これでは既存のDB構造に強く依存してしまい、ドメインオブジェクトの構造自体が既存構造に引きずられかねません。

Repositoryインタフェースにドメインオブジェクトを渡したいので、JPA用のAdapterクラスを実装して、JPAエンティティへの変換と永続化責務を移譲することにしました。これで、既存資産のJPAエンティティへの依存を切ることが出来ました。DDDの文脈でのコンテキストマップにおいて腐敗防止層という考え方がありますが、これを既存概念(データモデル)に適用した形です*3

f:id:a-tomono:20201027000355p:plain
Repository以降で既存資産との変換を行う(※図はサンプル)

では、このAdapterクラスをどこに置くかというと、また新しくinfrastructureパッケージを追加しました。前節では三層アーキテクチャ+ドメインオブジェクトと記載しましたが、ここに関しては部分的に”依存関係逆転の原則”に従い、ヘキサゴナルアーキテクチャ(ポート&アダプター)の構造になっています。

ふりかえり

設計変更の効果と開発プロセスへの影響度合い、実装イメージの共有を目的として一部追加機能で実装を始めた段階です。結果が出るのはまだ少し先になりそうですが、既存のデータモデルに引きずられず、ドメインモデリングした結果をうまくコードで表現できていると思います。また、追加機能の実装を中心的に推進してくれたメンバーからは以下のようなコメントをもらっています。

  • 既存のテーブルに依存せず、本当に必要なものだけをドメインモデルで表すという考え方はとてもシンプルで分かりやすい。実際に出来上がったドメインモデルを見ると、必要な情報が思っていたより少なくて驚いた。ドメインモデルがシンプルなので処理の流れもシンプルになるという好循環。
  • Repository+Adapterの実装パターンのおかげで、既存のテーブルを気にせずドメインモデルをコードに起こすことに集中できた。
  • まだ理解しきれていない部分も多くあり、詰まる部分もあるが、実践していく中でドメイン駆動設計の良さに気づけることもあるので継続していきたい。

最も複雑、かつコアドメインである契約領域への適用は、ロードマップに従って順次進めていきます。並行して、本記事で触れていないプラクティスについても有効性を確認しながら適用していこうと考えています(少なくとも、ドメインサービスはかなり悩んだ/悩んでいるので、別途記事にできればと思います)。

まとめ

戦略的なドメインモデリングと並行して取り組んでいる、コードで表現する最小限のパターンをご紹介しました。本記事で触れた以外にも多くのプラクティスがありますが、すべてをすぐにプロダクトへ適用するのは難しいものです。なにより既存資産、既存サービスへの影響を考えながら、手段と目的を履き違えぬよう注意深く進めなくてはなりません。

◯◯をしたからDDDやってます!もなければ、△△していないのならDDDとは呼べない!というのはないと考えています*4

ドメイン知識を育てながら、愚直にオブジェクト指向プログラミングをするのが、一番のDDD成功への近道と言えそうです。

最後に

Holmesでは、今後もコアドメインに注力し、変更に強いプロダクト構築を通じて、お客様の契約業務に関する課題の解決とCLM市場拡大を進めていきます。 DDDに関して知見や興味があり、一緒にHolmesの目指す世界観を作りたい方、是非力を貸してください!

Holmesでは現在エンジニアを募集しています。 興味がある方は是非こちらからご連絡ください!

lab.holmescloud.com

*1:集約、値オブジェクト、エンティティ、ドメインサービス等のプラクティスやアーキテクチャなどを指します。詳細は以下の書籍をご参照ください。

エリック・エヴァンスのドメイン駆動設計(牧野 祐子 牧野 祐子 今関 剛 今関 剛 今関 剛 和智 右桂 和智 右桂 Eric Evans)|翔泳社の本 www.shoeisha.co.jp

*2:厳密にはJPARepositoryインタフェースを継承していて、Springが暗黙的に実装していますが、分かりやすさを優先しています。

*3:念のため補足すると、既存資産が腐っているという意味ではありません。念のため…。

*4:さすがにドメインを無視してDDDだ!はないとは思いますが…

WebシステムのXcodeによるモバイル確認についてまとめました(デザイン、動作)

こんにちは、Holmesでエンジニアをしている柳沢です。

今回はモバイルのデバッグ方法について書こうかと思います。 皆さんはどのように確認していますか?

多数の人はWebブラウザに搭載されているデベロッパーツールを利用していると思います。
バイスを選択できたり、縦横比をいれるだけで簡単に確認できるのはいいですよね。
ただ、JavaScriptの挙動やデザインなどを見るとなると物足りないと思うはずです(レスポンシブの動作を見るにはまだ良いのですが、デザインがかなり異なる場合やジェスチャ操作、カメラ、ファイル選択、シェアAPIのエミュレートなど)。
やはり実機、、でも揃えられるわけないじゃん!
ということで、iOSXcodeAndroidAndroid Studioに落ち着くわけです。

XcodeAndroid Studioはそもそもネイティブアプリ開発用のIDEです。ですので開発したものがモバイルでどうなっているのかなんて当然わかるわけです。
その開発用のIDEの機能の一部を使用し、シミュレートするのです。これは頼り甲斐がありますねー 。

そこで需要の高いiOSXcodeでのデバッグを少し詳細に書いてみようと思います。

操作

現在、XcodeMacOSバージョン10.15.4以上でないとインストールできないようです。
https://apps.apple.com/jp/app/xcode/id497799835?mt=12
上記よりApp Storeにいきダウンロードします。

起動するとプロジェクトを作る画面が表示されますが無視して、ウィンドウにあるアプリケーションメニューよりXcode → Open Developer Tool → Simulatorを選びます。

シミュレーター
シミュレーター

初期ではiPhoneが表示されますが、File → Open Simulatorで切り替えることができます。

デバイス切替
バイス切替

ipadシミュレーター
iPadシミュレーター

Safariを起動して開発よりシミュレーターを選択するとデバッグできたのですが、
現在ではSafari Technology Previewという最新の技術を試験実装したプレビュー版に統合されたようですね。
こちらをダウンロードして起動すれば今までと同じ操作、Develop → Simulator → デバッグしたいページを選択で見ることができます。

safari technology preview
safari technology preview

*先にSafariを立ち上げていると開発にシミュレーターが出てきませんので再起動しましょう

リモートデバッグ
リモートデバッグ

所感

ネイティブアプリ開発用ツールなので実機のように挙動やデザインが確認できることはすばらしいですね。 ただ容量が結構必要なことと、動作が重いのは難点ですかね。。

それではまた!

最後に

Holmesはエンジニア・デザイナーを募集しています!
興味がある方はこちらからご連絡ください!

lab.holmescloud.com

lab.holmescloud.com