Time to Read

3分

mocksmtpd という便利ルビージェムがある。詳細な使い方は 作者さんの日記 にもあるが、今日はより簡単にメール配信のテストが出来るようなおまじないのやり方を紹介する(主に、未来の自分が見返すために)。

インストール

rubygems.org には謎のフォークされた gem がホストされていて胡散臭い。さいわい、githubにホストされているため、今日びのフレームワークなら bundler とかを使って楽にインストールできる。

Gemfile:

1
2
3
group :test do
  gem 'mocksmtpd', :git => "git://github.com/koseki/mocksmtpd.git"
end

しかるのち bundle コマンド。

準備

動作に必要な設定ファイルやディレクトリ~がある。APP_ROOTにもぐって以下を実行。

1
2
3
4
5
$ bundle exec mocksmtpd init
Created: mocksmtpd/
Created: mocksmtpd/inbox/
Created: mocksmtpd/log/
Created: mocksmtpd/mocksmtpd.conf

mocksmtpd/mocksmtpd.conf はほぼデフォルトのままで良いが、 UN*X 系のシステムでは 1024 番以下のポートをリスンするには root 権限が必要で、テストのために sudo するわけにもいかないので、ポートだけは変えておく。 10025番とかいいんじゃないでしょうか:

1
2
3
4
5
6
7
8
9
10
ServerName: mocksmtpd
Port: 10025
RequestTimeout: 120
LineLengthLimit: 1024
LogLevel: INFO
 
LogFile: ./log/mocksmtpd.log
PidFile: ./log/mocksmtpd.pid
InboxDir: ./inbox
Umask: 2

ここまできたら、

1
$ bundle exec mocksmtpd -f mocksmtpd/mocksmtpd.conf

とすることで、モックの smtp サーバが立ち上がるはずなので、 lsof -i:10025 したり、メールを送りつけたりできるか試してほしい。

で、以下は RSpec 利用の場合なので適宜読み替えてほしい。

RSpec の before フックで、 mocksmtpd サーバを立ち上げるスレッドを用意する。こんな感じ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  before(:all) do
    # サンプルは Padrino なので、こういう便利APIがある。普通に Sinatra とかの場合は適宜読み替え
    app_root = Padrino.root
 
    # 下処理
    FileUtils.rm Dir.glob(File.join(app_root, "mocksmtpd", "inbox", "*"))
 
    @conf = File.join(app_root, "mocksmtpd", "mocksmtpd.conf")
    @smtp_thread = Thread.new do
      Mocksmtpd.new(["-f", @conf]).run
    end
 
    # 10025 番ポ~トがリスンされるまで待つ
    until (TCPSocket.new("localhost", 10025) rescue false)
    end
  end

after フックの後始末は今回やらない。理由は:

  • テストプロセスが終了すれば無事 SMTP サーバも終了する
  • SMTPD プロセスを一度立ち上げれば、テスト終了までプロセスを落とす理由がない
  • そもそも Mocksmtpd 自身にポートを unlisten する API が用意されていないため、上手にできない(Thread を kill しても unlisten されません)

送ったメールを取得する

たとえばパドリーノフレームワークであれば、以下のような設定を追加することで、テスト環境でのメール送信をアレできる。

1
2
3
4
5
6
7
8
9
10
class DoMailApp < Padrino::Application
  configure :test do
    set :delivery_method, :smtp => {
      :address              => "localhost",
      :port                 => 10025,
      :enable_starttls_auto => true,
      :openssl_verify_mode  => OpenSSL::SSL::VERIFY_NONE
    }
  end
end

Rails とかでも同じような感じだし、まあ昨今は Pony とかも使うと思うけれど大体一緒(どれも Mail gem 使ってますんで)。

で、ここで一点困ったことが、 mocksmtpd はそもそも 送ったメールをHTMLにして目視確認するために 作られたものなので、プログラムに読ませるには若干工夫が必要になる。

以下をご参照あれ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
include Rack::Test::Methods
def app
  DoMailApp
end
 
describe DoMailApp do
  it "sends email" do
    # メールを送るエンドポイント
    get "/send/email"
 
    # メールは、 APP_ROOT/mocksmtpd/inbox/(\d+ = 送信日時).html に保存される
    filename = 
        Dir.glob(File.join(Padrino.root, "mocksmtpd", "inbox", "*.html")).
            select{|n| n =~ /\d+\.html/}.first
 
    # メールからメール本体部分だけを抜き取る
    # nokogiri を使えれば便利だが、まあ、以下の正規表現でも大丈夫
    re = /<div id="source" style="border: solid 1px #666; background:white; padding:2em;">\n(.*)<\/p>\n<\/div>/m
    body = open(filename, "r").read.scan(re).flatten.first.gsub(/<br.*>/, '')
 
    body.should match /Subject:\s*A Mail Sent/
  end
end

おもったこと

そもそも自動テスト向けの作りになっていないこともあって、今の API 具合ではやっぱり自動化テスト時に不便な感じがするので、メールソースそのまま保存する API とか、あと停止する API とかを追加したものを fork すると便利に使えそう。

あと、他のプロトコル( irc とか)でも同じ考え方で応用できそう。