【Rails5】assert_differenceで複数の値をチェックする

概要

特定の処理の前後における数値の増減をテストしたいことはよくあります. 例えばPOSTリクエストの前後でUserが増加するテストなどです.

これをテストするにはassert_differenceというメソッドを使い, 次のように書けばいいですね.

assert_difference 'User.count', 1 do
  post users_path, params: { user: { name: "foo" } }
end

では一つの処理に対して複数の値をチェックしたい場合はどうすればよいでしょうか. 実はこれもassert_differenceでテストすることができます.

テストの書き方

詳しくは公式ドキュメントを参照してもらうことにして, ここでは2つのケースを考えてみます.

チェックする値は複数あるが, 増減幅は一緒の場合

例えば記事Articleを1つ投稿すると通知Notificationも1つだけ送られる場合を考えます. このときは以下のように書けばよいでしょう.

assert_difference [ 'Article.count', 'Notification.count' ], 1 do
  # 記事を投稿する処理
end

チェックする値が複数あり, それぞれ増減幅が異なる場合

例えば記事Articleを1つ投稿しつつ, 新たにタグTagを2つ作成する場合を考えます. このときは以下のようにテストを書くことができます.

assert_difference ->{ Article.count } => 1, ->{ Tag.count } => 2 do
  # 記事を投稿する処理
end

ラムダを使った特殊な記法ですね.

ひとこと

あったらいいのになーと思う機能があったりなかったり. もっとRailsを理解していけば, 「こんな機能はRailsが用意してくれてるはずだ」と分かるようになっていくんでしょうかね.

【Rails】RakeタスクをMinitestでテストしてみた

はじめに

TDDでRakeタスクを作ろうとしたものの, テストをどう書けば良いのかがわかりませんでした. ネット上にあるのは, RSpecの記事や英語の記事ばかりだったため, MinitestでRakeタスクをテストする方法を書き残しておくことにしました.

主に参考にしたのは以下の記事です.

https://ieftimov.com/test-rake-tasks

Rakeタスクの例

具体例を考えるのが面倒なので, 自分が実際につくったタスクをもとに説明していきます.

以下は, YAMLファイルからデータを読み込んでusersテーブルに登録するタスクです.

# lib/tasks/user.rake
require 'yaml'

namespace :user do
  desc "insert users"
  task create_users: :environment do
    users = YAML.load_file(File.dirname(__FILE__) + "/users.yml")
    users.map do |user|
      User.create!(user)
    end
  end
end

テスト

テストコードは以下のように書きました.

# test/lib/create_users_test.rb
require 'test_helper'
require 'yaml'

class CreateUsersTest < ActionDispatch::IntegrationTest
  def setup
    # タスクをロード
    # Myappはアプリケーション名
    Myapp::Application.load_tasks

    # 挿入されるレコードの数を取得
    @count = YAML.load_file(Rails.root.to_s + '/lib/tasks/users.yml').count
  end

  test "users creation" do
    # タスク実行前後でユーザーの数が変化していることをテスト
    assert_difference 'User.count', @count do
      Rake::Task['user:create_users'].invoke
    end
  end
end

ポイントは, タスクをロードするコードと, タスクを実行するコードですね. これさえ押さえておけば, あとは普通のテストと一緒です.

ちなみにテストコード内のMyappはアプリケーション名ですが, これはconfig/application.rbに定義されているモジュールの名前です. アプリケーションごと異なるので注意してください.

また, test/libというディレクトリは自分で作りました.

テストの実行

ここで作成したテストは, rails test:libのように実行できないようです. rails testでは実行されました. 個別にテストしたい場合はファイル名を指定するのが良さそうですね.

HaskellのStateモナドを定義から理解する

本記事の目的

Stateモナドの挙動を定義から導出したい. それが本記事の目標です.

Stateモナドについて調べると, いくつかの記事がヒットします. そこではStateモナドの使い方として以下のようなサンプルコードが引き合いに出されることが多いでしょう.

import Control.Monad.State

pickHead :: State [Int] Int
pickHead = do
    x:xs <- get    -- 状態(整数のリスト)を取得
    put xs         -- 先頭以外の部分を新たな状態としてセット
    return x       -- 先頭を返す

sumHeads :: State [Int] Int
sumHeads = do
    x <- pickHead  -- 先頭を取得(このとき状態が更新される)
    y <- pickHead  -- 先頭(初期状態では2番目の要素)を取得
    return $ x + y -- 足して返す

main = do
    print $ runState sumHeads [1, 1, 1, 1, 1]
    print $ runState sumHeads [1, 2, 3, 4, 5]

