ruby-jmeter

JMeterはとても強力なツールなんですが、UIがいまいち(ですよね?)なのとテストケースがXMLなので、あまり積極的に使っていませんでした。

しかし、どうしてもJMeterを使わざるを得ないケースが出てきて*1GUIツールとXMLを避ける方法をいろいろと探していたところ、ruby-jmeterがというライブラリが見つかりました。

ruby-jmeter

https://github.com/flood-io/ruby-jmeter

Tired of using the JMeter GUI or looking at hairy XML files?

はい、疲れました…というひとのためにRubyDSLjmxを出力 or JMeterを実行してくれます。

require 'ruby-jmeter'

test do
  thread_group count: 3, duration: 60 do
    visit name: 'example_com', url: 'http://example.com' do
      header([
        {name: 'User-Agent', value: 'test.rb'}
      ])
    end

    post name: 'www_example_com', url: 'http://www.example.com', raw_body: 'any_data' do
      {name: 'User-Agent', value: 'test.rb'}
    end
  end
end.run(properties: nil) # run -> jmx でjmxファイルを出力

おかげさまでJMeter嫌いがほぼ直った気がします。

Rubyでグラフ化

出力したjtlファイルはGUIツールでグラフ化できますが、それもだるいのでjtlファイルのパーサをRubyで書きました。

https://bitbucket.org/winebarrel/jtl

Gruff等のグラフ描画ライブラリと連携すれば、GUIツールを使わなくてもグラフが書けます。

jtl = Jtl.new('jmeter.jtl', interval: 10_000)
g = Gruff::Line.new

g.title = 'elapsed (avg)'
g.labels = jtl.scale_marks.map {|i| i.strftime('%M:%S') }.to_gruff_labels

g.data :all,  jtl.elapseds {|i| i.mean }
g.data :example_com, jtl.elapseds.example_com {|i| i.mean }
g.data :www_example_com, jtl.elapseds.www_example_com {|i| i.mean }

f:id:winebarrel:20140126222019p:plain

jtl = Jtl.new('jmeter.jtl').flatten
frequencies = jtl.elapseds.frequencies(10)

g = Gruff::Bar.new
g.title = 'histogram'
g.labels = frequencies.keys.to_gruff_labels {|k, v| (v % 100).zero? }
g.data :elapsed, frequencies.values

g.write('histogram.png')

f:id:winebarrel:20140127053735p:plain

統計レポートも以下のようなかんじで出力できます。(厳密な90% Lineの計算方法は調べてないですが…)

require 'jtl'

jtl = Jtl.new('jmeter.jtl')
rows = []

# たぶん不正確です。正規化しないとまずいような…
def _90_line(data)
  data = data.sort
  n = (data.length * 0.1).to_i
  data.slice!(-n, -n)
  data.max
end

def error_rate(data)
  100.0 * data.count {|i| !i } / data.length
end

def data_to_row(label, data_set)
  [
    label.slice(0, 7),
    data_set.flatten.length,
    data_set.flatten.mean.to_i,
    data_set.flatten.median.to_i,
    _90_line(data_set.flatten),
    data_set.flatten.min,
    data_set.flatten.max,
    error_rate(data_set.flatten),
    data_set.map {|i| i.length }.mean.to_i
  ]
end

# header
rows << [
  'Label',
  'Samples',
  'Average',
  'Median',
  '90%Line',
  'Min',
  'Max',
  'Error%',
  'req/sec',
]

jtl.labels.each do |label|
  rows << data_to_row(label, jtl.elapseds[label])
end

rows << data_to_row('Total', jtl.elapseds)

puts rows.map {|i| i.join("\t") }.join("\n")
Label   Samples Average Median  90%Line Min     Max     Error%  req/sec
example 689     130     126     707     120     707     0.0     11
www_exa 688     126     121     668     117     668     0.0     11
Total   1377    128     125     707     117     707     0.0     22

jmeterRSpec

RSpecも書けそうなので書いてみました。

require 'ruby-jmeter'
require 'jtl'
require 'tempfile'

def jmeter(params = {}, &block)
  jtl = nil

  Tempfile.open(File.basename(__FILE__) + ".#{$$}") do |f|
    test(&block).run(jtl: f.path, properties: nil)
    f.flush
    jtl = Jtl.new(f.path)
  end

  return jtl
end

describe "load" do
  it "should return a status code 200" do
    jtl = jmeter {
      constant_throughput_timer throughput: 60.0

      thread_group(count: 1, duration: 10) do
        visit(url: 'http://example.com/')
      end
    }

    expect(jtl.response_codes.flatten.all? {|i| i == 200 }).to be_true
    expect(jtl.elapseds.flatten.all? {|i| i <= 1_000 }).to be_true # <= 1,000ms
  end
end

確認すべきパラメータはもっとあるのですが(サーバ側の負荷とか)、うまくすればCIに組み込めるかも…という夢が見れます。

その他

*1:リクエストごとにヘッダを変える必要がありました