Septeni Engineer's Blog

セプテーニ・オリジナルのエンジニアが綴る技術ブログ

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を行ってきました。テストデータの管理に困っていたら試してみてはいかがでしょうか。

【書籍】Java並行プログラミング「第6章 タスクの実行」を読みまして - 前編

中途三年目、堀越です。

突然ではありますがわたくし、並行プログラミングについて学習しております。その活動の一部として絶版になっている「Java並行プログラミング」読んでいる最中であります。

www.amazon.co.jp

普段 Scala を書くことが多いわたしにとっては6章がとても親近感のある内容でしたので、前・後編に分けて紹介したいと思います(一回でうまくまとめきれなかった)。

続きを読む

危険!AIに奪われる遊び 〜本当にゲームはヤバいのか検証する〜

こんにちは。エンジニアの菅野です。

みなさんはAIと聞いてどういうものを思い浮かべますか?
あまり自分とは関わりが無いなと思う方もいるかも知れません。

でも写真をいい感じに補正する写真アプリや部屋をいい感じの状態にしてくれるエアコン等があり、知らず知らずの間に既にお世話になっていたりするものです。
仕事においてもExcelにはいい感じにグラフを作る機能があったりして、確実にAIは活用できる段階になっていると思います。

もしAIが人の仕事を奪ってやってくれるなら、遊ぶのもAIに任せたいと思いませんか?

思いますよね!
やってみましょう。

ゲームをAIにやらせて時短

ちょうどSteamでやらないまま積み上げてるゲームがいくつかあるので、そいつをAIにやらせればプレイする手間が省ける気がしたので早速AIにゲームをやらせようと思います。

ゲーム×機械学習だと強化学習でやるのが一般的です。
多数の試行錯誤を重ねてマリオをクリアできるようになったり、囲碁のAlphaGoも強化学習で人間を上回る強さになっています。

でも今回は雑にゲームのプレイ画面を見せて、それを模倣して同じようにプレイしてくれたらなと思うので、単純な教師あり学習で実験してみたいと思います。
強化学習だとスコアリングのために各種パラメータを取得しなきゃいけないし(市販のゲームのメモリ解析は無理)、多数の試行をするために並列化必須(たくさんゲーミングPC買わないと)なので…

積んであるゲームの中からAIにやってもらうゲームは…

F1 2018です。

早速プレイしてもらいましょう。

続きを読む

The Elm Architectureの構成とデータフロー

こんにちは、丸山です。

前回フロントエンドの状態管理を考える一つの方法としてFluxを見てみました。 今回はThe Elm Architectureについて見ていきたいと思います。

The Elm Architectureとは

公式ガイドによればThe Elm Architectureは関数型プログラミング言語であるElmでアプリケーションを構築する際に使われるパターンで、 モジュール性やコードの再利用性、テストのしやすさなどに優れているということです。 Fluxの時と同じように、まずThe Elm Architectureの要素を見ていきます。

Model

アプリケーションの状態

Update

状態を更新する方法

View

HTMLとして状態を閲覧する方法

The Elm Architectureは以上の3つの要素から構成されます。
次に実際にコードを見て、これらがどう使われているのかを見ていきます。

続きを読む

Fluxの構成とデータフロー

こんにちは、丸山です。
最近業務でVue.jsの状態管理ライブラリであるVuexを触る機会が多くありました。
VuexはFlux、Redux、The Elm Architectureの影響を受けています。
これらを理解していくことはVue.jsのみならずフロントエンドにおける状態管理を考える助けになるのではないかと思います。
そこで今回はまずFluxの考え方をまとめてみました。

Fluxとは

FluxはFacebookが提唱している状態管理パターンです。 以下の図はFluxのデータフローを表しています。

f:id:to_maruyama:20190319183205p:plain

Fluxの特徴はデータフローが単方向という点です。 常にデータが同じ流れで処理されるので、管理しやすく不整合が起きにくくなります。

Fluxには主に4つの要素があります。それを以降で説明します。

続きを読む