Mattermost 記事まとめ: https://blog.kaakaa.dev/tags/mattermost/

本記事について

Mattermost の統合機能アドベントカレンダーの第 24 日目の記事です。

以前から開発に参加しているMatterpollが Mattermost 公式の Plugin Marketplace で Community Plugin として公開されました。

昨日までの記事で Matterpoll Plugin の Server 側・Webapp 側の実装について紹介しましたが、本日の記事では Matterpoll Plugin のテストや CI などの開発周りの話を紹介します。

Mattermost Plugin のテスト

Server 側のテスト

Mattermost Plugin の Server 側の機能を Mattermost を実際に起動することなくテストするには、plugintestパッケージを使用します。

plugintestパッケージを使用してテストを記述していく方法について、Mattermost 本体にあるプラグインテストのサンプルコードを元に紹介します。Matterpoll プラグインも大部分はこれと同じ方式でテストが記述されています。

...
type HelloUserPlugin struct {
	plugin.MattermostPlugin
}

func (p *HelloUserPlugin) ServeHTTP(context *plugin.Context, w http.ResponseWriter, r *http.Request) {
	userID := r.Header.Get("Mattermost-User-Id")
	user, err := p.API.GetUser(userID)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		p.API.LogError(err.Error())
		return
	}

	fmt.Fprintf(w, "Welcome back, %s!", user.Username)
}
...

https://github.com/mattermost/mattermost-server/blob/5122b9e2929dbf84e22f496ee97d007fa18f2d2e/plugin/plugintest/example_hello_user_test.go#L21

Mattermost プラグインの本体は、plugin.MattermostPluginが embedded された構造体です。ここでは、HelloUserPlugin 構造体がそれにあたります。この構造体を通じてプラグイン用の API を実行したり、Hook となるメソッドを実装したりすることで、サーバー側の動作を定義することができます。HelloUserPlugin では、プラグイン独自のエンドポイントを追加する ServeHTTP Hooks が実装され、その処理の中で GetUser API を実行してエンドポイントへアクセスしたユーザーの情報を取得し、そのユーザー名をレスポンスとして返しています。

このServeHTTP Hooks をテストする場合、単にHelloUserPlugin構造体を生成してServeHTTPメソッドを実行しただけだと、プラグイン API (GetUser) の実行部分で実際の Mattermost サーバーの機能を呼び出そうとしてしまいエラーとなってしまいます。

	user, err := p.API.GetUser(userID)

HelloUserPluginAPI フィールドは、plugin.MattermostPlugin 構造体が持っていたフィールドであり、このフィールドを通して呼び出されるメソッドは Mattermost サーバーの処理に依存しているからです。Mattermost サーバーがない状態でテストを実行する場合、このAPIフィールドを入れ替える必要があります。

ここで、プラグイン用のテストメソッドとして定義されている Example メソッドをみてみましょう。

...
func Example() {
	t := &testing.T{}
	user :&model.User{                           ... (1)
		Id:       model.NewId(),
		Username: "billybob",
	}

	api := &plugintest.API{}                       ... (2)
	api.On("GetUser", user.Id).Return(user, nil)   ... (3)
	defer api.AssertExpectations(t)                ... (4)

	helpers := &plugintest.Helpers{}
	defer helpers.AssertExpectations(t)

	p := &HelloUserPlugin{}
	p.SetAPI(api)                                  ... (5)
	p.SetHelpers(helpers)

	w := httptest.NewRecorder()
	r := httptest.NewRequest("GET", "/", nil)
	r.Header.Add("Mattermost-User-Id", user.Id)
	p.ServeHTTP(&plugin.Context{}, w, r)           ... (6)
	body, err := ioutil.ReadAll(w.Result().Body)
	require.NoError(t, err)
	assert.Equal(t, "Welcome back, billybob!", string(body))
}

https://github.com/mattermost/mattermost-server/blob/5122b9e2929dbf84e22f496ee97d007fa18f2d2e/plugin/plugintest/example_hello_user_test.go#L37

