Holmes開発者ブログ

契約マネジメントシステム「ホームズクラウド」の開発者ブログです

@SpringBootTestを使ったSpockテストが遅いので、Gradleでテストをカテゴリ分けして起動を高速化する

Holmesでエンジニアをしている山本です。

以前の記事でも言及があったように、現在はユニットテストの起動および実行に非常に時間がかかっています。

最もコード量が多いプロジェクトで、ユニットテストの起動に約2分、実行には数十分かかります。

理由としては、Spring Bootを使用したJavaプロジェクトにて、ControllerやServiceに対して @SpringBootTest を付与したテストを行っているためです。

中でもControllerに対しては WebEnvironment.RANDOM_PORT を指定した結合テストとして記述されているため、 gradle test --tests <className> などでControllerのテストを除外しないと、Webサーバーや組み込みDBの起動が行われ、テスト起動および実行に時間がかかってしまいます。

完了まで数十分では、変更の都度すべてのテストをローカルで実行というわけにはいかず、変更したクラスに対応するテストのみ修正する、という運用になりがちです。結果、CIから実行されたテストの失敗が、度々起こりました。

単体レベルのテストは、簡単に実行して30秒以内には結果が出てほしいので、結合レベルのテストを除外してテストしたいと考え、SpockとGradleで実現する方法を調査しました。

テストのカテゴリ分け

テストサイズの考え方を参考に、Smallテストを導入したいと思います。

Spockのテストのカテゴリ分けについては、JUnit@Category がそのまま使用できます。

任意のクラスをカテゴリ分けに使用できますが、マーカーとしてわかりやすいよう、テストサイズに対応したクラスを作成します。

package com.holmescloud

final class SmallTests {

    private SmallTests() {
    }

}

POJOEnum、依存クラスをモック化したServiceなど、Springに依存しないテストクラスに対して、 @Category(SmallTests.class) を付与しておきます。

smallTestタスクの作成

続いて、Gradleのタスクとして実行できるよう、build.gradleに以下のタスク定義を追加します。

task smallTest(type: Test, dependsOn: testClasses) {
    useJUnit {
        includeCategories 'com.holmescloud.SmallTests'
    }
}

これで gradle smallTest を実行すると、@Category(SmallTests.class) を付与したテストクラスのみが実行されます。

ログ出力の追加

このままでは実行経過がわかりにくいため、 afterSuite にて lifecycle レベルのログ出力を追加します。

除外したテストも afterSuite に渡されますが、それらは第2引数 TestResultTestResult#getTestCount が0となるため除外しています。

また、クラス単位、Test Executor単位、タスク単位の結果が渡されますが、Test Executor単位の結果は不要と思い、除外しています。

Test Executorの場合、第1引数の TestDescriptor のうち、 TestDescriptor#getClassName がnull、かつ TestDescriptor#getParent が存在するため、それで判定します。

それ以外は成功件数とテスト件数、経過時間と表示名を出力するよう、タスク定義に追記します。

task smallTest(type: Test, dependsOn: testClasses) {
    useJUnit {
        includeCategories 'com.holmescloud.SmallTests'
    }

    afterSuite { desc, ret ->
        if (!ret.testCount || (!desc.className && desc.parent)) {
            return
        }

        def retSec = (ret.endTime - ret.startTime) / 1000
        def retCount = "${ret.successfulTestCount}/${ret.testCount}"
        def retMsg = "[${ret.resultType}] ${retCount}, ${retSec}s"
        logger.lifecycle "${retMsg}: ${desc.displayName}"
    }
}

一度コンパイルした状態で、 gradle cleanCompileTestGroovy smallTest し、テストクラスのコンパイルおよびカテゴリ分けしたテストの実行時間を確認しました。以下のように、クラスごとの結果とタスクの総計が出力されます。

> Task :smallTest
[SUCCESS] 14/14, 0.981s: <className>
[SUCCESS] 13/13, 0.055s: <className>
[SUCCESS] 8/8, 0.041s: <className>
[SUCCESS] 35/35, 5.539s: Gradle Test Run :smallTest

BUILD SUCCESSFUL in 29s

smallTestタスクにおける経過時間の総計と各クラスごとの合計の差は、カテゴリ分けされていないテストの読み飛ばし時間です。

対象となるテストクラス数が少ないこともありますが、Webサーバーの起動が行われないため、タスクの実行時間を5.5秒まで短縮できました。テストクラスのコンパイルを含めても約30秒と、許容できる時間に収まったかと思います。

今後の課題

ひとまずテストをカテゴリ分けし、選択的に実行可能となりましたが、全てのテストを実行したときの遅さは解消されていません。

個人的には以下のように順を追って、少しずつ解消していければと考えています。

  1. Serviceについては、 @SpringBootTest は使わず、依存クラスをモックとしたシンプルな単体テストを記述し、Smallテスト扱いとする
  2. Controllerについては、 @SpringBootTest は使わず、 @WebMvcTest を利用し、依存するServiceなどは @MockBean などでモック化したテストを記述、Mediumテスト扱いとする
  3. Repositoryについては、組み込みDBを用いたテストを記述し、Mediumテスト扱いとする

