40歳からのキャリアチェンジ

20代はエンジニア・PM、30代はWeb系エンジニア向けのキャリアアドバイザー。40代の今はフリーランスで開発含めて色々やってます。技術ネタとしてはRuby/RailsとJavaScript関連あたり

Ti.Network.HTTPClientを少し機能拡張しながらjasmineの使い方について学ぶ-part2

前回のpart1の記事では、自分自身の作業環境を紹介しつつ、httpCLientのタイムアウトの設定に関するテストケースを書きました。今回はHTTPClientとしてのメイン機能になる通信部分の処理についてとりあげていこうと思います

まずはベタに実装する

【13日目】イベント機構&リトライ機構を組み込んだHTTPClient (Titanium Mobile)というエントリではaddEventListenerでイベントを登録&発火出来る仕組みを取り入れてます。

その機能を取り入れた上で、httpClientの通信機能も実装して・・となると解説する分量が多くなるので、今回はイベント登録機能は外して、ベタな実装だけしておくことにします

実装後はこんな感じで利用できることをイメージしてます

httpClient._request('GET','https://qiita.com/api/v1/tags',(data)->
  Ti.API.info data
)

jasmineで非同期通信のテストケース書く時の注意点

Titanium+jasmineでの非同期通信のテストの書き方が意外と難しかったのでハマりどころと対処方法についてまとめてみましたというエントリを以前書きましたが、jasmineでの非同期通信のテストの書き方はちょっとわかりづらいかなと個人的には感じてます。

前回のエントリでも書いたのですがjasmineにはwaitsFor()とruns()というものがあるので、言葉の印象からして、

  • waitsFor()の中で非同期通信の処理を実施
  • 非同期通信の処理が完了した段階で、runs()が実行
  • runs()内のexpect()が呼ばれてテストが評価される

かと思っていたら、残念ながらそうではありません。

そのため、Jasmine.Async: Making Asynchronous Testing With Jasmine Suck Lessというサイト内で紹介されているjasmine.async.jsをダウンロードして、以下のようなディレクトリ構成にした上で利用してます

├── LICENSE
├── README.md
├── Resources
├── build
│   ├── xxxx
├── Resources
│   ├── app.js
│   ├── lib
│   │   ├── jasmine-1.3.1.js
│   │   ├── jasmine-titanium-console.js
│   │   ├── jasmine-titanium.js
│   │   ├── jasmine.async.min.js
│   └── test
│       ├── httpClient.js
│       └── tests.js
├── coffee
│   ├── app.coffee
│   ├── httpClient.coffee
│   └── test
│       ├── httpClient.coffee
│       └── tests.coffee
├── manifest
└── tiapp.xml

jasmineで非同期通信のテストケースを書く

ここ最近、Qiitaのビューワーアプリ作っていたので、QiitaAPIにアクセスして、タグの情報を取得する処理を例にして、具体的なテストケースの書き方について説明していきます

先頭から4行目まで

前回説明を省いていた所があるので、最初にそこについて説明しておきます。

describe 'httpClient', ->
  beforeEach ->
    httpClient = require('httpClient')
    @client = new httpClient()

2行目のbeforeEachですがjasmineの公式ドキュメントには

Setup and Teardown

To help a test suite DRY up any duplicated setup and teardown code, Jasmine provides the global beforeEach and afterEach functions. As the name implies the beforeEach function is called once before each spec in the describe is run and the afterEach function is called once after each spec.

という記述があります。

関連しそうなところだけ意訳すると、describe句内部のテストケース(原文だとspecという箇所)が実行される前に、beforeEach functionが一度だけ呼ばれるということで、インスタンス化する処理をここに書いておくことで例えば

describe 'httpClient', ->
  beforeEach ->
    httpClient = require('httpClient')
    @client = new httpClient()
  it 'should be xxx', ->
    result = @client.xxx
    expect(result).toXX
    
  it 'should not be xxx', ->
    result = @client.anotherMethod()
    expect(result).toXX    

このようにインスタンス化した@clientを使いまわしすることができます。

4行目以降で今回キモとなる部分のコード

  #   
  # 〜中略〜
  #
  
  describe 'HTTPClient main function', ->
    contents = null
    async = new AsyncSpec(@)
    async.beforeEach (done) =>
      runs ->
        @client._request('GET','https://qiita.com/api/v1/tags',(data)->
          contents = data
          done()
        )

    it 'get a tags from Qiita API', ->
      expect(contents.length).toBe 20

QiitaAPIにアクセスして、タグの情報を取得した結果を格納しておく変数(contents)を宣言しています

その後のasync.beforeEach()内の処理ですが、@client._request()を実行して処理が完了した段階でdone()が呼ばれます。

※たぶん↓こういうサンプルの方が理解しやすいかもしれませんね

async.beforeEach (done) ->
  runs ->
    setTimeout(->
      alert('test')
      done()
    ),10

async.beforeEach内の処理が実行されることで、変数 contents 内にQiitaAPIからJSON形式で以下のような値が得られます

[{"name":"Qiita","url_name":"Qiita","icon_url":"http://qiita.com/system/tags/icons/000/000/001/medium/favicon.png?1320171109","follower_count":3756,"item_count":212},{//以下略}]
it 'get a tags from Qiita API', ->
  expect(contents.length).toBe 20

を評価できることになります

最後にソースコード全体

httpClient.coffeeの最終形

class httpClient 
  constructor: (args) ->
    args = args or {}
    # HTTPClientのタイムアウト(ミリ秒)
    @httpTimeout = args.httpTimeout or 5000
    # リトライ回数
    @retryCount = args.retryCount or 2
    # リトライまでの待ち時間(ミリ秒)
    @retryWaitTime = args.retryWaitTime or 1000
    # 現在のリトライ回数
    @currentRetryCount = 0
    # リトライ用に保存しておくHttpClient用パラメータ
    @saveMethod = ""
    @saveUrl = ""
    @saveData = null
    @events = null
  _request:(method,url,callback) ->
    xhr = Ti.Network.createHTTPClient()
    xhr.open(method,url)
    xhr.onload = ->
      callback(@.responseText)
        
    xhr.onerror = (e) =>
      Ti.API.info "status code: #{@status}"
      error = JSON.parse(@.responseText)
      
    xhr.send()    
module.exports = httpClient  

test/httpClient.coffeeの最終形

describe 'httpClient', ->
  beforeEach ->
    httpClient = require('httpClient')
    @client = new httpClient()

  it 'should be object', ->
    expect(typeof @client).toBe "object"
    
  it 'has Timeout seconds', ->
    expect(@client.httpTimeout).toBe 5000

  describe 'HTTPClient main function', ->
    contents = null
    async = new AsyncSpec(@)
    async.beforeEach (done) =>
      runs ->
        @client._request('GET','https://qiita.com/api/v1/tags',(data)->
          contents = data
          done()
        )

    it 'get a tags from Qiita API', ->
      expect(contents.length).toBe 20