脱エクセル管理!?テストケース設計とテストケース自動生成ツールKTestCaseDSLを作りました。 (コアコンセプト編)

テストケース設計者、テストケースレビュー、テストケース実施者にやさしいDSLをつくるにはどうしたらよいのか考え、プロトタイプを作りました。
2021.10.25

まずモノをみてもらう

テストケース設計を記述するDSLをKotlinで作りました。

KTestCaseDSL

以下のようなコードでテストケース設計ができます。

サンプルはこちら

testSuite("ログイン画面") {
        case("ログインできること") {
            preCondition {
                condition("未ログイン状態であること")
            }
            step("https://xxxxx.com にアクセスする")
            step("ユーザ名を「Kamedon」と入力する")
            step("パスワードを「Password」と入力する")
            step("ログインをクリックする")
            verify("「ログインしました」とトースト通知されること")
            postCondition {
                condition("ログイン済み状態であること")
            }
        }
        case("入力が不正のときはログインができないこと") {
            preCondition {
                condition("未ログイン状態であること")
            }
            step("https://xxxxx.com にアクセスする")
            step("ユーザ名を「Kamedon」と入力する")
            step("ログインをクリックする")
            verify("「パスワードが未入力です」とトースト通知されること")
            postCondition {
                condition("未ログイン状態であること")
            }
        }
}

テストケース設計とテストケース生成(レンダリング)は別にしているので、任意のフォーマットで出力できるようにしています。 Mardkdown形式で出すことも可能です。以下自動生成のサンプルです。

このまま GitHub IssueやNotionなどに貼り付けても良い感じです。

# ログイン画面
## ログインできること
### 事前条件
- 未ログイン状態であること

### テスト手順
1. https://xxxxx.com にアクセスする

2. ユーザ名を「Kamedon」と入力する

3. パスワードを「Password」と入力する

4. ログインをクリックする
### 期待結果
- [ ] 「ログインしました」とトースト通知されること
### 事後条件
- ログイン済み状態であること

## 入力が不正のときはログインができないこと
### 事前条件
- 未ログイン状態であること

### テスト手順
1. https://xxxxx.com にアクセスする

2. ユーザ名を「Kamedon」と入力する

3. ログインをクリックする
### 期待結果
- [ ] 「パスワードが未入力です」とトースト通知されること
### 事後条件
- 未ログイン状態であること

コアコンセプト

モチベーション

ユニットテストやE2Eツールなどで自動テストに寄せてはいるんですが、自動でやりにくい定性的な部分は、手動テストすることがありました。

意外と(手動)テストケースの管理ツールで満足するものがなかった。

色々触ってみたのですが、テスト管理系ツールはテスト実行の管理は良かったけど、テストケース作成の部分ではまぁまぁ不満がありました。

テストケースレビューがやりづらい問題

GUIベースでフォームでポチポチ、カキカキするタイプだと、初めてのときは全部読んでレビューできるんですが、レビューが入って指摘されたときにど修正箇所を全部探さないと行けない。差分でのレビューがやりづらい。

ツールによっては、事前条件とかをまとめて、あとでテストケースに紐付けるみたいな共通化ができるといえばできるのだけど、結局全体でどこが変わったがわからないので、探す手間が発生してました。

テストケース修正がモレモレ問題

テストケースを修正する場合、たとえば表記ゆれを直したい時一括で変換できないし、まずその箇所を検索するのも大変でした。 同じワードだったとしても、文脈よっては正しいことがあるので、直してはいけないケースもあります。

つまり、同じ文脈で使ってるワードを直したいときに、過剰に直してします、あるいは修正が漏れてしまうことがありました。

また、抜けたいたテスト手順を足すときも足し忘れていたりすることもあって、対応箇所すべて直すのも大変でした。

僕が欲しかったもの

xUnitやe2eテストの自動テストスクリプトはとてもわかりやすく、直感的だなぁっと思っていました。

例: - CodeceptJSのスクリプト例

Feature('Search');