また、Spring Bootアプリケーションのテストに関する私の知識はv1.5ごろのものです。v2になって以降、より良いテスト方法があるかもしれないので、適宜調査していきたいと思います。

最後に

Holmesではバックエンドだけでなく、フロントエンドでもテスト記述を推進しております。

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

lab.holmescloud.com

lab.holmescloud.com

参考資料

testing.googleblog.com

www.slideshare.net

qiita.com

qiita.com

Vuex + TypeScriptで非同期通信のサンプルを作りました

Holmesでエンジニアをしているid:w-miuchiです。

Holmesのアプリケーションは、フロントエンドを主にThymeleaf + JQueryで構築しています。
今後はSPA(Single Page Application)に切り替えます。それにともないNuxtJS + TypeScriptを採用することになりました。
現在、NuxtJSおよびVue.jsを鋭意勉強中です。

まずはNuxtJSの公式ドキュメントを読むところから始めました。
読む→作って試す、と繰り返していたのですが、いまいちうまく試せなかったのがVuexでした。 公式通りであれば問題なかったのですが、実現したいのは以下の条件でした。

  • Actions, Mutations, Stateを定義
  • TypeScript(Interface)
  • 非同期通信(GET/POST)

この条件で公開されているコードを調べました。
何かの条件が欠けていることが多かったのですが、見つけたのがGoogle-Books-APIです。

こちらのコードを元にしてサンプルを作ってみました。
Vuexの大まかな流れを理解できるかと思います。

Vuexとは

詳しくは下記のVuexの公式ドキュメントを見て頂ければと思います。
Vuexはコンポーネントが共通で持てるStoreであり、コントローラーになります。
ただし「共通の状態を共有する複数のコンポーネントを持ったときに、すぐに破綻します」 とあるように使い方は難しいです。
今回はこの点は置いておいて全体像を掴むことにしました。

Vuexの構成

f:id:w-miuchi:20200422090202p:plain
"単方向データフロー"のコンセプト
Stateを扱うのはMutations、そのMutationsを操作するのはActionsとなります。
作成したサンプルにおいては以下の役割としました。

Actions

非同期処理(GETやPOST)を記述する。
またComponentから呼ぶ場合、受け渡すパラメータはインターフェースを用意する。

Mutations

非同期処理(Actions)の処理結果をStateに保存する。
Componentからは呼ばない。

State

Componentから呼ばれる。
Stateにもインターフェースを用意する。

サンプル

作ったサンプルは、以下の画面2つです。
今回はアカウント一覧画面を紹介します。

  1. アカウント一覧画面

    • 非同期でアカウント一覧情報を取得すること
    • 取得時はローディングすること
    • 検索機能があること
  2. アカウント編集画面

    • URIにidを含むこと
    • idを元に非同期でアカウント情報を取得すること
    • 名前を編集すること
    • 非同期でアカウント情報を更新すること

Store

ファイル構成

store
├── account
│   ├── edit.ts
│   ├── search.ts
│   └── types.ts
├── dummy.ts
├── store.ts
└── types.ts

インターフェースを設定

store/account/types.ts

// Account
export interface Account {
  id: string
  name: string
}

// Account検索結果
export interface SearchAccountState {
  keywords: string
  accounts: Account[]
  total: number
  page: number
  isLoading: boolean
  isThere: boolean
  isError: boolean
}

// Account検索用パラメータ 
export interface SearchAccountsPayloadObj {
  keyword: string
  page: number
}

インタフェースの定義はわかりやすくできました。
HolmesのようにサーバーサイドにJavaの使っている場合、インターフェースは理解しやすいと思います。またサーバーサイドと定義を合わせられるのもメリットです。 またローディングやページングのような共通化できるものはインターフェースにした方がいいでしょう。

RootStateにModuleとして設定

store/types.ts

import { SearchAccountState, EditAccountState } from './account/types';

export interface RootState {
  // アカウント一覧
  SearchAccountModule: SearchAccountState,
  // アカウント編集
  EditAccountModule: EditAccountState
}

検索結果と考えるとRootStateに設定することは考えどころです。
同じComponentを複数設置すると、検索結果まで同期してしまいます。

Vuex.Storeを設定

store/store.ts

import Vue from 'vue';
import Vuex from 'vuex';
import { RootState } from './types';

Vue.use(Vuex);

export default new Vuex.Store<RootState>({});

Mutaions, Actionsの処理

store/account/search.ts

import { Module, VuexModule, Mutation, Action, getModule, } from 'vuex-module-decorators'
import { Account, SetSearchAccountsObj, SearchAccountsPayloadObj } from './types';
import store from '../store';
import dummy from '../dummy';

@Module({dynamic: true, store, name: 'SearchAccountModule', namespaced: true})
class SearchAccountModule extends VuexModule {
  // Store
  public keyword: string = '';
  public accounts: Account[] = [];
  public total: number = 0;
  public page: number = 0;
  public isLoading: boolean = false;
  public isThere: boolean = true;
  public isError: boolean = false;

  // 検索結果の保存
  @Mutation
  public setSearchAccounts(payload: SetSearchAccountsObj): void {
    this.accounts = payload.accounts;
    this.page += 1;
    this.total = payload.total;
  }

  // 検索キーワードの保存
  @Mutation
  public setSearchKeywords(payload: string): void {
    this.keyword = payload
  }

  // 検索結果の初期化
  @Mutation
  public resetSearchAccounts(): void {
    this.keyword = '';
    this.accounts = [];
    this.total = 0;
    this.page = 0;
  }

  // 検索結果取得中のローディング
  @Mutation
  public setLoading(payload: boolean): void {
    this.isLoading = payload
  }

  // 検索結果の有無
  @Mutation
  public setThere(payload: boolean): void {
    this.isThere = payload
  }

  // 検索結果取得のエラー有無
  @Mutation
  public setError(payload: boolean): void {
    this.isError = payload
  }

  // 検索結果取得
  @Action({})
  public async getAccounts(payload: SearchAccountsPayloadObj): Promise<void> {
    const { data, error }: any = await
      new Promise(res => 
        // 非同期処理
        setTimeout( () => {
          const items: Account[] = dummy.accounts.filter((item: Account) => {
            return item.name.match(new RegExp(payload.keyword, 'ig'));
          });
          res({
              data: {
                total: items.length,
                items: items
              },
              error: null
            }
          )
        }, 1000));
    this.setLoading(false);

    // 検索結果のStore保存
    if (data) {
      const info: SetSearchAccountsObj = {
        total: 0,
        accounts: [],
      };
      if (data.total !== 0) {
        info.total = data.total;
        info.accounts = data.items;
        this.setSearchAccounts(info);
        this.setThere(true)
      } else {
        this.setSearchAccounts(info);
        this.setThere(false)
      }
      this.setError(false)
    } else {
      this.resetSearchAccounts();
      this.setError(true)
    }
  }
}

export default getModule(SearchAccountModule)

こちらの@Mutationや@Actionというアノテーションの書き方はvuex-module-decoratorsを利用しています。
@MutationはStateの設定処理になります。
@Actionはダミーの情報をsetTimeoutで取得していますが、axiosを利用した通信に変わることを想定しています。
ダミーの情報は以下になります。

store/dummy.ts

export default {
  accounts: [
    {id: '1', name: 'Giorno Giovanna'},
    {id: '2', name: 'Bruno Bucciarati'},
    {id: '3', name: 'Guido Mista'},
    {id: '4', name: 'Leone Abbacchio'},
    {id: '5', name: 'Narancia Ghirga'},
    {id: '6', name: 'Pannacotta Fugo'},
  ]
};

Component

<template>
  <div class="AccountBlock">
    <div class="AccountBlock--header">
      <div class="AccountBlock--title">Account List</div>
      <a class="AccountBlock--refresh" @click="fetchAccounts">Refresh</a>
      <form @submit="search"><input class="AccountBlock--inputKeyword" type="text" placeholder="Keyword" v-model="keyword"></form>
    </div>
    <div class="AccountBlock--item" v-for="(account, index) in accountItems">
      <div class="AccountBlock--itemName">{{ account.name }}</div>
      <div class="AccountBlock--itemEdit"><nuxt-link v-bind:to="{name:'account-edit-id',params:{id:account.id}}">Edit</nuxt-link></div>
      <div class="AccountBlock--itemRemove"><a href="/remove">Remove</a></div>
    </div>
    <div class="AccountBlock--Loading" v-show="isLoading">Loading...</div>
    <div class="AccountBlock--noData" v-show="!isThere">No data.</div>
  </div>
</template>

<script lang="ts">
  import { Vue, Component } from 'vue-property-decorator';
  import { Account, SearchAccountsPayloadObj } from '../../store/account/types';
  import SearchAccountModule from '../../store/account/search';

  @Component
  export default class SearchAccounts extends Vue {
    protected keyword: string = '';
    // アカウント一覧の取得
    protected get accountItems(): Account[] {
      return SearchAccountModule.accounts;
    }
    // ローディングの取得
    protected get isLoading(): boolean {
      return SearchAccountModule.isLoading;
    }
    // アカウント一覧有無の取得
    protected get isThere(): boolean {
      return SearchAccountModule.isThere;
    }
    // アカウント取得エラーの有無
    protected get isError(): boolean {
      return SearchAccountModule.isError;
    }
    // アカウント取得
    protected async fetchAccounts(): Promise<void> {
      await SearchAccountModule.resetSearchAccounts();
      SearchAccountModule.setLoading(true);
      SearchAccountModule.setThere(true);
      const data: SearchAccountsPayloadObj = {
        keyword: this.keyword,
        page: 0,
      };
      await SearchAccountModule.getAccounts(data);
      SearchAccountModule.setSearchKeywords(data.keyword)
    }
    created() {
      this.fetchAccounts();
    }
    search(e: any) {
      e.preventDefault();
      this.fetchAccounts();
    }
  }
</script>

レンダリングはStateを利用し、
非同期処理のアカウント情報取得はActionsを使用できるようになりました。

所感

Vuexの処理の流れや全体像を掴むことができました。 ただし、Storeにどの情報を持たせるべきかを考えて設計する必要があることがわかりました。

またサンプル作成のために他にもコードを調べましたが、
Vuexは作りたいものによって構造や書き方が様々でした。
非同期通信の有無やJavaScript OR TypeScriptでも大きく異なります。

ここからは、アプリケーションの構造に合わせた設計で考えて行きたいと思います。

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

