JavaでHTTPアクセスを記録/再生してテスト時に使える、Betamaxを試してみた

id:ikikko さんのこちらの記事を参考にJavaでHTTPアクセスを記録/再生してテスト時に使える、Betamaxを試してみました。

このBetamaxというのはその名の通り、テープに記録/再生するものなわけですが、その対象が映像ではなくHTTPアクセスだと言うことです。

 

Rubyvcrという同様のツールがあるのですが、こちらのクローンとなります。Betamxを使ったテストのメリットとして

  • ネットに繋げる必要がない
  • 状態を固定できる(検索結果などアクセス毎に変わるものに有効)
  • レアケースのテストに対応できる
  • モックを用意する必要がない

 

というのがあげられます。モックを用意しなくて良いので、テスト全体もシンプルになりますし、WebAPIを始め外部のリソースを叩くときにはかなり汎用的に活用できそうです。

 

基本的な仕組みはJettyベースのProxyを立ち上げて、Javaの標準Proxy設定を変更する作りのようです。

HttpUnitなど独自のProxyを利用するコードには明示的にProxyを指定してやる必要があります。

 

Groovyで記述されたライブラリですが、ピュアJavaからでも問題なく利用出来ました。おそらくScalaとかからの利用も可能でしょう。

 

では、使い方について下記に書いていきます。

pom.xmlの変更

<dependency>
    <groupId>co.freeside</groupId>
    <artifactId>betamax</artifactId>
    <version>1.1.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>2.0.4</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.2.1</version>
    <scope>test</scope>
</dependency>

ここで重要なのはgroovy-allとhttpclientを追加するのを忘れないことです。

このあたりが無いと下記のようなエラーがでるので要注意です。

 

httpclirntが無いとき:

groovy.lang.GroovyRuntimeException: Could not find matching constructor for: org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager()
    at groovy.lang.MetaClassImpl.invokeConstructor(MetaClassImpl.java:1477)
	at groovy.lang.MetaClassImpl.invokeConstructor(MetaClassImpl.java:1393)
	at org.codehaus.groovy.runtime.callsite.MetaClassConstructorSite.callConstructor(MetaClassConstructorSite.java:46)
	at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallConstructor(CallSiteArray.java:57)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callConstructor(AbstractCallSite.java:182)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callConstructor(AbstractCallSite.java:186)
	at co.freeside.betamax.proxy.jetty.ProxyServer.newHttpClient(ProxyServer.groovy:70)
	at co.freeside.betamax.proxy.jetty.ProxyServer.this$4$newHttpClient(ProxyServer.groovy)
	at co.freeside.betamax.proxy.jetty.ProxyServer$this$4$newHttpClient.callCurrent(Unknown Source)
	at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallCurrent(CallSiteArray.java:49)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:133)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:141)
	at co.freeside.betamax.proxy.jetty.ProxyServer.start(ProxyServer.groovy:52)
	at co.freeside.betamax.HttpInterceptor$start.call(Unknown Source)
	at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:45)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:108)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:112)

 

groovy-allが無いとき:

java.lang.NoClassDefFoundError: groovy/lang/GroovyObject
    at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:634)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:277)
	at java.net.URLClassLoader.access$000(URLClassLoader.java:73)

Javaだけ使ってると特にgroovy-allなんて入ってないと思うので、そこでハマるかもです。

テスト以外には不要なのでscopeはtestにしておきます。

 

テストの記述

Betamaxを使うときのポイントは3点です。

  • @RuleにRecorderを設定する
  • 各テストケースに@Betamaxを指定する
  • 必要に応じてresources/betamax.propertiesを編集する

 

まずは下記の通り@RuleにRecorderのフィールドを宣言します。この値はpublicでなければいけません。

@Rule public Recorder recorder = new Recorder();

次に各テストに対して@Betamaxアノテーションを追記します。

 

tapeで記載された値がHTTPリクエストの記録名となります。

