KotlinとSpekで仕様を書く

KotlinとSpekで仕様を書く

1. 前書き

Specification Testing frameworks are complementary to Unit Testing frameworks for testing our applications

このチュートリアルでは、Spek framework –JavaおよびKotlinの仕様テストフレームワークを紹介します。

2. 仕様テストとは何ですか?

簡単に言えば、仕様テストでは、仕様から始めて、ソフトウェアのメカニズムではなく、ソフトウェアの意図を説明します。

これは、アプリケーションの事前定義された仕様に対してシステムを検証することを目的としているため、行動駆動開発でしばしば活用されます。

一般的に知られている仕様テストフレームワークには、SpockCucumberJasmine、およびRSpecが含まれます。

2.1. Spekとは何ですか?

Spek is a Kotlin-based Specification Testing framework for the JVMJUnit 5 Test Engineとして機能するように設計されています。 これは、すでにJUnit 5を使用しているプロジェクトに簡単にプラグインして、他のテストと一緒に実行できることを意味します。

必要に応じてJUnitPlatform Runnerの依存関係を使用して、古いJUnit4フレームワークを使用してテストを実行することもできます。

2.2. Mavenの依存関係

Spekを使用するには、必要な依存関係をMavenビルドに追加する必要があります。


    org.jetbrains.spek
    spek-api
    1.1.5
    test


    org.jetbrains.spek
    spek-junit-platform-engine
    1.1.5
    test

spek-apiの依存関係は、テストフレームワークに使用される実際のAPIです。 テストで使用するすべてのものを定義します。 spek-junit-platform-engineの依存関係は、テストを実行するために必要なJUnit5テストエンジンです。

Spekの依存関係はすべて、互いに同じバージョンである必要があることに注意してください。 最新バージョンはhereにあります。

2.3. 最初のテスト

Spekがセットアップされると、テストを記述することは、正しい構造で正しいクラスを記述する単純なケースです。 これは、読みやすくするために少し珍しいです。

Spekでは、すべてのテストが適切なスーパークラス(通常はSpek)から継承し、このクラスのコンストラクターにブロックを渡すことでテストを実装する必要があります。

class FirstSpec : Spek({
    // Implement the test here
})

3. テストスタイル

Specification Testing emphasizes writing tests in a way that’s as readable as possible。 たとえば、Cucumberは、テスト全体を人間が読める言語で記述し、ステップに結び付けてコードを分離します。

Spek works by using special methods that act as readable strings。それぞれに、必要に応じて実行するブロックが与えられます。 テストの読み取り方法に応じて、使用する関数にはいくつかのバリエーションがあります。

3.1. given/on/it

テストを作成する1つの方法は、「given / on / it」スタイルです。

これは、その構造にネストされたgivenon、およびitと呼ばれるメソッドを使用して、テストを記述します。

  • given –テストの初期条件を設定します

  • on –テストアクションを実行します

  • it –テストアクションが正しく実行されたことを表明します

各ブロックを必要な数だけ持つことができますが、次の順序でネストする必要があります。

class CalculatorTest : Spek({
    given("A calculator") {
        val calculator = Calculator()
        on("Adding 3 and 5") {
            val result = calculator.add(3, 5)
            it("Produces 8") {
                assertEquals(8, result)
            }
        }
    }
})

このテストは非常に簡単に読み取ります。 テストステップに焦点を当てると、「計算機が与えられ、3と5を追加すると8が生成されます」と読むことができます。

3.2. describe/it

テストを記述するもう1つの方法は、「describe / it」スタイルです。 代わりに、これはすべてのネストにメソッドdescribeを使用し、アサーションには引き続きitを使用します。

この場合、テストを作成するために必要なだけdescribeメソッドをネストできます。

class CalculatorTest : Spek({
    describe("A calculator") {
        val calculator = Calculator()
        describe("Addition") {
            val result = calculator.add(3, 5)
            it("Produces the correct answer") {
                assertEquals(8, result)
            }
        }
    }
})

このスタイルを使用したテストに適用される構造が少ないため、テストの記述方法の柔軟性が大幅に向上しています。

残念ながら、これの欠点は、「given / on / it」を使用した場合ほどテストが自然に読み取られないことです。

3.3. 追加のスタイル

Spek doesn’t enforce these styles, and it will allow for the keywords to be interchanged as much as desired。 唯一の要件は、すべてのアサーションがit内に存在し、そのレベルで他のブロックが見つからないことです。

使用可能なネストキーワードの完全なリストは次のとおりです。

  • 与えられた

  • on

  • 記述する

  • コンテキスト

これらを使用して、テストの記述方法に最適な構造をテストに提供できます。

3.4. データ駆動型テスト

テストの定義に使用されるメカニズムは、すべて単純な関数呼び出しにすぎません。 これは、通常のコードのように、他のことを実行できることを意味します。 In particular, we can call them in a data-driven way if we so desire

これを行う最も簡単な方法は、使用するデータをループし、このループ内から適切なブロックを呼び出すことです。