lab.holmescloud.com

lab.holmescloud.com

新米エンジニアがTDD(テスト駆動開発)を、約3ヶ月間行なって感じたこと

2019卒で入社した、Holmesの倉島です。(サーバーサイドエンジニア)

2020年の1月から、自社でTDD(テスト駆動開発)を推進していく方向になりました。 そこで、私が開拓を任されましたので、ブログ記述時現在(2020年4月)まで約3ヶ月間TDDを行なった感想を述べたいと思います。

TDD開始時点

エンジニアとしての経験が浅いので、まずTDDとは何者かを知るところから始めました。
参考にさせていただいた資料:

channel9.msdn.com

dev.classmethod.jp


理解したこととしては...
  ・テストコードを記述してから実装をすること
  ・RED→GREEN→リファクタリング の3つのサイクルに沿って開発を行うこと
RED:落ちるテストを記述
GREEN:とにかくテストが落ちない実装をする
リファクタリング:テストが落ちない状態を維持して、意味のある実装にする



以上を意識して開発を行なってみました。
結果的に、プロダクトのカバレッジが15%上昇しました!
以下は行ってみての感想です。


よかったこと

1.テストコードを記述しない、という事が減った

テストコードを後に回してしまうと、記述し忘れや、時間の問題でテストコードが記述されないままになってしまう事がありました。TDDであればテストコードを記述してから実装を行うので、記述しないということは無くなっていきました。

2.仕様の確認をしながら実装することができる。

テストコードを記述するにあたって考えなくてはならない事が、「処理を通して如何なる結果を期待するか」であるので、何をしたいかが明確になる。私は「何から作ればいいんだ?」と思う事が多々あるので非常に有効でした。

3.画面(フロント)が完成する前に、サーバー側の動きをチェックできる

画面上から動かす事ができなくとも、処理の動きは確認する事ができるので、フロント側とサーバー側に作業のズレがあったとしても一定の処理の保証ができることが大きなメリットだと感じました。

4.既存処理の改修(リファクタリング)、仕様変更が楽。

テストコードが記述されていると、その処理は何をする処理なのかが理解しやすいし、既存のテストが落ちないようにリファクタリングをすれば良いので、悩む時間が(体感)減ったと感じました。

よくなかったこと

1.セットアップコードが多い場合やデータ関連が複雑な場合は時間がかかる

処理を動かすためのデータを用意する時間がとても多くなって、時間対効果が見込めなくなることがありました。

2.処理の結果が不鮮明な場合のテストコードが雑になりがち

処理の結果が不鮮明であると、テストで期待する結果が書きづらかった。例として、受け取ったデータを別の処理に送るだけの処理が挙げられます。結局、そういった処理は期待する結果を「エラーが出ない事」のみにしてしまうケースがありました。

3.テストの起動がすこぶる遅いので、時間がかかる(環境依存)

Holmesの開発環境に限った話ではあると思いますが、 Spring Boot を使用して起動を行なっていることに加え、DIコンテナで管理しているクラスの量が多いのでテストの起動がとても遅いです。一回の起動に2分ほどかかることもありました。

まとめ

TDDを行う上で最も気を使ったことは、どこまで端折りができるかだと思います。というのも、TDDのサイクルは細かくしようと思えばどこまでも細かい単位で行えてしまいます。この部分のこのテストは不要、と適切に判断し不要なテストを削っていくことがTDDにおいて大切なことだと感じました。 TDDを行なって、享受できるメリットは多いと思います。個人的には仕様の確認をしながら実装ができることが大きなメリットだと思いました。 また、上述の「2.処理の結果が不鮮明な場合のテストコードが雑になりがち」のようにどんな場面でも効果を発揮するものではないことがわかりました。

終わりに

私の感想が、TDD理解、導入への一助になれば幸いです。

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

lab.holmescloud.com

lab.holmescloud.com

認定スクラムマスター研修レポート

こんにちは! Holmesの吾郷です。

少し前になりますが、 2019年1月から本格的に運用スタートしたスクラム開発を経て、 2020年1月から新チームのスクラムマスターを務めることになりました。

それにあたり、会社から認定スクラムマスター研修を受ける機会をいただき、 無事に認定スクラムマスター資格を取得できました。

スクラムについてはスクラムガイドを参考にしてください。 https://www.scrumguides.org/docs/scrumguide/v2017/2017-Scrum-Guide-Japanese.pdf

研修概要

いくつか研修を開催している団体がありますが、

今回は株式会社Odd-e Japanさんが開催する研修に参加しました。 www.odd-e.jp

講師は日本人で唯一の認定スクラムトレーナーの江端さんです。

研修で認めてもらえると試験の受験資格を獲得できます。

無事、試験に合格すると下記資格を取得できます。 www.scrumalliance.org

1日目

午前

 スクラムとはなんぞや、スクラムマスターに必要な要素・役割を学習しました。大まかに以下が印象に残っています。

  • スクラムの目的とスクラムを通して得たい目的を区別すること
  • スクラムマスターは、スクラム適切かどうかを判断する(マスターとついてるだけに)
    • スクラム以外の方法論も知っている必要がある
  • スクラムはプロジェクトの現状を把握するフレームワークである
  • スクラムの世界では、人が言った言わないではなく、事実に基づく特定できる情報のみを扱う
  • スクラムはチーム中心型組織である
  • スクラムにおいては権利責任が同居している
  • スクラムマスターは安易に提案・遂行してはならない。
  • スクラムマスターの五大スキル
    • Teaching
    • Facilitating
    • Mentoring
    • Coaching
    • Situationaling
