go-sqlmockで行うクリーンアーキテクチャにおけるgateways層のテスト

みなさんこんちは!株式会社BLAMでエンジニアをしている池田です。 普段はテクノロジー本部で、自社サービス「カイコク」の開発を行なっています。

BLAMではこちらの記事でも紹介したようにクリーンアーキテクチャを採用しリアーキテクトに取り組んでいました。それに伴い課題であったテストについて現在注力しています!

blam.hatenablog.com

今回は、クリーンアーキテクチャでのgateways層(DB操作の実装を書く)のユニットテストについて、BLAMではどのようにしているかを書いていきます!

テストとは

テストを行うことでコードを修正する際などに期待通りの挙動になっているのかを確認してバグを防ぐことができます。 また、テストが実装されていると、改修を行う際にデグレーションのリスクと検品コストを下げることができます。

テストで起こりがちな問題
  • 外部依存する部分をテストするのが難しい(本記事でいうとDB)
  • テストデータの投入などでリソースを割く必要がある。
モックテストとは
  • 依存しているものをモックで差し替えてテスト対象の動作を確かめることができる。
  • 責務の分割を行えるようになる。(テスト対象を明確にすることでテストのスコープを定めるから)
  • 依存するオブジェクトはインスタンスを生成して注入する。(DI)

テストの種類

テストの種類に関して、分け方の切り口は複数ありますが、今回ご紹介するユニットテストに関しては以下のような分け方になります。

  • ユニットテスト: 関数やメソッド単位で行うテスト
  • 結合テスト: 関数やメソッドを結合した機能を対象にするテスト
  • 総合テスト: システム全体を対象にするテスト

今回は、ユニットテストを対象にご紹介します!

BLAMではDBを操作するメソッドのテストをどのように行なっているか

使用しているライブラリと標準パッケージは下記の通りです。実際にどのように使っているかは次の章で説明します!

使用しているライブラリ

BLAMではgo-sqlmockというライブラリを使って単体テストを書いています。

github.com

go-sqlmockは実際のデータベース接続を必要とせずに、テストでSQLドライバーの動作をシミュレートしてくれます。本物のDBの代わりにSQLドライバのような振る舞いをしてくれるモックライブラリです。

使用している標準パッケージ
  • errors エラーを操作する関数が実装されているパッケージ
  • regexp 正規表現に関する関数が実装されているパッケージ
  • testing テストを実行してくれるパッケージ

実際にどのようにテストを書いているか

sqlmockを生成する関数
package mock_handlers

