hubotでもMVWhateverしようぜ

hubotでもMVWhateverしようぜ

最初、hubot/scripts ディレクトリの下にこんな罪深いスクリプトを置いていた

# Description:
#   Deploy to specified ElasticBeanstalk environment
#
# Commands:
#   hubot eb deploy to ENVIRONMENT:VERSION

module.exports = (robot) ->
  robot.respond /eb\s+deploy\s+to\s+([\w_.-]+)\s*:\s*([\w_.-]+)\s*$/i, (msg) ->

    {ElasticBeanstalk} = require 'aws-sdk'
    eb = new ElasticBeanstalk
      region: process.env.AWS_DEFAULT_REGION

    params =
      EnvironmentName: msg.match[1]
      VersionLabel: msg.match[2]
    eb.updateEnvironment params, (err, data) ->
      if err
        msg.send err.stack
        return

      msg.send "```#{JSON.stringify data, null, 2}```"

最初はこれでよかった、まぁ、最初は。でもだんだん、ElasticBeanstalk の deploy も hubot でやろうぜとか、github の tag を見るだけじゃなくて tag も打とうぜとかいろんな話が出てきて、scipts/eb_deploy_to.coffeeとかに書くのに限界が出てきました。それにこれじゃテストできねえよってのもあって、MVWhatever しようぜって話に落ち着きました(たぶん実際にもう少し取り扱う Model が事前に明確であれば、MVWhatever じゃなくてもよかったのかなって思うんですが、ぼくらの hubot の場合は取り扱う Model が Jenkins の job だったり、CloudFront だったり、RDS だったり、ElasticBeanstalk だったりするので、返って Model がかっちりしてなくてよかったなぁってのはあります。)。

っというわけで、MVWatever すると

$tree ./
├── README.md
├── docker
│   └── Dockerfile
├── external-scripts.json
├── hubot-scripts.json
├── package.json
├── scripts
│   ├── controllers
│   ├── cron.d
│   ├── models
│   ├── roles
│   └── services
└── test
    ├── e2e
    ├── helper
    ├── mocha.opts
    ├── mock
    ├── regex
    ├── spec
    └── stub

MVWhatever しようぜっていうか、ディレクトリ構造をまともに保っておこうぜ的なはなしに近いかもですね。models には、たとえば

#models/eb_environment.coffee
{ElasticBeanstalk} = require 'aws-sdk'

module.exports = class RichElasticBeanstalkEnvironment

  constructor: ({
    @region
    @environmentName
  }) ->
    @eb = new ElasticBeanstalk
      region: @region

  deploy: (revision) =>
    params =
      EnvironmentName: @environmentName
      VersionLabel: revision

    return new Promise (resolve, reject) =>
      @eb.updateEnvironment params (err, data) ->
        if err
          reject err
          return

        resolve data

みたいな。Deploy のメソッドだけを書いておいて、controller 側で

#controllers/eb_deploy.coffee
EBEnv = require '../models/eb_environment'
#仮にslackに通知するnotify serviceみたいなのがあるとして
NotifyService = require '../services/slack'

module.exports = class EBDeployController

  constructor: ({
    @region
    @environmentName
    @revision
  }) ->
    @region ?= process.env.AWS_DEFAULT_REGION
    @ebEnv = new EBEnv
      region: @region
      environmentName: @environmentName
    @slack = new NotifyService

  execute:() =>
    @ebEnv.deploy revision
      .then (updatedEbDescription) ->
        @slack.notify "#{@environmentName} deployment is succeeded."

      .catch (err) ->
        @slack.notify """
          #{@environmentName} deloyment is faled!
          #{err.stack}
        """

みたいな感じで flow 制御と model の各メソッドの責務を分けやすくしてます。あと副次的な効果ですが、model のテストなんかはものによってはかなり楽になるんじゃないですかね。んでもってさいごにこれを

# Description:
#   Deploy to specified ElasticBeanstalk environment
#
# Commands:
#   hubot: eb deploy to ENVIROMENT:REVISION

EbDeployCtrl = require './controllers/eb_deploy'

module.exports = (robot) ->
  robot.respond /eb\s+deploy\s+to\s+([\w_.-]+)\s*:\s*([\w_.-]+)\s*$/, (msg) ->

    environmentName = msg.match[1]
    revision = msg.match[2]

    ebDeployCtrl = new EbDeployCtrl
      environmentName: environmentName
      revison: revision
    ebDeployCtrl.execute()

とかやっておけば、正規表現だけのテストもできるし、model だけの unittest も書けるしで、なんかコマンドは吸われてるっぽいけど、ちゃんと実行されてるかどうかわかんねーって現象は防げるんじゃないかなと思います。

ぼくは hubot が結構好きです。ぼくたちの MVWhatever of hubot でした :metal:

octobot: タコ・ソ・ノモノ