実行結果は以下の通りです.

(2,[1,1,1])
(3,[3,4,5])

コメントが書かれているため, どのように処理が進んでいくのかを追うことはできます. しかし釈然としません. なぜStateモナドはまるで状態を保持しているように振る舞うのかがわからないのです.

本記事ではそれを解決したいと思います.

解説

Stateモナドの定義

コードを定義から繙いていくわけですから, 定義の把握が必要ですね.

newtype State s a = State { runState :: (s -> (a,s)) } 
 
instance Monad (State s) where 
    return a        = State $ \s -> (a,s)
    (State x) >>= f = State $ \s -> let (v,s') = x s in runState (f v) s' 
class MonadState m s | m -> s where 
    get :: m s
    put :: s -> m ()

instance MonadState (State s) s where 
    get   = State $ \s -> (s,s) 
    put s = State $ \_ -> ((),s) 

ひとつの補題

サンプルコードを理解する上で助けになる補題をひとつ挙げておきます. 補題といっても定義をそのまんま適用したものにすぎません.

まずは補題に必要な登場人物の紹介です.

m :: State s a
f :: a -> State s b
x :: s

上記の設定のもとにおいて, 以下の2式は等価です.

runState (m >>= f) x
runState (f a) x'

ただし以下の言い換えをしています.

(a, x') = runState m x

記号aが重複していますが, そのほうが対応がわかりやすいだろうとの判断です.

以上が補題の主張です. >>=の定義を追っていけば確かめられるでしょう. この補題により, >>=をひとつずつほぐしていけるようになりました.

コードを読み解いていく

最初に挙げたサンプルコードを解読していきます.

まずは部分的に

まずは以下の式が何をどう返すのかをみていきましょう.

runState pickHead [1,2,3]

pickHeadの定義をそのまま適用します.

runState (
    x:xs <- get
    put xs
    return x
    ) [1,2,3]

ただし上の式は文法的に正しくありません. <-等の記号はdoブロックの中でしか使えないからですね. 見辛くはなりますが, 正しく書き換えましょう.

runState (
    get >>= \(x:xs) -> 
    put xs >> 
    return x
    ) [1,2,3]

こんな風にラムダ式の中でもパターンマッチが使えるんですね. さてこれで, 上で述べた補題が使える形になりました. では一つ目の>>=を解消しましょう.

getの定義から

runState get [1,2,3] == ([1,2,3],[1,2,3])

ですから, もとの式は次の式に等価です.

runState (f [1,2,3]) [1,2,3]

ただしf = \(x:xs) -> put xs >> return xという置き換えをしています.

さて,

f [1,2,3] == put [2,3] >> return 1

ですから, 結局もとの式は次の式に等価です.

runState (put [2,3] >> return 1) [1,2,3]

>>>>=の特殊なバージョンですから, この式にも補題を適用することができますね.

putの定義から,

runState (put [2,3]) [1,2,3] == ((), [2,3])

ですから, 結局もとの式は次の式に等価です.

runState (return 1) [2,3]

これを計算することにより, 以下が得られました.

runState pickHead [1,2,3] == (1, [2,3])

そして全体へ

今得られた等式を利用して, コード全体を読み解いていきましょう.

評価したいのは以下の式です.

runState sumHeads [1,2,3]

これが(3, [3])に等しいことを定義から導出していきます.

上の式に, sumHeadsの定義を文法が正しくなるように適用します.

runState (
    pickHead >>= \x ->
    pickHead >>= \y ->
    return $ x + y
    ) [1,2,3]

先と同様に補題を適用して>>=を解消していきます.

上で得られた結果から

runState pickHead [1,2,3] == (1, [2,3])

ですから, もとの式は次の式に等価です.

runState (
    pickHead >>= \y ->
    return $ 1 + y
    ) [2,3]

同様にして以下のように変形できます.

runState (
    return $ 1 + 2
    ) [3]

これを計算することにより, 以下の式が得られました.

runState sumHeads [1,2,3] == (3,[3])

まとめ

なんとか処理を追いかけることができました. 変化させた状態を次々にリレーしていく様子が明らかになったのではないでしょうか. このようにしてStateモナド状態を保持しているように振る舞うことができるわけですね.

【GAS】GoogleDrive上のExcelをGoogleスプレッドシートに一括変換

概要

まだまだExcelさんは現役ですね. でもMac使いだとOfficeがインストールされていなかったり, 共有したいときに不便だったりします. そんなときはGoogleスプレッドシートに変換してしまいましょう. GoogleDrive上でExcelファイルを開くと, 「Googleスプレッドシートで開く」というボタンが表示されます. それをクリックすればGoogleスプレッドシートに変換されたファイルが新たに生成されます.

ただし変換したいファイルが多数ある場合はプログラムで一括変換したいものですね. この記事ではGASからDrive APIを利用することにより, ExcelファイルをGoogleスプレッドシートに一括変換する方法をご紹介します.

ただし注意していただきたいのは, ExcelGoogleスプレッドシートは完全な互換性があるわけではありません. Excelにしかない関数やマクロを使っている場合はうまく変換できないことがあります.

参考ページ

いろいろ検索しまくって, 一番参考になったのはやっぱり公式リファレンスでした. このページには, GASからDrive APIを使う方法が書かれています. 本記事では少し手を加えて, Excelスプレッドシートに変換できるようにしていきます.

日本語のページではこちらも大いに助けになりました.

本記事ではGASの基礎を前提として進めていきます. 分からないところがあれば, 前回の記事を参考にしてください.

やってみよう

用意するもの

  • 複数のExcelファイルが入ったGoogleDrive上のフォルダ
  • スプレッドシートに変換されたファイルが格納されるフォルダ(最初はからっぽ)

同じフォルダに格納されても全く問題はありませんが, 今回は分けることとします.

早速コード

function myFunction() {
    // Excelファイルが入っているフォルダをidによって取得
    var source_folder = DriveApp.getFolderById('*****');
    // Excelファイルたちを変数に保存
    var excel_files   = source_folder.getFiles();

    // 変換されたファイルが格納されるフォルダをidによって取得
    var dest_folder   = DriveApp.getFolderById('*****');

    // Excelファイルをイテレートして順にスプレッドシートに変換
    while(excel_files.hasNext()) {
        var file = excel_files.next();
        convertToSpreadsheet(file, dest_folder);
    }
}

function convertToSpreadsheet(file, folder) {
    // 各種オプションを設定
    // mimeTypeの指定により, スプレッドシートに変換される
    options = {
        title: file.getName(),
        mimeType: MimeType.GOOGLE_SHEETS,
        parents: [{id: folder.getId()}]
    };

    // Drive APIへfileをPOSTする
    Drive.Files.insert(options, file.getBlob())
}

Drive APIを利用しているのはDrive.Files.insertの部分です.

Drive APIを有効にする

コードを書いただけではAPIを利用することはできません. APIを有効にするには, 以下の手順を踏む必要があります.

  • 「リソース」メニューから, 「Googleの拡張サービス」をクリック.
  • 一覧の中から「Drive API」を探し, 有効にする.

f:id:riemann1618:20181020171423p:plain:w500

f:id:riemann1618:20181020171249p:plain:w500

  • 右端にある「認証情報を作成」をクリック

f:id:riemann1618:20181020171313p:plain:w500

  • 認証情報を以下のように作成

f:id:riemann1618:20181020171448p:plain:w500

認証情報が作成できれば, このページは閉じてもらって構いません. APIの一覧が並んだダイアログの「OK」を押せば, APIの有効化は終了です.

実行

以上で準備は整いました. 初回実行時は認証が必要ですから, 指示にしたがって認証をしましょう. もしうまくいかないようであれば, 前回の記事を参考にしてください.

全てのExcelファイルがGoogleスプレッドシートに変換されていれば成功です.

GASからGoogleDrive操作してみた.

概要

Google Apps Scriptを使って, Google Drive上のファイルにアクセスしてみました. 当然ながら, アクセスするのは自分のGoogleアカウントに紐づいたDriveです.

情報源

基本的には公式リファレンスを読めば大体のことがわかります. 困ったときはそちらを参照するのがよいでしょう.

やってみよう

例として, 特定のフォルダにあるファイル名を全てログに出力するということをやってみます.

コード

まずはじめにコード全体をお見せします.

function myFunction() {
    // idをもとにフォルダを取得
    var folder = DriveApp.getFolderById('********');

    // フォルダに属するファイルを全て取得
    var files = folder.getFiles();

    // filesをイテレートして, ファイル名をログに出力
    while(files.hasNext()) {
        var file = files.next();
        Logger.log(file.getName());
    }
}

解説

はじめ, IDをもとにフォルダを取得していますが, そもそもフォルダのIDとは何なのでしょうか. それはGoogleDrive内のフォルダをひとつ開いてみるとわかります. URLが以下のようになっていますよね.

https://drive.google.com/drive/folders/******

******の部分がこのフォルダのIDです. これをコピペすればいいというわけです.

次に変数filesについてです. これはリファレンスを見ればわかるように, FileIteratorクラスのオブジェクトです. ファイルの配列ではないんですね. したがってファイルを順に処理したい場合は上のコードのようになります.

実行

実行するには三角の再生ボタンを押します. 初回の実行時は, 「承認が必要です」とのダイアログが表示されるでしょう. その場合は以下の手順を実行します.

  • 「許可を確認」をクリック
  • 自分のアカウントを選択

このとき, 「このアプリは確認されていません」というダイアログが表示された場合は, 追加で以下の手順を実行します.

  • 「詳細」をクリック
  • 「プロジェクト名(安全ではないページ)に移動」をクリック
  • 「許可」をクリック

これでコードが実行されるはずです. 上部にある「表示」メニューからログを選ぶと, ファイル名が順に表示されているのが確認できます.

さいごに

Google Driveの特定のフォルダにあるファイル名を, すべて列挙することに成功しました. とはいえこれができたところでなにも嬉しくはありません. 次回の記事では今回の応用として, ドライブのフォルダ内にあるエクセルファイルを一括でGoogleスプレッドシートに変換する方法を解説します.

Slack Botの土台をGASで構築

Slack Botの土台をGASで構築

業務効率を上げたり, 時には癒しをくれたり, 調べてみると様々なBotの活用事例があるようです. この記事ではSlackBotの土台となるものをGASで構築していきます.

ゴール

この記事を読んだらなにができるようになるかを述べておきましょう. 本記事におけるBotの土台とは,

Slackで特定のワードを発言すると, Botが反応して一定の返事をくれる

というものです. これだけっちゃこれだけですが, GASを利用しているので, 以下のようなBotに発展させることもできるでしょう.

手順

以下のような手順で進めていきます.

  • Webhook URLを取得
  • SlackからPOSTリクエストを受け取り, POSTリクエストを返す機構をGASで構築
  • SlackからPOSTリクエストを発行するためのトリガーを設定

Webhook URLを取得

Webhook URLというのは, メールアドレスのようなものですね. このURLに対しPOSTリクエストを投げることで, Slackにメッセージを投稿することができます. ではやっていきましょーう.

まずはSlack APIにアクセスしましょう. 右上のCreate New Appをクリックします. そうなんです実は, これからBotをSlackアプリとして作ろうとしているんですね. さてCreate New Appをクリックすると次のようなダイアログが出現します.

f:id:riemann1618:20180928204913p:plain:w500

上の欄には好きなアプリ名を, 下の欄では実験用のWorkspaceを選択しましょう. 注意書きにあるように, アプリ名は後から変えられるので心配いりません.

入力を終えたらCreate Appをクリックします.

すると以下のようなページに飛びます.

f:id:riemann1618:20180928205106p:plain:w500

まずはBotsをクリックし, このアプリがBotとして動作するよう設定します. といっても2つの入力欄を埋め, Always Show My Bot as Onlineをオンにするだけです.

f:id:riemann1618:20180928205817p:plain:w500

この設定を保存したら, 次に左のメニューのIncoming Webhooksをクリックし, トグルをONにします.

f:id:riemann1618:20180928210003p:plain:w500

下の方にいってAdd New Webhook to Workspaceをクリック.

f:id:riemann1618:20180928210058p:plain:w500

Botからの返信を待ち受けるチャンネルをここで選択します. 選択できたらAuthorizeをクリック.

f:id:riemann1618:20180928210439p:plain:w500

するとSuccess!というFlashメッセージとともに, 先のページにリダイレクトされます. 下のほうをみると, Webhookが追加されているのがわかりますね. Webhook URLもちゃんと書いてあります. これで第一段階は終了です.

f:id:riemann1618:20180928210758p:plain:w500

SlackからPOSTリクエストを受け取り, POSTリクエストを返す機構をGASで構築

さて次はGASです. Google ドライブ にアクセスし, GASプロジェクトを新規作成します. プロジェクト名はお好きに決めてください.

f:id:riemann1618:20180928211112p:plain:w500

ここからはコーディングです. GASのベースはJavaScriptですから, メソッド名の最後に()をつけたり, 式をセミコロンで締めくくったり, 気をつけなければならないことがたくさんありますね...

// POSTリクエストを受け取る関数
function doPost(e) {
  
  // Webhook URLを変数に格納
  var url = 'https://hooks.slack.com/services/ほにゃらら';
  
  // メッセージ本文
  var payload = {
    'text': 'ここが本文'
  };

  // HTTPリクエストの設定
  var options = {
    'method': 'post',
    'contentType': 'application/json',
    'payload': JSON.stringify(payload)
  }
  
  // SlackへのPOSTリクエストを発行
  UrlFetchApp.fetch(url,options);
}

各コードの意味はコメントに書いたので, だいたい理解できるかと思います.

コードが書けたら上の方にあるデバッグボタンを押して, 問題がないかチェックします. 問題がなければコードが実行されるので, Slackチャンネルに「ここが本文」というメッセージが投稿されるはずですが, どうでしょうか...?

次はこのアプリをネット上に公開する必要があります. そうでないとSlackからPOSTリクエストを投げられないですからね. 上の方の「公開」から, 「アプリケーションとして導入」を選択します.

f:id:riemann1618:20180928211535p:plain:w500

アプリケーションにアクセスできるユーザーを, 「全員(匿名ユーザーを含む)」に設定し, 「導入」をクリックします. 「変更内容の説明」は空欄でOKです.

f:id:riemann1618:20180928211553p:plain:w500

すると承認が必要という旨のダイアログが出るので, 誘導にしたがって承認作業をしてください.

次のようなダイアログが出れば成功です. 「現在のウェブ アプリケーションのURL」はあとで使うので, ダイアログは出したままでOKです.

f:id:riemann1618:20180928211923p:plain:w500

これで第二段階は終了です.

SlackからPOSTリクエストを発行するためのトリガーを設定

次は, SlackにTrigger wordを設定します. まず実験用に用意したWorkspaceへアクセスしてください. 左のペインからアプリを検索します.

f:id:riemann1618:20180928212222p:plain:w500

Outgoing WebHooksを検索します. 数文字入力すれば十分でしょう. インストールボタンをクリックします.

f:id:riemann1618:20180928212307p:plain:w500

Add Configurationをクリックします.

f:id:riemann1618:20180928212642p:plain:w500

次のページでAdd Outgoing WebHooks integrationをクリックします.

リダイレクト先のページを下にスクロールすると, 以下のような入力フィールドが現れます.

f:id:riemann1618:20180928212911p:plain:w500

ChannelではTrigger wordに反応してほしいチャンネルを選択します.

Trigger wordsはコンマ区切りで入力しましょう.

URLにはさっきの「現在のウェブ アプリケーションのURL」を入力します.

以上の入力ができたら一番したまでスクロールし, Save Settingsをクリックします.

これで第三段階も完了です.

完成とその後

これで全ての手順が終了しました. Slack上でTrigger wordを送信すると, 自動で返信が来ることが確認できるかと思います. 返信の表示形式をもっとリッチにする方法を, またこんど記事にするつもりです.

そして上でも述べましたが, GoogleカレンダーGoogle翻訳, Gmailなどのサービスを活かしたBotを作成することもできます. 本格的なBotを作るのは無理でも, 多少遊んでみるくらいはできそうです.

【Rails5】数時間溶かしたエラーの原因はRoutingにあった

長時間悩んだ末にエラーが解決できると, なんとも言えない達成感があります. それと同時に,「俺の時間があああ」とも思いますが...

今回時間を奪ったものは正確に言うと, エラーではなくテストです. 何度やってもテストが通りませんでした.

しかしエラーも引き起こし得るミスですから気をつけたいところですね. ただしRESTfulにRoutingを組んでおけば起ることのない事態ですから, もっとRESTを意識しておけばよかったのかもしれません.

間違ったコード

テストが通らなかった状態では, 以下のような記述をしていました.

# routes.rb
get :account_activation, to: 'account_activations#activate'
# account_activations_controller.rb
def activate
  # params[:id] を使う処理
end

なにがまずいのか

上のRoutingではnamed pathの挙動が以下のようになります.

irb(main)> app.account_activation('IdForActivation')
=> "/account_activation.IdForActivation"

本当はaccount_activation/IdForActivationというパスになって欲しいのに, そうはなっていないんですね.

正しいコード

:idを使うことをRailsに教えてやればOKです.

# routes.rb
get 'account_activation/:id', to: 'account_activations#activate', as: :account_activation

asオプションは, named pathを設定するためのものです. これがないとnamed pathが使えませんでした.

補足とまとめ

はじめにも述べたように, resourcesresourceメソッドを用いてRESTfulにRoutingを組んでいれば起こらないミスでした. もしRESTfulにするならば,

resources :account_activations, only: [:edit]

のように書けばよかったですね. もちろんこの場合はアクション名もactivateからeditに変わりますが.

今回得た学びは, 便利メソッドがやってくれていることを理解しよう ということですね.

resourcesというメソッドは7つものRoutingを一気に設定してくれます. しかしカスタマイズされたRoutingを設定したい場合は, resourcesの仕事を肩代わりしなくてはなりません. そのためにはその仕事内容を理解する必要があるということでした.