import (
    "database/sql/driver"
    "log"
    "os"
    "time"

    "github.com/DATA-DOG/go-sqlmock"
    "github.com/hoge/huga/internal/entities/handlers"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

func NewMockSQLHandler() (handlers.SQLHandler, sqlmock.Sqlmock, error) {
    // 1つ目の返り値はDB接続のためのmock、2つ目の返り値は設定を行うためmock
    db, sqlMock, err := sqlmock.New()
    if err != nil {
        return nil, nil, err
    }

    // モックDBに接続したDBインスタンスを定義
    mockSQLHandler, err := gorm.Open(
        mysql.New(mysql.Config{
            // 詳細な設定を行う
            Conn:                      db,
            SkipInitializeWithVersion: true,
        }),
    )
    if err != nil {
        return nil, nil, err
    }

    return mockSQLHandler, sqlMock, nil
}

NewMockSQLHandler()関数は、DBに接続したmockSQLHandlerというmockと、mockの中身を設定するためのsqlmockを返します。
mock自体はsqlmock.New()関数で生成します。1つ目の返り値ではDB接続のmock、2つ目の返り値では設定用のmockを受け取ります。

テストファイル
package gateways

import (
    "errors"
    "regexp"
    "testing"
    "time"

    "github.com/DATA-DOG/go-sqlmock"
    mock_handlers "github.com/hoge/huga/internal/entities/handlers/mock"
    "github.com/hoge/huga/internal/entities/models"
    "github.com/hoge/huga/internal/entities/repositories"
    "github.com/go-playground/assert/v2"
    "gorm.io/gorm"
)

func TestItemRepository_FindByCondition(t *testing.T) {
    itemID := uint(1)
    minStartAt := time.Date(2000, time.January, 1, 0, 0, 0, 0, time.Local)
    maxStartAt := time.Date(2000, time.January, 31, 0, 0, 0, 0, time.Local)
    outputItem := &[]models.Item{{
        Model: gorm.Model{ID: itemID},
    }}

    internalServerError := errors.New("Find Internal Server Error")

    tests := []struct {
        name               string
        in                 repositories.ItemRepoSearchCondition
        out                *[]models.Item
        err                error
        prepareExpectQuery func(sqlMock sqlmock.Sqlmock)
    }{
        {
            name: "TestItemRepository_FindByCondition_正常系",
            in: repositories.ItemRepoSearchCondition{
                MinStartAt: minStartAt,
                MaxStartAt: maxStartAt,
            },
            out: outputItem,
            err: nil,
            prepareExpectQuery: func(sqlMock sqlmock.Sqlmock) {
                sqlMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `items` WHERE start_at >= ? AND start_at < ? AND `items`.`deleted_at` IS NULL")).
                    WithArgs(minStartAt, maxStartAt).
                    WillReturnRows(sqlmock.
                        NewRows([]string{"id"}).
                        AddRow(itemID))
            },
        },
        {
            name: "TestItemRepository_FindByCondition_異常系",
            in: repositories.ItemRepoSearchCondition{
                MinStartAt: minStartAt,
            },
            out: nil,
            err: internalServerError,
            prepareExpectQuery: func(sqlMock sqlmock.Sqlmock) {
                sqlMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `items` WHERE start_at >= ? AND `items`.`deleted_at` IS NULL")).
                    WithArgs(minStartAt).
                    WillReturnError(internalServerError)
            },
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            mockSQLHandler, sqlMock, err := mock_handlers.NewMockSQLHandler()
            if err != nil {
                t.Error(err)
            }

            db, err := mockSQLHandler.DB()
            if err != nil {
                t.Error(err)
            }
            defer db.Close()
            tt.prepareExpectQuery(sqlMock)

            // テスト対象の関数を持つ構造体を生成(DI)
            itemRepo := NewItemRepo(mockSQLHandler)
            got, err := itemRepo.FindByCondition(tt.in)
            if err == nil {
                assert.Equal(t, got, tt.out)
            }
            assert.Equal(t, err, tt.err)

            // 期待値と実体値を照らし合わせてチェックし、一致していなければエラーを返す。
            if err := sqlMock.ExpectationsWereMet(); err != nil {
                t.Errorf("failed to ExpectationWerMet(): %s", err)
            }
        })
    }
}

よく使われているtestingパッケージを使用しており、テストケースを記述してそれぞれのテストを回し、期待される値と実際の値を比較します。
いくつか変数や、関数についての補足を入れます。

  • ExpectQueryでは、期待するSELECT文と取得結果の組み合わせをモック化します。
  • 正規表現が期待されているためregexp.QuoteMeta関数を使って期待するクエリを正規表現で書いています。
  • WithArgsで期待するクエリパラメータの値を引数として指定して、WillReturnRowsで返答を期待するレコードを指定。
  • NewRowsで取得結果に必要なカラムを指定し、AddRowsで期待するカラムの値を指定します。
  • エラーを返すテストではWillReturnErrorの引数に期待されるエラー型を指定します。
まとめ

今回は簡単な例のテストを紹介しましたが、 ExpectExec関数を使ったUPDATE、INSERT、DELETE文のテストや、 ExpectCommitExpectRollbackなどを使ったトランザクションのテストなども書け、 go-sqlmockには様々な便利関数があります。 今後もテストを拡充させて効率的な開発が行えるようにしていきます!

参照

少しずつ始めるgoでのクリーンアーキテクチャ