Scenario('Google検索でCodeceptJSを検索する。', ({ I }) => {

    I.amOnPage('https://www.google.co.jp/')

    I.fillField('検索', 'CodeceptJS')

    I.click('Google 検索')

});

xUnitやe2eで、実際にテストを回さず、そのままテストケースとして吐き出してくれたらよいのに。

いろいろ探してみたんですが、見つけられなかった。

なかったので作りました。(良いツール知ってる方いたら教えてください)

テストケース設計/実施のドメイン分析

他のテスト管理ツールの問題点は、たぶんテストケース設計者とテスト実施者両方の都合をまぜこぜにして管理してるからだと思いました。

テスト設計/レビューの関心事は、テストケース構造である。具体的なテストケースには興味はない。なぜなら個々の具体的なテストケースは構造から自動生成できるものだから。

テスト実施者の関心事は、テスト手順と確認事項である。テストケース構造はどうでもよい。

ちょっと例をだそう。検索画面のフォームをテストすることを考える。

const keywords = [
    {'keyword': 'CodeceptJS'},
    {'keyword': 'CodeceptJS e2e'},
]

Feature('Searchs');

Data(keywords).Scenario('Google検索でいろんなkeywordを検索する。', ({ I, current }) => {

    // https://www.google.co.jp/ を開く。
    I.amOnPage('https://www.google.co.jp/')

    // 検索窓にkeywordを入力する。
    I.fillField('検索', current['keyword'])

    // 「Google 検索」ボタンをクリックする。
    I.click('Google 検索')

});

テストケース設計としては、 これで十分なんですが、テスト実施者は実際にたたみこまれた状態じゃないと実施にしづらい。CodeceptJSで検索してOKだったら、次はCodeceptJS e2eで検索して、、っと頭中のメモリを使う必要がある。 つまりテストケース実施者とってはこっちのほうがわかりやすい。

Feature('Searchs');

Scenario('Google検索でCodeceptJSを検索する。', ({ I }) => {

    // https://www.google.co.jp/ を開く。
    I.amOnPage('https://www.google.co.jp/')

    // 検索窓にCodeceptJSを入力する。
    I.fillField('検索', 'CodeceptJS')

    // 「Google 検索」ボタンをクリックする。
    I.click('Google 検索')

});

Scenario('Google検索でCodeceptJS e2eを検索する。', ({ I }) => {

    // https://www.google.co.jp/ を開く。
    I.amOnPage('https://www.google.co.jp/')

    // 検索窓にCodeceptJS e2eを入力する。
    I.fillField('検索', 'CodeceptJS e2e' )

    // 「Google 検索」ボタンをクリックする。
    I.click('Google 検索')

});

この形式で書いちゃうと、追加でYahooでも検索するに変更するときに、抜け漏れを修正するのが大変だし、一括置換しようとすると、必要なGoogle検索のテストケースまで消してしまう。

テストケースの修正は、具体的なテストケースを修正するのではなく、テストケースの構造の段階で修正したほうがよく、修正後に再度自動生成すればよいだけなのである。

まとめ。

  • テストケース設計/レビューは構造だけを記述したい。
  • テスト実施者は具体的にテストケースだけを知りたい。

テストケースの構造分析

ではテストケース構造とはなにか。

他のテスト管理ツールのテストケースを追加をみると3つの観点がまざりあった状態になってると思う。(JSTQBとかでもっとちゃんとした定義があるんだろうけど)

こちらはQaseのテストケース作成画面

一部抜粋するが以下のような項目を設定する

タイトル、ステータス、概要、重要度、マイルストーン、事前条件、事後条件/期待結果、手順

これらの項目は以下の3つ観点にわけることができると思う。

  • テストケース構造
  • テストケース実施の項目
  • テストケース分析/レポートで必要な項目

これらをいい感じに管理したいというのが今回のお題です。

テストケース構造

テストケースとして実行するために必要な項目。

テストケースの説明/概要、事前条件、事後条件、手順、期待結果

これらがあれば最低限テストとしては実行できる(はず)