@Betamax(tape = "my tape")
@Test
public void testSimpleHttp1() throws Exception {
    // URL接続
    HttpURLConnection con = (HttpURLConnection) new URL("http://www.google.com/").openConnection();
    con.setRequestMethod("GET");
    BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()));

    // HTMLソースの表示
    String str;
    while *1 != null) {
        System.out.println(str);
    }

    // URL切断
    br.close();
    con.disconnect();
}

この状態で実行すると、まず初回アクセスは通常通りネットにアクセスを行います。

その時のネットワークの記録が

src/test/resources/betamax/tapes/記録名.yml

として出力されます。これ以降は実行の度にこのファイルを参照して、ネットワーク通信無しのダミーアクセスとなります。

 

なお、ファイルの中身は以下のようなフォーマットです。

!tape
name: my tape
interactions:
- recorded: 2012-11-25T23:29:55.459Z
  request:
    method: GET
    uri: http://www.google.com/
    headers:
      Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
      Host: www.google.com
      Proxy-Connection: keep-alive
      User-Agent: Java/1.6.0_24
  response:
    status: 200
    headers:
      Cache-Control: private, max-age=0
      Content-Type: text/html; charset=Shift_JIS
      Date: Sun, 25 Nov 2012 23:29:55 GMT
      Expires: '-1'
      P3P: CP="This is not a P3P policy! See http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657 for more info."
      Server: gws
      Set-Cookie: PREF=ID=fa5743e33aa1e0a8:FF=0:TM=1353886195:LM=1353886195:S=my_Hw0OYJp3nuD-z; expires=Tue, 25-Nov-2014 23:29:55 GMT; path=/; domain=.google.co.jp, NID=66=IijFeNtgPs0FQFBqIU7fwPxJB7ZFoUeRf5hdMExgaARPsU9M_VAzI1EwJyjr44MYEJzVZbZQ1G1Vyx6d_WjqlYJ6TImA1uxuTLz8CgKztwizzjjChom9d-_9Qr-LfMag; expires=Mon, 27-May-2013 23:29:55 GMT; path=/; domain=.google.co.jp; HttpOnly
      X-Frame-Options: SAMEORIGIN
      X-XSS-Protection: 1; mode=block
    body: |-
      <html>Hello World.</html>

こちら、ファイルフォーマットを直接変更することで任意の内容に変えることができるので、再現が難しい内容に関しては、直接テープを用意してしまうのが良いと思います。

 

またデフォルトポートが5555なのですが、こちらが既に利用されていてる場合は当然ですが、下記のような例外が出ます。

java.net.BindException: Address already in use

その場合はsrc/test/resources/betamax.propertiesを作成して設定内容を変更するのが有効です。私の環境の場合5555はぶつかってしまったので、以下の設定ファイルを作成して6555に変更してあります。

betamax.tapeRoot=src/test/resources/betamax/tapes
betamax.useProxy=true
betamax.proxyPort=6555
betamax.proxyTimeout=5000
betamax.defaultMode=READ_WRITE
betamax.ignoreHosts=localhost,127.0.0.1
betamax.ignoreLocalhost=false
betamax.sslSupport=true

こちらを読み込むことで無事、テストが動作します。

また、HTTPSアクセスを有効にするためには

betamax.sslSupport=true

を設定する必要があります。デフォルト設定ではfalseなので、下記のようなエラーが発生します。

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)

HTTPSを利用する場合は忘れずに変更するようにしましょう。

 

まとめ

さて、これで基本的なBetamaxの使い方は説明完了です。

まだ、自分も使い始めたばかりですが、開発中のAndroidアプリがちょうど外部アクセスをするのでテストに組み込んで見ました。ファイルはこちら。まだ、1ケースしか書いてないのであまり意味無いですがw

 

仕事でAPI叩いてることも良くあるので、その辺にも随時適用していきたいと思います。いまだと、ネットワーク不通でテスト通らないとか結構あるので...

 

ではHappy Hacking!

 

参考:

*1:str = br.readLine(