みなさんこんにちは!株式会社BLAM CTOの大沼です。(ぬま (@_numa999) | Twitter
普段はテクノロジー本部で、自社サービス「カイコク」の開発やマネジメント業務を行なっています。

今回は直近チームで注力したクリーンアーキテクチャに関して書きます。

なぜクリーンアーキテクチャに注力したのか?

日頃カイコクの開発チームでは、新機能はもちろんですが、内部的な改善も常に気づいたらissueを上げ、順次改善に取り組む活動を行なっています。 そんな中で直近、

  • 一部DRYじゃなくなっているコードが発生し始めている
  • 一部モジュールが肥大化しているので、テストコードを書くのが大変(モックを作るのが大変)
  • そもそもテストカバレッジが落ちてきているし、テストの実行時間が伸びてきている

といった課題がありました。上記の課題はクリーンアーキテクチャですとは言っている物の、さまざまな事情がありリニューアル当時はレイヤーをとにかく少なくしようと言う方針で進めました。

なので、現状あるものをしっかり活かす方針で改めて必要なレイヤーの洗い出しとそもそもチームでレイヤーごとの責務が統一されていない問題を改めてチームで考え直し、上記の課題解決のためにクリーンアーキテクチャに注力しました。

note.com

どのようなアーキテクチャにしたか?

下記の画像が大枠の全体像になります。基本的には有名な図にできるだけ習うように命名をしています。

ディレクトリ構造は下記のように変更しています。

/
├ internal
│ ├ infrastructures
│ ├ adapters
│ │ ├ controllers
│ │ └ gateways
│ ├ usecases
│ └ entities
│   ├ handlers
│   ├ models
│   └ repositories
└ main.go

各レイヤーの責務は下記のようにしています。

entities

ここではビジネスロジックを表現する

  • model
  • repository
  • handler

のinterfaceとstructを定義しています。

user_model.go

// user_model.go
type User struct {
    ID        uint
    FirstName string
    LastName  string
}

user_repository.go

// user_repository.go
// 実際には引数は大きいので、構造体にしています
type UserRepository interface {
    Find(firstName string) ([]User, error)
    Create(firstName string, lastName string) (*User, error)
}

sql_handler.go

// sql_handler.go
// ORMとしてgormを使用しています
type SQLHandler = gorm.DB

usecase

entitiesのstructとinterfaceを使用し、ユースケースを実現します。

type UserUsecase interface {
    RegisterUser(input RegisterUserInput) (*RegisterUserOutput, error)
}

type RegisterUserInput struct {
    FirstName string
    LastName string
}

type RegisterUserOutput struct {
    UserName string
}

type userInteractor struct {
    userRepository UserRepository
}

func NewUserUsecase(userRepository UserRepository) UserUsecase {
    return &userInteractor{
        userRepository,
    }
}

func (i *userInteractor) RegisterUser(input RegisterUserInput) (*RegisterUserOutput, error) {
    u, err := i.userRepository.Create(input.FirstName, input.LastName)
    if err != nil {
        return nil, err
    }

    return &RegisterUserOutput{UserName: u.FirstName + u.LastName}, nil
}

controller

controllerはhttpリクエストを受け取るところで、リクエストの検証と、リクエストからusecaseへ渡す値を生成することを役割とします。

// user_controller.go
// webのフレームワークとしてechoを使用しています
type UserController interface {
    Get(ctx echo.Context) error
    Post(ctx echo.Context) error
}

type userController struct {
    userUsecase UserUsecase
}

func NewUserController(userUsecase UserUsecase) UserController {
    return &userController{
        userUsecase,
    }
}

func (c *userController) Get(ctx echo.Context) error {
    // ...
}

func (c *userController) Post(ctx echo.Context) error {
    // リクエストの検証とusecaseへ渡す引数の作成
    req := RegisterUserInput{FirstName: "test", LastName: "tarou"}
    res, err := c.userUsecase.RegisterUser(req)
    if err != nil {
        // 受け取ったエラーをハンドリング
        return ctx.String(http.StatusInternalServerError, "予期せぬエラーが起きました")
    }

    return ctx.String(http.StatusOK, res.UserName)
}

gateway

gatewayではhandlerとrepositoryを参照し、データベースの操作を行う実装を書きます。

// user_repository.go
type userRepository struct {
    sqlHandler SQLHandler
}

func NewUserRepository(sqlHandler SQLHandler) UserRepository {
    return &userRepository{
        sqlHandler,
    }
}

func (r *userRepository) Find(firstName string) ([]User, error) {
    user := []User{}
    if err := r.sqlHandler.Where("first_name = ?", firstName).Find(user).Error; err != nil {
        return nil, err
    }

    return user, nil
}

func (r *userRepository) Create(firstName string, lastName string) (*User, error) {
    // ユーザー作成処理
    return nil, nil
}

次に

ここまでが各レイヤーの超簡易的なサンプルで、上記のコードを組み立て、infrastructure層で接続情報等を作成し、main.goでDIを行います。

まとめ

ここまでで、カイコクでのアーキテクチャのイメージをご紹介できたかなと思います!各レイヤーの詳細な考え方等はたくさん記事があるので、「カイコクではこのようにしています!」の紹介でした。

直近では、gomockとgo-sqlmockを活用し、単体テストがかなり書きやすくなりテストカバレッジの向上にもしっかりと取り組め、直近の課題は解決の方向に向かっていると思います。

github.com

github.com

ただやってみてまだまだ、解決したい点や悩んだ末にやらなかった点があります。

  • DIコンテナを作成したい(wireとか)
  • ORMの型の取り扱い?(entitiesに外部依存の型が入るのって良いんだっけ?)
  • presenterは結局usecaseの値を加工するだけになってしまうのではないかと思いつくらなかったが良いのか?
    • 結局echoでrender関数を使用するときにオブジェクトを渡すことになる
  • CQRSやDTOなどの考慮はしていない
  • DBの更新でトランザクションが絡む処理の最適解がまだ見えていない

等、今のところ困ってはいませんが、将来的に考えないといけないなと言う点はまだまだたくさんあります。

大枠のところしか記載できていないですが、これからgoでクリーンアーキテクチャをはじめるチームに少しでも参考になると嬉しいです!

参考

blog.cleancoder.com

nrslib.com

tech.mirrativ.stream

最後に

BLAMではエンジニアを募集しています。ご興味のあるかた、ラフに話を聞いてみたいと言う方がいましたらぜひこちらのフォームからご応募ください!

recruit.blam.co.jp

www.wantedly.com

新卒エンジニア職でBLAMに入社した理由と、開発を通して学んだこと。

こんにちは!株式会社BLAMのテクノロジー本部でエンジニアをしている池田です。 私は2022年4月に新卒で弊社に入社しました。 今回の記事ではなぜ私がBLAMに入社したのか、入社から1ヶ月経って感じた事などを書いていきたいと思います。

目次

  1. BLAMについて

  2. BLAMに入社した経緯と理由

  3. 入社前と後のギャップ

  4. 入社後1ヶ月間で何をやったか

  5. 今後の目標

記事を読んでいただく前に自己紹介をします。 私は大学3年生になってからプログラミングに興味を持ち、2021年の1月から約1年間ベンチャー企業で長期インターンに参加し、RubyとVueを使ったCMSサービスの開発を行っていました。 なので、プログラミングを始めて1年ちょっとという状態です。 現在は、後ほど紹介するカイコクという自社サービスの開発を行っています。

BLAMについて

BLAMは運用型広告を中心としたデジタルマーケティングと 7,000人以上のマーケティング・クリエイティブ人材を抱えるマーケティングDX支援会社です! 私が開発に携わっているカイコクというサービスは、マーケティング課題において必要な複業人材をマッチングし、課題解決に向け伴走するサービスです!現在は日本のデジタルマーケター2万人のうち7,000人以上の方に登録していただいております。

blam.co.jp

kaikoku.blam.co.jp

BLAMに入社した経緯と理由

就職活動中にBLAMのことを知り、マーケターと企業をマッチングするというサービスがとても面白いと感じたのと、個人的にとても興味があったGoとReactを使っているという事に惹かれて応募しました。面接やインターンなどの選考を通して、最終的にBLAMを選んだ理由は主に下記の3つです。

  1. すごく成長できる環境だと感じたから
  2. 社員の方々に憧れを感じたから
  3. フレックスタイムと週4日リモートワークが働きやすいと感じたから

1つ目の成長できる環境だと感じた要因は、テクノロジー部が技術を大事にしていると感じたからです。技術ドリブンというわけでは無いですが、Rubyで書かれていたバックエンドをGoへリプレイスしたり、フロントエンドでReactを導入したりと、サービスを継続して運用していくためという長期的な目線で開発を行っているのでエンジニアとして成長できると感じました。また、現段階ではエンジニアの人数が多くないためバックエンドとフロントエンドの両方を開発でき、インフラ周りを触れる機会があるため、成長できると確信しました。
2つ目は、社員の人達に憧れを感じたからです。具体的に言うとお互いが信頼し合っている姿や、楽しんで仕事をしている姿、話し方や考えの深さなどが、自分目指している社会人像と重なったのでこの環境に身を置きたいと感じました。
3つ目は、子育てをしながらでもフレックスタイムを利用して働きやすいと感じたからです。私は学生中に結婚をしたので妻と子供がいます。そのため保育園の送り迎えなどで子供の時間に合わせる必要があり、拘束時間連続9時間の定時で働くことが難しい状況でした。BLAMは週4日リモートワーク(エンジニア職)+フレックスタイム制を導入しているためコアタイムの12時ー17時以外は基本的に自由な時間で働くことができます。新卒で子供がいる人はレアケースだと思ったので選考途中でご相談しました。その際に快くフレックスタイムの利用を了承して下さり、BLAMでなら子育てしながらでも働きやすいと感じました。

入社前と後のギャップ

このようなタイトルにして恐縮ですが、選考の際に1週間実際の業務を体験するインターンがあったので大きなギャップはありませんでした。笑

入社前のイメージ

まず、社員同士が尊敬し合っていると感じました。私が1週間のインターンで参加した当時は、主にデザイナーが所属するクリエイティブ本部と、エンジニアが所属するテクノロジー本部は合同の部署でした。チーム内でコミュニケーションを取る際の、年齢や立場関係無く丁寧な対応や、相手を信頼し意見を尊重している言動を見て尊敬し合っていると感じました。
また、とにかく社員の皆が活き活きと働いているという印象を持ちました。弊社ではoViceというツールを使っているのですが、リモートワークでもしっかりと過不足なく情報を伝え、円滑なコミュニケーションを取りながら仕事を進めていたり、時には雑談をしてリフレッシュしたりとメリハリを持って働いていると感じました。

入社してから感じたこと

入社前のイメージはその通りだったので、入社前に感じたこと にある内容は省略し主に下記の3つにまとめます。

  1. 思っていたよりも自由な働き方ができる
  2. 開発環境がとても良い
  3. 思っていたよりも手厚い体制で成長できる

1つ目は、入社前に思っていたよりも自由な働き方ができると感じました。例をあげると入社2日目にフレックスタイムを利用して歯医者に行かせてもらいまいた。笑 また、保育園のお迎えの時だけ一旦抜けて夜再稼働をしたり、朝早くに働き始めるなど柔軟に働けています。
2つ目は、開発環境がとても良いと感じました。M1チップの最新MacBook Proが貸与され、外部モニターなどがきちんと整備されているため働きやすいです。特に M1チップの最新MacBook Proが手元に来た時にはとても興奮しました。出社日は週に一度とはいえ、大きい外部モニターがあることで作業が捗ります。
3つ目は、思っていたよりも手厚い体制で成長できると感じました。具体的には、こまめなフィードバック・コードレビューや、OJTのような指導をしていただけています。指導していただく際も、ただ答えを教えてもらうだけではなく汎用的な考え方や足りない知識の埋め方などをアドバイスがもらえるので成長することができていると感じています。また、デイリースクラムでエンジニア同士が技術的な相談事をほぼ毎日しているため、自分のタスク以外のコードについて考える機会があり知識を増やすきっかけを与えてもらえています。

入社後1ヶ月間で何をやったか

行ったこと

入社してからはクリーンアーキテクチャへのリアーキテクト作業を主に行っています。まだ私のGoの理解やバックエンド開発の知識が薄いため、簡単な機能のリアーキテクトタスクにアサインしてもらっています。よくある新卒研修期間のようなものは無く、必要な敬語・マナー研修や他部署の説明会などを各30分〜1時間ほど行なっていただきました。
クリーンアーキテクチャへのリアーキテクト作業については現在まだ作業途中なので具体的な内容などは今回は省きます。(いずれテックブログにも掲載されるはずなのでお楽しみにしていただけると幸いです。🙇‍♂️)

学んだこと

入社してからの1ヶ月で学んだことを下記の3つにまとめます。

  1. 仕事の進め方
  2. クリーンアーキテクチャの理解
  3. チームとしての最適解を求め続ける姿勢

1つ目は仕事の進め方を学びました。具体的には話す前に要点をまとめること、また結論ファーストで話すこと、そして提案力です。提案力は、前提の共有、現状の課題と解決策、解決策を用いることでどうなるのかを相手に想像してもらうことだと思っています。チームで開発を進めるために円滑なコミュニケーションが必要だと思うので、まだまだ実際に行動に移し切れていませんが、上記のことは常に心がけたいと思います。
2つ目はクリーンアーキテクチャの理解です。今まではMVCでしかアプリケーションを開発したことがなかったため、最初は概念を理解することに苦労しました。しかし、作業前に弊社のCTOにまとまった時間を割いていただきマンツーマンで教えていただいたり、実際のタスクを行いつつコードレビューしていただいたりしながら、徐々に理解が深まってきました。
3つ目はチームとしての最適解を模索し続けている姿勢を学びました。現在テクノロジー部では、命名規則や共通処理をどこに置くか、エラーハンドリングはどうするかなどのことを、デイリースクラムや、適宜時間を見つけてチーム内で相談して擦り合わせチーム開発の効率を上げるための最適解を求め続けているので、チームにとって良い選択と言う判断軸をもつ意識が生まれました。

今後の目標

約1ヶ月働いていてまだまだ1人でタスクを行えていないので、まずは今年中に1人でタスクを実装できるようになりたいと感じています。そして現在はチーム内の相談では内容を理解することで精一杯なので、早くチームにとっての最適解を提案できるエンジニアになりたいと思っています。また、中期的にはフロントやインフラ周りでも戦力になれるようなエンジニアになりたいです。
そして、会社の一員として他部署との連携などをスムーズに行えるようになり、他の人が仕事をしやすくなる環境を作れる人間になりたいです。そのためにサービス理解を深め、会社全体のことを理解した上で、過不足無い情報の共有やドキュメントなどを更に整理していきたいです。

BLAMではエンジニアの工数可視化に取り組んでいます!

こんにちは!テクノロジー本部でエンジニアをやっている三崎です。(三崎 秋人 / Shuto Misaki (BLAM) (@smish0000) | Twitter
普段は自社サービス「カイコク」の開発をメインに行なっています。

今回はテクノロジー本部で行っている工数可視化の取り組みに関してご紹介します!

なぜ可視化を行っているのか

弊社でエンジニアの工数可視化に取り組んでいる理由は下記の通りとなります。

  1. 開発フローにおけるボトルネックを可視化する
  2. 機能開発やリファクタリングなど、タスクの種類別に工数を可視化する

以下、項目ごとの詳細をまとめていきます。

開発フローにおけるボトルネックを可視化する

開発フローにおいて、仕様策定やコーディング、レビューなど複数のステップがありますが、 各ステップに要した時間を可視化し、ボトルネックになっているステップがあれば、テコ入れをできるようにしています。

タスクの種類別に工数を可視化する

バグや機能開発、差し込みなどの分類を行い、種類別に工数を把握できるようにしています。

  • バグの対応工数が多ければテストコードやレビューフローを見直す必要がある
  • 差し込みの対応工数が多ければ依頼フローや計画時に調整を行う必要がある

などの判断をできる状態を目指しています。

計測方法

ここからは実際の計測方法をご紹介します!

今回の計測に当たって、下記の要件を設定しました。

  • ソースコードの管理にGitHubを使用しているため、タスク管理にもGitHubを使い、タスクフローをシンプルにする
  • コミットの作成やPRの作成などのイベントを拾い、自動で集計できるようにする
  • タスクの種類を判別するために、ラベルなどをつける
  • 数値を視覚的に見られるようにし、定期的に確認できるようにする
  • 下記のイベントの集計を行う
    • イシューの作成
    • イシューにおける最初のコミットの作成 (=コーディング開始)
    • PRの作成
    • PRの承認
    • イシューのクローズ

これらの要件を満たすために、下図のような構成にしました。

要件のイベント発生時にGitHub Actionsを実行し、ペイロードで渡されるイシューの担当者やコミット時間などを整形、Big Queryのテーブルにインサートしています。

一部省略していますが、GitHub Actionsの設定ファイルは下記のようになっています。

name: Post Code Event to BigQuery
on:
  issues:
    types: [opened, closed]
  pull_request:
    types: [review_requested]
  pull_request_review:
    types: [submitted]
jobs:
  post-event:
    name: Post Code Event to BigQuery
    runs-on: ubuntu-latest
    steps:
    - name: 'get the related issue with the PR'
      ...
    - name: 'post big query API'
      run: |
          curl -X POST https://bigquery.googleapis.com/bigquery/v2/projects/our-project/datasets/internal/tables/code_activities/insertAll \
            -H "Authorization: Bearer ${{ steps.auth.outputs.access_token }}" \
            -H "Content-Type: application/json" \
            -d '{"rows": [{"json": {"issue_id": "${{ steps.issue.outputs.ISSUE_ID }}", "issue_label": ${{ steps.issue.outputs.ISSUE_LABEL_NAMES }}, "issue_assignee": ${{ steps.issue.outputs.ISSUE_ASIGNEE_NAME }}, "issue_milestone": ${{ steps.issue.outputs.ISSUE_MILESTONE_NAME }}, "event_type": "${{ steps.event-type.outputs.EVENT_TYPE }}", "datetime": "${{ steps.current-time.outputs.CURRENT_TIME }}" }}]}'

そして、Redash上で下記のようにデータをまとめ、タスクごとのタイムラインやチーム全体のリードタイムやコーディング時間などを数値ベースで算出しています。

現在の運用

このように自動的に計測できる体制を整えたことで、工数の集計・可視化を自動化しつつ、下記のようなGitHub上で完結するシンプルな運用を実現することができました。

  1. イシューの作成 => Big Queryにデータ送信
  2. スプリントプランニングでGitHubのカンバンを確認しながら、担当者をアサイ
  3. 作業を開始し、手元のPCからGitHubにプッシュする
  4. GitHub上でPRを作成し、レビュー依頼を行う => Big Queryにデータ送信
  5. レビューを行い、GitHub上でPRの承認を行う => Big Queryにデータ送信
  6. PRをマージし、イシューをクローズ => Big Queryにデータ送信

また、PRの作成時に特定のキーワードを用いて、イシューとの紐付けを行うと、
PRのマージ時に紐付けたイシューをクローズし、カンバン内の移動も自動的に行ってくれます。

紐付けに関しての詳細はこちらをご覧ください。

まとめ

BLAMのテクノロジー本部では、上記のようにGitHub Actionsなどの技術を用いて、生産性の分析やチケットの管理にかかるコストを減らすことで、エンジニアが集中すべき箇所に集中して業務を行えるようにしています。
今後もエンジニアにとって働きやすい環境、エンジニアのアウトプットを最大化するための取り組みを積極的に行っていきます。

さいごにBLAMでは一緒に開発を行ってくれる方を募集しております。ご興味のある方は下記リンクからぜひご応募ください!

www.wantedly.com

www.wantedly.com

BLAM Developers BLOG開設🎉

みなさんこんにちは!株式会社BLAM CTOの大沼です。(ぬま (@_numa999) | Twitter
普段はテクノロジー本部で、自社サービス「カイコク」の開発をメインの業務として行なっています。

この度、「BLAM Developers BLOG」を開設しました🎉

このメディアでは「チーム運用のナレッジ」や「開発技術のナレッジ」、「BLAMのエンジニアの知識のアウトプットの場」として活用していきたいと考えています。

なぜやるのか?

今回BLAMでテックブログを開設する理由は

  1. エンジニアの技術の定着
  2. BLAMの認知向上
  3. 組織の技術に関して、可能な限りオープンにしたい

という3つの理由です。

エンジニアの技術の定着

1番の目的はエンジニアの技術力の向上に大きく役立つと考えています。
「インプットする→リリースする(もしくは検証する)→アウトプットする」と言うフローを繰り返すことにより自分の知識を整理でき定着するものだと思います。
そう言ったことを当たり前のように行いたいですし、個人のスキルをチームのものにしていく動きをとりにいけるとも考えています。

チームとしても推奨する動きをしていきたいと思っています!

BLAMの認知向上

やはりエンジニアたるもの、自身が所属する・興味がある組織の「文化」や「チーム運用の方法」や「そもそもの技術力」等気になると思います。
色々な物事をより良くしていきたいといった過程での課題や解決策もオープンにすることによってテクノロジー本部の外から見える信頼性も向上すると考えています。

組織の技術に関して、可能な限りオープンにしたい

僕が今まで所属した良い文化だと思う組織は全て、テックブログをやっています。常日頃オープンにしているナレッジの情報はとても参考になるものが多いです。
BLAMでも、会社として技術コミュニティや世のエンジニアに参考になるような事例を知識としてオープンにしていきたいと考えています。 会社の行動指針でもある「Give and Give」の精神で、将来的には同じようなことで迷っている開発チームの手助けになったり、コミュニティの発展に寄与して行けたら良いなと考えています。

最後に

初回のテーマもあり短くなってしまった&&できていないけど当たり前のことを多く記載していますが、しっかりとチームで運用していきたいと考えています。 今後ともよろしくお願いします🙋‍♂️