**(2)**で plugintest.API という構造体のインスタンスを生成し、生成されたインスタンスをテスト対象のプラグイン構造体 HelloUserPluginSetAPI 関数を通じてセット (5) しています。 plugintest.API自体は、そのままでは何も処理を行わないため、api.On で特定の引数が与えられた時の処理をテスト側で実装しています (3)

    ...
	user :&model.User{                           ... (1)
		Id:       model.NewId(),
		Username: "billybob",
	}
	...
    api.On("GetUser", user.Id).Return(user, nil)   ... (3)
	defer api.AssertExpectations(t)                ... (4)
	...

上記のコードの場合、user.Id を引数として GetUser メソッドが呼び出された場合に (1) で定義された user を返却するようモックを定義しています。また、defer api.AssertExpectations(t) (4) を書いておくことで、テスト実行が終了した時に api.On で定義したモックが実行されていなかった場合にテストを失敗させることができます。

HelloUserPlugin では Helpers関数を利用していませんでしたが、Helpersのメソッドを利用している場合も API と同様に関数をモックすることができます。

最後に httptest パッケージを使って ServeHTTP Hook を呼び出し、レスポンスが想定通りであることをチェックしています。

	w := httptest.NewRecorder()
	r := httptest.NewRequest("GET", "/", nil)
	r.Header.Add("Mattermost-User-Id", user.Id)
	p.ServeHTTP(&plugin.Context{}, w, r)           ... (6)

このように plugintest パッケージを使うことで 、Mattermost サーバーがない状況でも Mattermost プラグインの処理をテストできるようになります。あとは、とにかくパターンを網羅するケースを書き出すだけです。筋力です。

Server 側のテスト(Store)

Matterpoll では Mattermost Plugin の Key Value ストアへのアクセスを抽象化した store パッケージを用意しています。テスト対象のメソッド内で Key Value ストアへのアクセスが必要な処理があった場合、プラグイン API の KVSetKVGet などのメソッドを plugintest パッケージでモックすることも可能ですが、テスト記述が煩雑になるため、Matterpoll では vektra/mockery を使って store.Store インタフェースからモックを生成してテストを記述しています。この vektra/mockery は、Mattermost 本体の plugintest パッケージを生成するのにも使われているものです。

Webapp 側のテスト

ここについては、Mattermost Plugin 特有のトピックというものはなく、開発した React Component に対してJest を使った snapshot テストを実装しています。

CI

CI は CircleCI を使っており、Mattermost プラグイン用の CircleCI Orb を使って lint / build/ coverage / deploy を行っています。

https://github.com/matterpoll/matterpoll/blob/master/.circleci/config.yml

カバレッジは Codecov による PR へのコメントフィードバックを実施しています。

Checks

静的解析系のツールは Mattermost プラグインのテンプレートリポジトリ mattermost/mattermost-plugin-starter-template: Build scripts and templates for writing Mattermost plugins. で定義されているものとベースは同じで、採用している linter やルールが少し異なるという感じです。

サーバー側は golangci-lint を使っており設定ファイルは下記の通りです。

run:
  timeout: 5m
  modules-download-mode: readonly

linters-settings:
  goconst:
    min-len: 2
    min-occurrences: 2
  gofmt:
    simplify: true
  goimports:
    local-prefixes: github.com/matterpoll/matterpoll
  golint:
    min-confidence: 0.0
  govet:
    check-shadowing: true
    enable-all: true
  misspell:
    locale: US
  maligned:
    suggest-new: true

linters:
  disable-all: true
  enable:
    - bodyclose
    - deadcode
    - dogsled
    - errcheck
    - goconst
    - gocritic
    - gofmt
    - goimports
    - golint
    - gosec
    - gosimple
    - govet
    - ineffassign
    - interfacer
    - maligned
    - misspell
    - nakedret
    - scopelint
    - staticcheck
    - structcheck
    - stylecheck
    - typecheck
    - unconvert
    - unparam
    - unused
    - varcheck
    - whitespace

