Skip to content

Instantly share code, notes, and snippets.

@tkob
Last active October 1, 2016 11:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tkob/0ee5f804149265615334bc52504c8a30 to your computer and use it in GitHub Desktop.
Save tkob/0ee5f804149265615334bc52504c8a30 to your computer and use it in GitHub Desktop.
単体テストのための外部DSL (第十七回 #渋谷java)

単体テストのための外部DSL

tkob

JUnit 4 の assertThat の典型的な説明

assertThat(account.getBalance(), equalTo(100));

マッチャーを使った文は英語に近い形で左から右へと自然に読み下せます。つまりこの例は「assert that the account balance is equal to 100(「口座の残高が100に等しいことを確認せよ」)」と読めます。

実践JUnit

assertThat(actual, is(expected));

(前略) JUnit 4のスタイルではMatcher APIを活用することで、検証コードが自然言語の表記に近く、「assert that actual is expected」(実測値が期待値であると表明する)と読むことができます。

JUnit実践入門

そう?

  • assertとThatの間にスペースがないのはいいの?
  • Tが大文字なのは?
  • 何故そこが括弧でくくられているの?
  • isの前にカンマがあるのは?
  • ノイズが多い

本当はこう書きたかったのでは?

Assert that actual is expected.

何故そう書けないか

  • 内部DSLだから Java 文法の制約を受ける
  • 単体テストのための外部DSLを作ろう

YokohamaUnit

https://github.com/tkob/yokohamaunit/

  • 単体テストのための独自の文法を持った言語・テスティングフレームワーク
  • ソースから直接(javacを経由せず)バイトコードを生成する
    • スタックトレースに適切な行番号を残すため
  • JUnit のテストランナーで実行される
    • エコシステムにのっかる
                              transformation
+---------+         +----------+  +----+  +----+
|Test Code|-parse-->|Parse Tree|->|AST1|->|AST2|
+---------+ (ANTLR) +----------+  +----|  +----+
                                            | code generation
                                            v (Apache BCEL)
                                        +---------+
              test execution (JUnit) <--|Byte Code|
                                        +---------+

言語の設計

テストのためのDSLには「テストを記述するDSLに属する部分」と「テスト対象に属する部分」がある

Assert that `actual' is `expected'.
  • Assert, that, is, . → テスト記述に属する
  • actual, expected → テスト対象に属する

テスト対象に属する部分はDSLにしない

  • account がテスト対象であるときに account.getBalance() を対象言語から遠い仕方で表現してもしょうがない

例: 簡単なアサーション

# Test: This is my first test
 
Assert that `Integer.valueOf("123")` is 123.
  • バッククォートの中は Groovy の式であり、実行時に評価される

例: 例外のテスト

例外の送出

Assert that `StringUtils.toSnakeCase(null)` throws 
an instance of `NullPointerException`.

例外を送出しない

Assert that `Integer.valueOf("123")` throws nothing.

例: パラメタ化テスト

# Test: Test cases for `toSnakeCase`
 
Assert that `StringUtils.toSnakeCase(input)` is `expected`
for all input and expected in Table [1].
 
| input           | expected          |
| --------------- | ----------------- |
| ""              | ""                |
| "aaa"           | "aaa"             |
| "HelloWorld"    | "hello_world"     |
| "practiceJunit" | "practice_junit"  |
| "practiceJUnit" | "practice_j_unit" |
| "hello_world"   | "hello_world"     |
[1]
  • 各行に対してテストメソッドを生成する(各1ケースとカウントされる)

例: 組み合わせテスト

# Test: Combination in where-clause

Assert that `a * x * y` is 0
where a is 0
  and x is any of 1, 2 or 3
  and y is any of 4, 5, 6.
  • これもそれぞれに対してテストメソッドを生成する
  • where は組み合わせテストでないときも変数束縛に使える
  • Pairwise の組み合わせにするオプションも用意した(未リリース)

4フェーズテスト

# Test: AtomicInteger.incrementAndGet increments the content

## Setup
Let i be `new AtomicInteger(0)`.
 
## Exercise
Do `i.incrementAndGet()`.
 
## Verify
Assert `i.get()` is 1.
  • Teardown もある

例: 複数行リテラル

# Test: Interpret anchor expression 

Assert `"cheer\n".multiply(3).denormalize()` is [Three cheers]. 

### Three cheers 

``` 
cheer 
cheer 
cheer 
```    

例: 正規表現リテラル

# Test: Regular Expression

Assert that `"hello"` matches re `^h.*o$`.

Assert that `"hello"` matches regex `\Ah.*o\z`.

Assert that `" hello "` matches regexp `h.*o`.

Assert that `" hello "` does not match regexp `\Ah.*o\z`.

例: リソース式

# Test: resource without "as" is a URL

Assert `url` is an instance of `URL`
 where url = resource "blahblah".

# Test: resource as InputStream

Assert `instream` is an instance of `InputStream`
 where instream = resource "blahblah" as `InputStream`.
  • ほかにも as `File` , as `URI` が使える

一時ファイル式

# Test: create a temp file

## Setup

Let temp = a temp file.

## Verify

Assert `temp.exists()` is true.
  • 自動的に deleteOnExit が呼ばれる

スタブ式

# Test: Collections.unmodifiableMap preserves lookup
 
Assert `unmodifiableMap.get("answer")` is 42

 where map is a stub of `Map` such that
              method `get(java.lang.Object)` returns 42
                        
   and unmodifiableMap is `Collections.unmodifiableMap(map)`.
  • Mockito を使うようなバイトコードにコンパイルされる

作った感想

  • テストのために外部DSLを用いると良いこと (内部DSLではできない)
    • ノイズのないDSL
    • 自由にリテラルを作れる
    • ホスト言語の構造に左右されない文法(e.g.例外)
  • たぶんテストの全ての側面が「自然に読み下せ」る必要はないかも
    • 最初のモチベーションの一部を否定
    • モックを自然言語っぽく書いてもすっきりしない
      • 本当はどう書けると嬉しいのか?
    • 例えば表は自然言語ではないが読みやすい
  • 式言語(Groovy)を動的に評価させることの限界
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment