FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

jsverifyでjavascriptのProperty Based Testingをやってみる

こんにちは、丸山です。
最近フロントエンドのテストデータの管理にコストがかかるという問題がチームで出ました。テストデータを手動で用意すると、データの記述量が多くなり、また仕様が変わりテストを直すことになると、今度はそのテストデータを一つ一つ直さなくてはいけなくなり、これにも大きな手間がかかってしまいます。

そこでフロントエンドのテストにProperty Based Testingを取り入れようという話が上がりました。

Property Based Testingはテストデータをランダムに生成することで、その手間を減らします。テストデータを変えなければならなくなったら、データを生成しているところだけを変えればよくなります。

今回はチームでも取り入れつつあるjavascript用のProperty Based Testingライブラリである、jsverifyを使ってのProperty Based Testingをやってみたいと思います。

jsverifyを使ったテスト

では早速jsverifyを使ってテストを実行してみます。 ちなみにjsverifyはいくつかのjavascriptのテストフレームワークに対応していますが、今回はjasmineを使っていきます。

以下が今回のテスト対象の関数です。

function add (a, b) { return a + b }

引数として渡された二つの値を足し合わせるだけの単純な関数です。 この関数をテストしていきます。

こちらがテスト内容です。

describe('Calculation', () => {
  it('add', () => {
    expect(jsc.check(
      jsc.forall(jsc.nat, jsc.nat, (a, b) => add(a, b) === b + a))
    ).toBeTruthy()
  })
})

forallという関数にjsc.natというarbitraryと、テスト内容を書いた関数を引数として渡しています。 さらにそれをcheck関数に渡して、テスト結果を検証しています。(arbitraryについては後で説明します)

これを実行すると以下のような結果が得られます。

Randomized with seed 07108
Started
OK, passed 100 tests

これでランダムなテストデータ(ここでは二つの数値)を使って、100回テストを実行することができました。

jsverifyはデフォルトでは100回テストを実行するようになっていますが、これはcheck関数にtestsでテスト回数を渡すことで変更することができます。

describe('Calculation', () => {
  it('add', () => {
    expect(jsc.check(
      jsc.forall(jsc.nat, jsc.nat, (a, b) => add(a, b) === b + a),
      {tests: 5} // テスト回数を5回に設定
    )).toBeTruthy()
  })
})
Randomized with seed 01978
Started
.....OK, passed 5 tests

arbitrary

先ほど書いたテストでforallにjsc.natという引数を渡していました。これはarbitraryというオブジェクトの一つで、自然数の値がテストで必要なときに使うものです。 jsverifyにはboolやstring、arrayなどいろいろなarbitraryが用意されています。 arbitraryはgeneratorshrinkshowの3つの関数を持っています。

generator

任意の型の値を生成する関数です。

shrink

ある型aの小さい値の配列を返す関数です。 これはテストが失敗した場合、段階的に小さい値で試していって、失敗した原因がわかるようにするときに使用されます。

show

失敗した値を文字列に変換する関数です。テスト結果を表示するときに使われます。

失敗するケースのテスト

上記の関数が使われることを確認するため、失敗するテストケースにconsole.logを入れてを実行してみます。

describe('Comparison', () => {
  it('Bigger than 10', () => {
    expect(jsc.check(
      jsc.forall(jsc.nat, jsc.nat, (a,b) => {
        console.log(a + ', ' + b)
        add(a, b) > 0
      }),
      {tests: 5} // テスト回数を5回に設定
    )).toBeTruthy()
  })
})
Randomized with seed 23879
Started
.....13, 5
6, 5
3, 5
1, 5
0, 5
0, 2
0, 1
0, 0
Failed after 1 tests and 7 shrinks. rngState: 00b51963caf0782e4d; Counterexample: 0; 0;  [ 0, 0 ]

上記の実行結果にあるように、値がランダムに生成され、さらに失敗したため何度かのshrinkが実行されて最終的に0; 0; というCounterexample(反例)が表示されました。

クラスのテスト

先ほどテストを実行するときにはforallarbitraryを渡すと言いました。jsverifyにはいろいろなarbitraryがありますが、自分で定義したclassのarbitraryはもちろんありません。 というわけで次は自分で定義したclassのテストをやってみます。 (この辺りはドキュメント等に書いてなかったので自分で考えたやり方になります。他にも方法はあるかもしれません。)

今回使うclassとして以下を用意します。

module.exports = class User {
  constructor(id, firstName, lastName) {
    this.id = id  
    this.firstName = firstName
    this.lastName = lastName
  }
  
  fullName () {
      return this.lastName + ' ' + this.firstName
  }

}

smap

jsverifyにはsmapという関数があります。

.smap(f: a -> b, g: b -> a, newShow: (b -> string)?): arbitrary b

これは型aのarbitraryを型bのarbitraryに変換する関数です。これを使ってUserクラスのarbitraryを作ってみました。

const userArb = jsc.record({
    id: jsc.nat,
    firstName: jsc.asciinestring,
    lastName: jsc.asciinestring
  }).smap(
    record => { return new User(record.id, record.firstName, record.lastName) },
    user => { return {id: user.id, firstName: user.firstName, lastName: user.lastName} }
  )

exports.userArb = userArb

まずjsc.recordでオブジェクトのarbitraryを作り、その後smapを使ってUserのarbitraryに変換しています。
smapには引数として、a型の値をb型に変換する関数とb型をa型に変換する関数を渡すようになっているので、それらの関数を記述しています。(この他にもsmapにはオプションの引数としてb型をstringに変換する関数がありますが今回は省略しています)

それではこのUserクラスのarbitraryを使ってテストを実行します。

describe('User', () => {
  it('addition is commutative', () => {
    expect(jsc.check(
      jsc.forall(userArb.userArb, (user) => {
      console.log(user)
      return user.fullName().includes(user.firstName) && user.fullName().includes(user.lastName)
    })
    )).toBeTruthy()
  })
})
Randomized with seed 88356
Started
.....User { id: 46, firstName: '}c', lastName: 'Z<R' }
User { id: 2, firstName: 'p', lastName: '"X' }
User { id: 16, firstName: ')n?', lastName: '6-&' }
User { id: 5, firstName: 'K', lastName: 'C' }
User { id: 3, firstName: 'd"BbL', lastName: '4#&Z' }
OK, passed 5 tests

Userクラスのテストを実行することができました。

以上、jsverifyを使ってjavascriptのProperty Based Testingを行ってきました。テストデータの管理に困っていたら試してみてはいかがでしょうか。