気づき

 スクラムは、改善の為のフレームワークと思っていましたが、改善ができるかどうかは別物でした。
 また、スクラムマスターとなるには、まだまだ知識が足らないことに気づきました。前提として、別の開発手法を知っていることが大切で、さらに学問的知識(集団心理学、組織論)をしっかり学ぶべきだと感じました。
 冒頭でも書いてますが、開発チームの一員だった頃は、思ったことをすぐに提案してました。しかし、論理的となると一度自分の中で落とし込み、チームに効果的な提案をする必要があります。POとチームの両者の連携を促進する立場として、両者から信頼されていることはとても重要です。そのためにはいい加減な言動をしてはならないということです。

午後

午前で学んだことを使ったグループワークを行いました。 トレーナーから出されたお題に研修参加者から提案を行うというものです。

条件は二つ

  • 発言は論理的であること
  • NGワード(とりあえず・まずは・一旦)
気づき

これは本当に難しいお題でした。 発言時に論理的でなければ、トレーナーから指摘を受けます。頭の中で論理的な提案を考え続けます。
初日だったこともあり、基本的に指摘が多く出ていたように思います。 午後はほとんど発言できなかったと記憶しています。

お恥ずかしながらその日の振り返りも掲載します。

自分の意見をなかなか共有できなかった。

因果を捉え、説明できるようにする

固定観念に囚われ、柔軟な発想ができなかった。

2日目

午前

主にスクラムの構成要素について学習しました。大まかに以下が印象に残っています。

  • Feedbackについて
    • 人とモノは分けて考える
    • 弱点/補強的であること
  • PBI(Product Baclog Item)は一つ一つがプロダクトであること。(顧客に提供できる単位)
気づき

 自分も以前経験したことがあるのですが、スプリント毎に提供可能なプロダクトを作っていかないと、まとめてリリースする直前にリリーススプリントなるものを経験しないとならず、たくさんの不具合が出て大変なことになります。提供可能状態を維持し続けることの大切さは身に沁みて感じました。。。

午後

前日からのワークを引き続き継続して行いました。
結果としては提案の内容の決め型を決めることに終始していましたが、冒頭で発言できそこから議論が活性化したように感じ非常に嬉しかったです。

気づき

前日の夜から、練って練って練ってだした一言ですが、論理的であることの大切さを感じました。
また、参加者の方が、ボードや紙を用いて説明し始めたりと言葉のみではなく道具を使ったディスカッションも活発になりました。

2日目の最後には、トレーナーから、どういう戦略指標をもっているのか?という問いにハットさせられたことも覚えています。また、行動しないと評価されないという厳しいお言葉も参加者には投げかけられました。

3日目(最終日)

午前

見積もりとDONEの定義について学習しました。大まかに以下が印象に残っています。

  • 相対見積もりは精度が高く、スクラムに適している(現状がわかるため)
  • ベロシティは生産性指標ではない
  • DONEとはProductを提供できる状態である
  • スプリント毎の提供状態を作ることでVolatilityを下げるようにする
気づき

DONEの定義に関しては、例えばアプリケーションによっては推奨環境などをもともと設定している物があると思いますが、推奨環境という制限をかけていることはすべてのユーザに提供できる状態ではないことから、DONEの定義を満たしていないと判断になります。なるほど、、、となりました。つまるとこ、そういった不可避なUNDONEというのは存在するはずで世界中の企業がUNDONEが少しでもなくなるように動いているとのことです。 また、DONEの定義の学習の過程で、DONEの定義をできていると言える状態にもっとも近い企業はNetflixであるということを学びました。今度遊びに行ってみたい。。。!

午後

前日に引き続き、お題に対してどう決めていくかのディスカッションです。 ある程度、方向が決まっていき最後には決め方が決まりかけるところまではいけました。 結局、トレーナーに提案するところまではいけませんでした。

  • 決める上での判断のための優先順位が必要であること
気づき

集団での合意を行う上では、個々の判断基準が異なると難しくなります。 今回自分たちは、議論をすすめる上での判断基準を設けずに走ってしまったことが低迷した原因の一つでした。

最後に

今回、エバッキーこと江端さんの研修を受けました。噂通り、へとへとになる研修でした。笑 ただ、非常に気づき、学びも多く、さらにトレーニングによって実践的に経験・理解することができたと思います。そんな成長する機会をいただけた会社に感謝すると共に、継続的にスクラムマスターとして成長していきたいと感じました。 次はCSPO,CSDも受けてみたい。。。。!!!!

Holmesはエンジニア・デザイナーを募集しています。
興味を持っていただけた方は、こちらからご連絡ください!

lab.holmescloud.com

lab.holmescloud.com

オンライン勉強会を運営してみて

こんにちは、id:c-terashima です

長野のエンジニアを盛り上げようと「長野Javaユーザグループ(通称:ながのJava)」を立ち上げ、3/25に第1回目の勉強会をオンラインで開催しました

