Написание спецификаций с Kotlin и Spek

Написание спецификаций с Kotlin и Spek

1. Вступление

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

В этом руководстве мы познакомим вас сSpek framework - средой тестирования спецификаций для Java и Kotlin.

2. Что такое тестирование спецификаций?

Проще говоря, в тестировании спецификаций мы начинаем со спецификации и описываем предназначение программного обеспечения, а не его механизм.

Это часто используется в поведенчески-ориентированной разработке, поскольку целью является проверка системы на соответствие предопределенным спецификациям нашего приложения.

Общеизвестные структуры тестирования спецификаций включаютSpock,Cucumber,Jasmine иRSpec.

2.1. Что такое Спек?

Spek is a Kotlin-based Specification Testing framework for the JVM. Он разработан для работы какJUnit 5 Test Engine. Это означает, что мы можем легко подключить его к любому проекту, который уже использует JUnit 5 для запуска вместе с любыми другими тестами, которые у нас могут быть.

Также возможно запускать тесты, используя старую структуру JUnit 4, используя при необходимости зависимость JUnit Platform Runner.

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 - это тестовый движок JUnit 5, необходимый для выполнения наших тестов.

Обратите внимание, что все зависимости 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

Один из способов, которым мы можем написать наши тесты, это стиль «задано / включено / отправлено».

Здесь используются методы с именамиgiven,on и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

Другой способ, которым мы можем написать наши тесты, это стиль «описать / это». Вместо этого он использует метод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)
            }
        }
    }
})

В тестах, использующих этот стиль, реализована меньшая структура, а это означает, что у нас гораздо больше гибкости при написании тестов.

К сожалению, обратная сторона этого заключается в том, что тесты не читаются так естественно, как когда мы используем «дано / на / оно».

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. Утверждения

Спек не предписывает какого-либо конкретного способа использования утверждений. Вместо этогоit allows us to use whatever assertion framework we’re most comfortable with.

Очевидным выбором будет классorg.junit.jupiter.api.Assertions, поскольку мы уже используем платформу JUnit 5 в качестве средства выполнения тестов.

Однако мы также можем использовать любую другую библиотеку утверждений, которая нам нужна, если она улучшает наши тесты - например,Kluent,Expekt илиHamKrest.

Преимущество использования этих библиотек вместо стандартного класса JUnit 5Assertions заключается в удобочитаемости тестов.

Например, вышеприведенный тест, переписанный с использованием 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 будет выполнять каждый блокbeforeEachTest непосредственно перед каждым блокомit, вложенным в ту же группу, и каждый блокafterEachTest сразу после каждого блокаit. Точно так же 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.