issues:
  exclude-rules:
    # Exclude some linters from running on tests files.
    - path: _test\.go
      linters:
        - dupl
        - goconst
        - scopelint # https://github.com/kyoh86/scopelint/issues/4

https://github.com/matterpoll/matterpoll/blob/d4ffdbfd6dcdea359b7419e0baa3ab8aaa32e420/.golangci.yml

クライアント側は ESLint を使っており、設定ファイルは下記の通りです。(長いためリンク先参照)

https://github.com/matterpoll/matterpoll/blob/0b797025cfbf319c43464fcf457d4cdfe5086188/webapp/.eslintrc.json

翻訳

Matterpoll のメッセージは翻訳可能な形式で管理されており、現在、下記の言語が利用可能です。

言語の切り替えは、Mattermost 本体の設定に応じて実施されます。

Mattermost Plugin における翻訳処理の詳細については、Matterpoll の共同開発者である Hanzei による下記の記事で紹介されています。

https://developers.mattermost.com/blog/localizing-matterpoll/

Server 側の翻訳

Server 側の翻訳機能はgo-i18nを使用しています。

翻訳対象のメッセージは、コード上では以下のようにgo-i18ni18nパッケージの構造体として書かれています。

...
HelpText: p.LocalizeWithConfig(l, &i18n.LocalizeConfig{
	DefaultMessage: &i18n.Message{
		ID:    "dialog.createPoll.setting.multi",
		Other: "The number of options that an user can vote on.",
	}}),
...

https://github.com/matterpoll/matterpoll/blob/45f095875a98fb1d4f3f166851c86f41b987493e/server/plugin/command.go#L265

実際の翻訳を行う場合は、go-i18nのコマンドラインツールを使って、上記のようなi18nの構造体として宣言されたメッセージを json ファイルに集約します。その辺りの手順については以下のドキュメントにまとめられています。

https://github.com/matterpoll/matterpoll/blob/master/CONTRIBUTING.md#translating-strings

go-i18nによって集約されたメッセージを各国のコントリビュータにローカライズしてもらうことで、翻訳されたメッセージが表示されるようになっています。 https://github.com/matterpoll/matterpoll/tree/45f095875a98fb1d4f3f166851c86f41b987493e/assets/i18n

Webapp 側の翻訳

Matterpoll Plugin では、まだ Webapp 側の翻訳機能は実装されていませんが、Mattermost Plugin の機能としては実装できるようになっています。

Webapp 側の翻訳は、react-intlを使用しています。

...
import {FormattedMessage} from 'react-intl';
...
        <FormattedMessage
            id='rootModal.message'
            defaultMessage='Root Modal2'
		/>
...

上記のようにコード内で使用されている翻訳対象のメッセージは、make i18n-extractで集約することができます。

...
## Extract strings for translation from the source code.
.PHONY: i18n-extract
i18n-extract:
ifneq ($(HAS_WEBAPP),)
ifeq ($(HAS_MM_UTILITIES),)
	@echo "You must clone github.com/mattermost/mattermost-utilities repo in .. to use this command"
else
	cd $(MM_UTILITIES_DIR) && npm install && npm run babel && node mmjstool/build/index.js i18n extract-webapp --webapp-dir $(PWD)/webapp
endif
endif
...

https://github.com/matterpoll/matterpoll/blob/master/Makefile#L205

集約されたメッセージファイルを Mattermost Webapp Plugin API のregisterTranslationsで登録することで、Webapp 側のメッセージの翻訳ができるようになります。

さいごに

本日は、Matterpoll Plugin のテストや CI などの開発に関する事柄について紹介しました。 Mattermost Integractions に関する紹介は本日の記事で終了です。

明日は、本記事を執筆する中で見つかった問題に対する Issue/PR について紹介します。

comments powered by Disqus