nagano-java.connpass.com

オンラインでの開催ということもあり、県外の方も含め多くのエンジニアにご参加いただき感謝しております
この場をお借りして改めて御礼申し上げます
ありがとうございました!!
初めての勉強会運営・オンライン開催で反省や気付きがあったので、共有しつつ次回に活かしていきたいと思います

登壇者集め

connpassやTwitterFacebookで開催告知や登壇者を募集したものの、なんの実績もない地方の勉強会に登壇してくれるエンジニアが現れるわけがありません
オフライン勉強会であれば、集まったメンバーで「もくもく会」や「○○を議題にワーク」みたいなことも可能ですが、オンライン勉強会だとなかなかそういう訳にもいかず。。
「登壇者は自分だけでは...」という不安を常に感じてました
他のJUG勉強会も事前にお願いしているそうなので事前準備をしっかりしようと反省しました

反省

  • 登壇者は事前に見つけてからイベント公開すべし

発表者が寂しい

オフラインの勉強会であれば、目の前に聞いてくれている人がいて反応もあるのですが、オンラインだと

  • 声が聞こえているのか
  • スライドは見えているのか
  • 深堀りする必要があるのか

が分かりにくく不安になります
顔が見えて相打ち等があるとだいぶ違うように思いますが、発表者以外がマイクをオンにしてしまうと声が被ってしまったりするので検討の余地がありそうです
YoutubeLiveで配信する勉強会もありますので色々試してみたいと思っています

反省

  • 登壇者のみが音声ONにするとボッチになる

最後に

運営者としての反省・配信の反省点と上げてみましたが、配信についてはリモートワーク上のミーティングでも同じことが言えるかと思っています
この気付きを普段の仕事にも活かしていきたいです
反省が多かった初めての勉強会ですが、これにめげずにイベントを開催していきたいと思いますので興味がある方はぜひご参加ください

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

lab.holmescloud.com

lab.holmescloud.com

PS.

私の発表スライドも掲載しておきます^^;

speakerdeck.com

BEMを自社に適用させて運用する

こんにちは!Holmesでサーバーサイドエンジニアをしている平田です。

タイトルにもある通り、HolmesではBEMをベースにアレンジを加えて運用しています。
私は2020年1月からフロントエンドもがっつりやるようになったので、その際に勉強したBEMと自社でどのようにアレンジして運用しているかを紹介します。

BEMとは

BEMとは、より構造化されたCSS設計を行うための方法論の一つです。公式
BEMはBlock、Element、Modifierの頭文字をとったもので、”ベム”と呼びます。
BEMに則って開発することで、保守性・使用性・移植性の高いCSSコードを書くことができます。

以前、Holmesでは複数画面に渡って利用している3000行からなるCSSファイルを秘伝のタレのように継ぎ足し継ぎ足しで運用していたため、修正の際に毎回他の画面が崩れたり、!importantが乱用される状況になっていました。

Holmesでは2019年5月からBEM等を取り入れて改善を図ってきました。
BEMを導入することによって、画面固有のCSS、共通のCSSが記述・利用しやすくなったため、不要な考慮も少なく、開発スピードも向上しました。

BEMのルール

BEMはclassセレクタを利用します。
そうすることでCSSの優先度をBEMで記載した通りに統一することができます。

Block

Blockは独立した意味のある塊です。
Block名は小文字の英数字で記述し、単語はハイフンで区切ります。

  • HTML
<div class="block"> ... </ div>

<div class="article"> ... </ div>
.block { ... }

.article { ... }

Blockは他のBlockにネストすることができます。

<div class="block1">
  ...
  <div class="block2"> ... </ div>
</ div>

Element

ElementはBlockの中に記述されるもので、Blockと紐づくことで意味をなすものです。
Element名は小文字の英数字で記述し、単語はハイフンで区切ります。Blockとは__(アンダースコア2つ)で繋げます。
Elementは単体では利用できず、Blockの入れ子として利用します。

  • HTML
<div class="block">
  ...
  <span class="block__element"> ... </ span>
</ div>

<div class="article">
  ...
  <span class="article__title"> ... </ span>
</ div>
.block__element { ... }

.article__title { ... }

ElementのElementを記述することはできません。

<span class="block__element__element"> ... </ span>

ただし、DOMツリー内でのネストはできます。

<div class="block">
  ...
  <div class="block__element1">
    ... 
    <span class="block__element2"> ... </ span>
  </ div>
</ div>

Modifier

Modifierは、BlockやElementに付随して、外観、動作、または状態を変更します。
Modifier名は小文字の英数字で記述し、単語はハイフンで区切ります。BlockやElementと_(アンダースコア1つ)で繋げます。
Modifierとしてプロパティーと値を表現する場合は、_(アンダースコア1つ)で紐づけます。color_red
また、Modifierは修飾子のため、単体では利用できず、非修飾要素(BlockやElement)とマルチクラスで記述します。

  • HTML
<div class="block block_modifier">
  ...
  <span class="block__element block__element_modifier"> ... </ span>
</ div>

<div class="article article_size-big">
  ...
  <span class="article__title article__title_color_red"> ... </ span>
</ div>
.block { ... }
.block .block_modifier { ... }
.block__element { ... }
.block__element .block__element_modifier { ... }