class DataDrivenTest : Spek({
    describe("A data driven test") {
        mapOf(
          "hello" to "HELLO",
          "world" to "WORLD"
        ).forEach { input, expected ->
            describe("Capitalising $input") {
                it("Correctly returns $expected") {
                    assertEquals(expected, input.toUpperCase())
                }
            }
        }
    }
})

必要に応じて、このようなあらゆることを実行できますが、これはおそらく最も有用です。

4. アサーション

Spekは、アサーションを使用する特定の方法を規定していません。 代わりに、it allows us to use whatever assertion framework we’re most comfortable with

すでにJUnit5フレームワークをテストランナーとして使用しているため、当然の選択はorg.junit.jupiter.api.Assertionsクラスです。

ただし、テストが改善される場合は、他のアサーションライブラリを使用することもできます(例:KluentExpektHamKrest)。

標準のJUnit5Assertionsクラスの代わりにこれらのライブラリを使用する利点は、テストの読みやすさにあります。

たとえば、Kluentを使用して上記のテストを書き換えると、次のようになります。

class CalculatorTest : Spek({
    describe("A calculator") {
        val calculator = Calculator()
        describe("Addition") {
            val result = calculator.add(3, 5)
            it("Produces the correct answer") {
                result shouldEqual 8
            }
        }
    }
})

5. Before/After Handlers

ほとんどのテストフレームワークと同様に、Spek can also execute logic before/after tests.

これらは、その名前が示すとおり、テスト自体の前または後に実行されるブロックです。

オプションは次のとおりです。

  • beforeGroup

  • afterGroup

  • beforeEachTest

  • afterEachTest

これらは任意のネストキーワードに配置でき、そのグループ内のすべてに適用されます。

Spekの動作方法では、ネストされたキーワード内のすべてのコードはテストの開始直後に実行されますが、制御ブロックはitブロックを中心とした特定の順序で実行されます。

Spekは、外側から内側に向​​かって、同じグループ内にネストされたすべてのitブロックの直前に各beforeEachTestブロックを実行し、すべてのitブロックの直後に各afterEachTestブロックを実行します。 同様に、Spekはすべてのグループの直前に各beforeGroup ブロックを実行し、現在のネスト内のすべてのグループの直後に各afterGroupブロックを実行します。

これは複雑であり、例を挙げて説明するのが最適です。

class GroupTest5 : Spek({
    describe("Outer group") {
        beforeEachTest {
            System.out.println("BeforeEachTest 0")
        }
        beforeGroup {
            System.out.println("BeforeGroup 0")
        }
        afterEachTest {
            System.out.println("AfterEachTest 0")
        }
        afterGroup {
            System.out.println("AfterGroup 0")
        }

        describe("Inner group 1") {
            beforeEachTest {
                System.out.println("BeforeEachTest 1")
            }
            beforeGroup {
                System.out.println("BeforeGroup 1")
            }
            afterEachTest {
                System.out.println("AfterEachTest 1")
            }
            afterGroup {
                System.out.println("AfterGroup 1")
            }
            it("Test 1") {
                System.out.println("Test 1")
            }
        }
    }
})

上記の実行の出力は次のとおりです。

BeforeGroup 0
BeforeGroup 1
BeforeEachTest 0
BeforeEachTest 1
Test 1
AfterEachTest 1
AfterEachTest 0
AfterGroup 1
AfterGroup 0

すぐに、外側のbeforeGroup/afterGroupブロックがテストのセット全体の周りにあるのに対し、内側のbeforeGroup/afterGroupブロックは同じコンテキストのテストの周りにのみあることがわかります。

また、すべてのbeforeGroupブロックがbeforeEachTest ブロックの前に実行され、afterGroup/afterEachTestの場合はその逆であることがわかります。

複数のグループの複数のテスト間の相互作用を示すこの大きな例は、on GitHub.で確認できます。

6. 被験者

Many times, we will be writing a single Spec for a single Test Subject。 Spekはこれを書く便利な方法を提供し、テスト対象を自動的に管理します。 We use the SubjectSpek base class instead of the Spek class for this

When we use this, we need to declare a call to the subject block at the outermost level。 これは、被験者を定義します。 次に、これを任意のテストコードからsubject.として参照できます。

これを使用して、以前の計算機テストを次のように書き直すことができます。

class CalculatorTest : SubjectSpek({
    subject { Calculator() }
    describe("A calculator") {
        describe("Addition") {
            val result = subject.add(3, 5)
            it("Produces the correct answer") {
                assertEquals(8, result)
            }
        }
    }
})

それほど多くはないように思えるかもしれませんが、これにより、特に考慮すべきテストケースが多数ある場合に、テストをより読みやすくすることができます。

6.1. Mavenの依存関係

Subject Extensionを使用するには、Mavenビルドに依存関係を追加する必要があります。


    org.jetbrains.spek
    spek-subject-extension
    1.1.5
    test

7. 概要

Spekは非常に読みやすいテストを可能にする強力なフレームワークです。つまり、組織のすべての部分がテストを読むことができます。

これは、すべての同僚がアプリケーション全体のテストに貢献できるようにするために重要です。

最後に、コードスニペットは、いつものように、over on GitHubで見つけることができます。