テストケース実施の項目

テストケース実施者が実施するために必要な項目、またその結果を記録するために必要な項目

テストケース構造 + ステータス(OK/NG) + 結果のレポート(エビデンスとか)

テストケース分析/レポートで必要な項目

今回必要なテストケースを選定したり、レポート用に必要な項目。本質的にテストケースを実施するために不要なもの

タグ、重要度、マイルストーン

Kotlinで実装していく

テストケースの構造を実装

テストケースをまとめたのがテストスイート テストケースの中に説明/概要、事前条件、事後条件、手順、期待結果がある。 テスト手順の中にも確認項目があることもある

これをKotlin化すると以下のようになります。

data class TestSuite(val title: String,val cases: List<TestCase>)
data class TestCase(
    val title: String,
    val preConditions: TestCaseConditions,
    val caseSteps: List<TestCaseStep>,
    val verifies: List<TestCaseVerify>,
    val postConditions: TestCaseConditions,
)
data class TestCaseStep(val title: String, val verifies: List<TestCaseVerify>)
data class TestCaseVerify(val title: String)

構造が正しければ書きやすいとは限らない

さきほどの実装は構造としては正しいが、テスト設計が読み書きがしやすい/レビューしやすいかというと微妙である。

以下は読み書きがしやすいだろうか?

val case = TestCase(
    "ログインできること",
    preConditions = TestCaseConditions(listOf(TestCaseCondition("未ログイン状態であること"))),
    postConditions = TestCaseConditions(listOf()),
    caseSteps = listOf(
        TestCaseStep("https://xxxxx.com にアクセスする", listOf()),
        TestCaseStep("ユーザ名を「Kamedon」と入力する", listOf()),
        TestCaseStep("パスワードを「Password」と入力する", listOf(TestCaseVerify("ログインボタンが活性化してること"))),
        TestCaseStep("ログインをクリックする", listOf()),
    ),
    verifies = listOf(TestCaseVerify("ログインしました」とトースト通知されること")),
)
val suite = TestSuite("ログイン画面", cases = listOf(case))

正しくてもこれで書きたい人という思う人はいないはず。あと読んでて、目がしばしばするよね。

読み書きがしにくい問題点は以下とおり

  • テストケース設計が本当に書きたいもの以外の言葉多い。
    • TestCaseStep、TestCase、TestSuiteなどの記述は、Kotlinの都合であってテストケース設計の都合ではない
  • 人間の思考と記述順序が不一致
    • 上の記述とテストケースを書き、次にテストスイートの順番で記述必要がある。なぜなら定義をするときにインスタンスが存在しないといけないから(Kotlinの都合)

実際にテストケース設計したときの書き味も重要だ。

テストケース作成は事務的な作業と思われがちだが、テストケースは設計はいわば本を書くが如く、クリエイティブな作業である。

人間が一発で完璧に抜け漏れなくテストケースなんて欠けないのである クリエイティブな作業は必ず推敲の工程がある。

推敲の工程のやりやすいかで書き味や使いやすさに直結してるといってもよいと思う

推敲に必要な要素

これは僕の個人的な感想になってしまうが、以下の項目をスピーディーに操作できると推敲がとてもやりやすい。

  • 構造/順番の入れ替えが簡単
  • 変更したときに、意味的に紐付けがある箇所は一括で変更される
  • ドメインの言葉となるべく近い言葉でかける

トップダウン的にテストケース設計してる最中に、または実際にテストケースを書いていて、抜け漏れや、表記ゆれや、前後関係がおかしいとかどんどん気づくことがあります。

一発で正しくかけないから、やりながら、思いつき、気付き、再構成、何度も修正するのです。

人間の思考とシステムの制約をつなぐDSLを作る

テストケース構造のまま、システムの制約のままだと、人間にとって使いにくい。

そこをつなぐのDSL(ドメイン固有言語)。システムのためではなくテストケース設計者が書きやすいだけの特化言語を作って、人間にもシステムにもやさしいシステムができる。