.article { ... }
.article .article_size_big { ... }
.article__title { ... }
.article__title .article__title_color_red { ... }

ファイル構造

Blockごとにファイルを分けます。
そうすることで、意味のある塊であるBlockの重複を防ぐことができるとともに、巨大なCSSファイルが出来上がるのを抑制できます。

Holmes流にアレンジしたBEM

SCSS

SCSSはSassというスタイルシート言語の構文の一つです。
SCSSを使うことで、BEMをより書きやすくなります。

.block { ... }
.block .block_modifier { ... }
.block__element { ... }
.block__element .block__element_modifier { ... }
  • SCSS
.block {
  ... 
  &_modifier { ... }
  &__element {
    ...
    &_modifier { ... }
  }
}
  • CSS (SCSSの変換)
.block { ... }
.block_modifier { ... }
.block__element { ... }
.block__element_modifier { ... }

メリット

  • 冗長な記述を減らせる
  • 入れ子であることで親子関係がわかりやすい

デメリット

  • Modifierをマルチクラスで設計するには工夫が必要
  • 見難い

Holmesではこれらのメリット・デメリットを踏まえて下記のような運用を行っています。

記述ルールのアレンジ

Block

Block名は先頭が大文字、単語を繋げる際は先頭を大文字にして繋げる、アッパーキャメルケースで記述します。
理由はサードパーティCSSとのコンフリクトを防ぐためで、開発スピードや崩れを防ぐための策です。 やってみるとHTMLでは大文字が視認し易く、よかったと思います。

.Block { ... }
.BlockList { ... }

Element

ElementはBlockの要素であることを明確にするため、Block配下として記述します。
Element名は先頭が子文字、単語を繋げる際は先頭を大文字にして繋げる、ローワーキャメルケースで記述します。 Blockとは本家同様__(アンダースコア2つ)で繋げます。 記述はやや冗長になりますが、視認性の高さとclassのネスト関係のスコープを優先しています。

  • HTML
<div class="block">
  ...
  <span class="block__element"> ... </ span>
</ div>
  • SCSS
.block {
  ...
  .block__element { ... }
}
  • CSS (SCSSの変換)
.block { ... }
.block .block__element { ... }

Modifier

Modifierは先頭-(ハイフン1つ)にして、文字の先頭を子文字、単語を繋げる際は先頭を大文字にして繋げる、ローワーキャメルケースで記述します。
また、BlockやElementとは繋げた冗長な記述はせず、非修飾要素(BlockやElement)とマルチクラスで記述します。CSSでマルチクラスを保証します。

  • HTML
<div class="block -modifier">
  ...
  <span class="block__element -modifier"> ... </ span>
</ div>
  • SCSS
.Block {
  ...
  &.-modifier { ... }
  .Block__element {
    ...
    &.-modifier
  }
}
  • CSS (SCSSの変換)
.Block { ... }
.Block.-modifier { ... }
.Block .Block__element { ... }
.Block .Block__element.-modifier { ... }

運用した結果

メリット

  • 冗長な記述を減らせる
  • HTMLやCSSでの視認性が上がった
  • 親子関係で縛るのでスコープが明確になり、マークアップのルールも縛れる
  • 既存のCSSの影響も最小限に抑えられて開発スピードが向上した

デメリット

  • Blockの分け方や命名は開発者次第になっている
  • Block__elementは冗長な記述になる
  • マルチクラスになった場合、HTML上でModifierがどちらを修飾しているかわからない

このModifier運用のデメリットについて

上記でデメリットとして、「マルチクラスになった場合、HTML上でModifierがどちらを修飾しているかわからない」をあげました。
調べてみると、このようにModifier名を単体で書くことには否定の意見をちらほら見かけました。
個人的な考えとしては、適切なModifierであればどちらを修飾しているかは意識しなくて構わないと思っています。

理由

  • セレクタの指定で縛っているので、記述しても適切なものにしか修飾されない
  • 適切に設計していれば、どちらを修飾していようと適切な振る舞いをする

解決できていない問題

改修によってマルチクラスになっている片方を削除しなければならない場合は問題となります。
Modifierが修飾している方を消した場合、Modifierがあるにも関わらず、Modifierが効かないということが起こります。

これについては、それぞれのBlock設計時にModifierを考慮していないことに問題があるのではないかと思います。そもそも共通で使用するモジュールであれば、優先的にModifierをつける等の設計工夫ができます。
HTML側では下記のように、Modifierを被修飾classの次に書くなど、ルール化することでも極力問題を減らせそうです。

class="被修飾class Modifier 他class"

HolmesではBEMを使い始めてからModifier以外をマルチクラスにならないようにしているため、問題は今の所起こっていません。

おわりに

いかがでしたでしょうか。
BEMの表面的な部分とHolmesでどのようにアレンジして運用しているかを紹介してきました。
BEMを検索してみると、オリジナルから派生した色々な記法がBEMとして語られています。
序盤に説明したBEMは公式だと思われるものを参照していますが、間違いがあれば指摘していただければ幸いです。
Holmesで利用しているアレンジもどこかで同じものがあるかもしれませんし、皆さんも環境に合ったものを考えてみてください。

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

lab.holmescloud.com

lab.holmescloud.com

PythonでGmailAPIを使用してメールのタイトルと本文を取得してみた

初めまして、HolmesのNabeです。

今回はPythonからGmail APIを使用してアカウントにアクセスする事でメールのタイトルと本文を取得することができたので、その手順などを共有できればと思います。

環境

開発言語: Python3

ライブラリ: google-api-python-client · PyPI

GmailAPI有効化

APIを使用するに当たって、GmailAPIを有効化する必要があります。APIキーが発行された状態は以下のようになります。

f:id:nabe-holmes:20200319185507p:plain
APIキーが発行された状態(ID・シークレットは加工済み)

画面上部の「JSONをダウンロード」からJSONファイルをダウンロードすると、接続に必要なファイルが生成されます。このファイルをプログラムと同じ階層に設置することでプログラムからAPIを使用することができます。今回のサンプルではcredentials.jsonというファイル名で使用しています。作成するファイルは全て同一ディレクトリに設置してください。

Pythonのコード記載例

ライブラリの準備

前提条件として、いくつかライブラリをインストールする必要があります。requirements.txtに下記のように記載します。

requirements.txt

google-api-python-client==1.7.11
httplib2==0.17.0
oauth2client==4.1.3

下記のコマンドを入力することで、インストールが実行されます。

pip install -r requirements.txt

接続・認証

接続するにあたって必要な認証は下記のコードで実装できます。

gmail_api.py

class GmailAPI:
    def __init__(self):
        # If modifying these scopes, delete the file token.json.
        self._SCOPES = "https://www.googleapis.com/auth/gmail.readonly"

    def connect_gmail(self):
        store = file.Storage("token.json")
        creds = store.get()
        if not creds or creds.invalid:
            flow = client.flow_from_clientsecrets("credentials.json", self._SCOPES)
            creds = tools.run_flow(flow, store)
        service = build("gmail", "v1", http=creds.authorize(Http()))

        return service

取得処理

取得処理は以下のようになります。 先ほど作成したGmailAPIクラスの中に追加で実装する形になります。

gmail_api.py

    def get_message_list(self, DateFrom, DateTo, MessageFrom, MessageTo):

        # APIに接続
        service = self.connect_gmail()

        MessageList = []

        query = ""
        # 検索用クエリを指定する
        if DateFrom != None and DateFrom != "":
            query += "After:" + DateFrom + " "
        if DateTo != None and DateTo != "":
            query += "Before:" + DateTo + " "
        if MessageFrom != None and MessageFrom != "":
            query += "From:" + MessageFrom + " "
        if MessageTo != None and MessageTo != "":
            query += "To:" + MessageTo + " "

        # メールIDの一覧を取得する(最大100件)
        messageIDlist = service.users().messages().list(userId="me", maxResults=100, q=query).execute()
        # 該当するメールが存在しない場合は、処理中断
        if messageIDlist["resultSizeEstimate"] == 0:
            print("Message is not found")
            return MessageList
        # メッセージIDを元に、メールの詳細情報を取得
        for message in messageIDlist["messages"]:
            row = {}
            row["ID"] = message["id"]
            MessageDetail = service.users().messages().get(userId="me", id=message["id"]).execute()
            for header in MessageDetail["payload"]["headers"]:
                # 日付、送信元、件名を取得する
                if header["name"] == "Date":
                    row["Date"] = header["value"]
                elif header["name"] == "From":
                    row["From"] = header["value"]
                elif header["name"] == "To":
                    row["To"] = header["value"]
                elif header["name"] == "Subject":
                    row["Subject"] = header["value"]
            MessageList.append(row)

        return MessageList

タイトルや本文の取得について

取得したデータはメールの情報が大量に入っているため、必要な情報を抜粋してくる必要があります。

メールのタイトル等は特定の場所にあるため決め打ちで取得が可能です。メール本文についてはペイロードの中身を探していく必要があり、メールがテキスト形式かHTML形式かで取得方法が大きく変わってくるので、形式に合わせた場所を参照する必要があります。取得処理のMessageList.append(row)の直前に下記のコードを追加します。

gmail_api.py

# HTMLメール本文を取得する
MessageDetail = service.users().messages().get(userId="me", id=message["id"], format="full").execute()
if MessageDetail["payload"]["mimeType"] == "multipart/alternative":
    for payload_parts in MessageDetail["payload"]["parts"]:
        if payload_parts["mimeType"] == "text/plain":
            for payload_header in payload_parts["headers"]:
                if payload_header["name"] == "Content-Type" and payload_header["value"].lower() == "text/plain; charset=utf-8":
                    row["Body"] = email.message_from_string(str(base64.urlsafe_b64decode(payload_parts["body"]["data"]), "utf-8")).get_payload()

感想

メールの本文の取得についてはメールの形式によって変わるので、どこに記載されているかはデータ構造をデバッグしながら確認したり、いくつかサイトを確認しながら探したため苦戦しましたが、実装自体は思ったより簡単にできたと思います。

参考サイト

キー発行手順は下記ページが参考になりました。

valmore.work

PythonでのAPI使用方法はこちらを参考にしています。

qiita.com

データ構造については下記ページ(英語)に記載されています。

developers.google.com

おわりに

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