DSLで人間が書きやすいようにし、DSLからテストケース構造に変換してあげる。

またテストケース設計とテストケース作成(レンダリング)を意図的にわけることでDSLから任意のフォーマットでアウトプットが可能になる。

Excelでも、MardkdownでもHtmlでも、あとは煮るなり、焼くになり好きにすれば良い。

どのツールを使ってもテストケース構造は同じなのだから。

Kotlin DSLの実装

KotlinでDSLをつくる際に、必要な技術はすでに発表しているので、そちらを参照ください。

テストケースドメインの言葉だけのこして、無駄な言葉はなるべく排除しました。人間の思考しやすいように、階層構造のまま記述できるようにしました。

再掲。

testSuite("ログイン画面") {
        case("ログインできること") {
            preCondition {
                condition("未ログイン状態であること")
            }
            step("https://xxxxx.com にアクセスする")
            step("ユーザ名を「Kamedon」と入力する")
            step("パスワードを「Password」と入力する")
            step("ログインをクリックする")
            verify("「ログインしました」とトースト通知されること")
            postCondition {
                condition("ログイン済み状態であること")
            }
        }
        case("入力が不正のときはログインができないこと") {
            preCondition {
                condition("未ログイン状態であること")
            }
            step("https://xxxxx.com にアクセスする")
            step("ユーザ名を「Kamedon」と入力する")
            step("ログインをクリックする")
            verify("「パスワードが未入力です」とトースト通知されること")
            postCondition {
                condition("未ログイン状態であること")
            }
        }
}

Kotlin DSLを作って達成したこと

テストケース設計者/レビュー者

書き味

  • 書き味については、IDEの機能フルにつかえるので、みなさんがご存知の通り最高です。
  • テストケースの依存関係は、コードジャンプで検索など解決できる、普段されたエディタで置換や行入れなどもでき推敲もらくらく。
  • コード補完で何ができるか、何が設定できるのかもわかる

レビューのしやすさ

  • テストケースの構造自体をレビューできるようになりました。
  • GitHubなどで管理すればバージョン管理もできますし、各バージョンごとの差分も一発。

読みやすさ

  • 不要な言葉がないのでシンプルな記法で読みやすい
  • 階層構造のまま読めるので無駄な頭のメモリを使う必要がない

テスト管理

  • よりテストケースが管理しやすいツールがでてきたら乗り換え可能。資産の再利用

テスト実施者

好きなツールにアウトプットして使える。

後の発展

テストケース設計のプログラミングができることによってプログラミングテクニックがつかえるようになったということ。その土台ができた。

たとえば、DI系をつかって、各レイヤーをDIによって依存関係を自動で解決させたり、夢が沢山ありますね。

テストケース設計もプログラミングする時代が来るかもしれないね

補足:Kotlinを選んだわけ

Kotlinを選んだ理由も書いておきます。

今回はコアコンセプトを伝えたいプロトタイプのようなもので、ぶっちゃけ上のドメインが解決できれば方法がなんでもよいです。むしろ教えて下さい。

僕が扱える言語で、PHP, Koltin, JS , TypeScript, Java, (Rust) の中で一番DSL向きだとおもったのがKotlinでした。

Lamdbaの省略系の短さ、レシーバー指定のLamdba、拡張関数のおかげで表現力がかなり高いと思いました。

DSLの中であまりにも自由にかけすぎても、不要な不具合に出会う確率が高くなるので、sealed classや Null安全の機能で、型である程度縛り、できることだけできるようになりました。

あとがき

テストケース設計者、テストケースレビュー、テストケース実施者にやさしいDSLをつくるにはどうしたらよいのか考え、プロトタイプを作りました。

え?タグ、重要度、マイルストーンとかテストケースのレポート/分析用の話が忘れていないかって?

今回は、コアコンセプトにフォーカスしました。ここがもっとも重要で素朴な実装です。0.3.0で実装されています。

0.4.0ではレポート/分析用の任意のデータのDSLまで扱っています。次回のブログをお楽しみに。