Тестирование RESTful API при помощи Serenity и REST Assured на Kotlin
В этой заметке попробуем разобраться с тем, как на языке Kotlin написать тесты для проверки RESTful API приложения. В интернете полно статей про то, как писать подобные тесты на Java и Serenity, мы же воспользуемся Kotlin.
За основу была взята статья RESTful API Testing Using Serenity and REST Assured — A Guide.
Какие технологии и библиотеки будем использовать:
- Kotlin
- JUnit
- Serenity BDD
- Rest Assured
- Gradle
- IntelliJ Idea
Что такое Serenity BDD?
Serenity - это фреймворк с открытым исходным кодом для написания функциональных тестов, в который из коробки встроена система генерации отчетов о выполненных тестах.
Краткий список возможностей:
- Интеграция с другими популярными фреймворками при помощи плагинов (JUnit, Cucumber, JBehave, Rest-Assured, Selenium и другие)
- Множество встроенных возможностей (параллельное выполнение, сохранение скриншотов для UI тестов, Data Driven testing)
- Формирование детализированного отчёта
- Интеграция с системами сборки (maven, gradle, ant)
Что такое Rest Assured?
Rest Assured — это Java-библиотека для тестирования RESTful API. Код, написанный с помощью этой библиотеки, имеет простой и понятный синтаксис. В тесте можно выполнить запрос к API буквально в одну строчку кода и при помощи DSL синтаксиса проверять полученный результат.
Подготовка
В оригинальной статье в качестве сборщика проекта используется maven, мы же воспользуемся Gradle. (https://gradle.org/maven-vs-gradle/)
Перед тем как приступить к написанию тестов, необходимо:
- Установить JDK8 http://www.oracle.com/technetwork/java/javase/downloads/index.html
- Установить Gradle.
В простейшем варианте надо скачать архив, распаковать его в любую папку и прописать путь к папке bin в PATH https://docs.gradle.org/current/userguide/installation.html
В зависимости от используемого дистрибутива ОС, gradle можно установить и через apt, brew, linuxbrew, choco и т.п. - Установить IDE.
Я использую IntelliJ Idea Community, так как поддержка Kotlin в ней есть из коробки — https://www.jetbrains.com/idea/download/
Настройка проекта в IDE
Создаем новый проект (Create new project), выбираем Gradle — Kotlin (Java):
Заполняем необходимые поля:
Указываем путь к gradle. У меня в MacOS он установлен через homebrew и Idea не находит его автоматически, поэтому я указываю путь руками: /usr/local/Cellar/gradle/4.2.1/libexec:
В итоге получаем пустой проект, с настроенным файлом конфигурации build.gradle, в котором все, что необходимо для написания кода на Kotlin, уже подключено.
Настройка зависимостей
Нам необходимо подключить следующие библиотеки:
Вносим изменения в build.gradle. Жирным шрифтом отмечены сделанные изменения относительно той версии, которая получилась после создания проекта.
group 'io.github.zaz600'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '1.1.60'
ext.serenity_version = '1.8.0'
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "net.serenity-bdd:serenity-gradle-plugin:$serenity_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'net.serenity-bdd.aggregator'
repositories {
mavenCentral()
}
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
testCompile('junit:junit:4.12')
testCompile group: 'net.serenity-bdd', name: 'serenity-core', version: "$serenity_version"
testCompile group: 'net.serenity-bdd', name: 'serenity-junit', version: "$serenity_version"
testCompile group: 'net.serenity-bdd', name: 'serenity-rest-assured', version: "$serenity_version"
testCompile group: 'net.serenity-bdd', name: 'serenity-gradle-plugin', version: "$serenity_version"
testCompile('org.assertj:assertj-core:3.8.0')
testCompile('org.slf4j:slf4j-simple:1.7.25')
}
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
gradle.startParameter.continueOnFailure = true
Вот и вся настройка. Перейдем к написанию тестов.
Первый тест
Для написания тестов нам необходим какой-либо веб-сайт, работающий по RESTful API. Воспользуемся http://www.groupkt.com/, который предоставляет сервисы, с помощью которых, например, можно выполнить поиск страны по коду ISO при помощи метода API /country/get/iso2code/
Полный URL для поиска по коду RU выглядит так: http://services.groupkt.com/country/get/iso2code/RU
Ниже приведён ответ сервера в формате JSON:
{
"RestResponse" : {
"messages" : [ "Country found matching code [RU]." ],
"result" : {
"name" : "Russian Federation",
"alpha2_code" : "RU",
"alpha3_code" : "RUS"
}
}
}
В папке serenity-rest-assured-automation/src/test/kotlin/tests/ создаем файл GroupktAPITest.kt и добавляем в него следующий текст:
package tests
import io.restassured.RestAssured
import net.serenitybdd.junit.runners.SerenityRunner
import org.junit.Test
import org.hamcrest.Matchers.`is`
import org.junit.runner.RunWith
const val ROOT_API_URL = "http://services.groupkt.com"
@RunWith(SerenityRunner::class)
class CountriesSearchTests {
@Test
fun verifyThatWeCanFindRussiaUsingTheCodeRU() {
RestAssured.`when`().get("$ROOT_API_URL/country/get/iso2code/RU")
.then().assertThat().statusCode(200)
.and().body("RestResponse.result.name", `is`("Russian Federation"))
}
}
Тест делает запрос на адрес http://services.groupkt.com/country/get/iso2code/RU и проверяет, что в ответе в поле RestResponse.result.name содержится значение “Russian Federation”
Кстати, код взят из оригинальной статьи и автоматически конвертирован из Java в Kotlin при его вставке в Idea. При этом некоторые инструкции (when, is) были экранированы при помощи символа ``, так как они являются ключевыми словами в Kotlin.
Попробуем запустить тест.
Запуск тестов
Тесты можно запускать двумя способами: через JUnit и через Gradle.
Чтобы запустить тесты через JUnit, в Idea надо навести мышь на значок запуска слева от строки, в которой объявлен класс и выбрать Run Test:
После прогона тестов (в нашем случае одного теста), внизу окна Idea отобразится результат тестирования:
Теперь настроим запуск тестов через Gradle и в дальнейшем будем пользоваться этим способом запуска.
- Меню Run — Edit Configurations — + — Gradle
- В tasks вводим: clean clearReports test aggregate
clean — удалить старый скомпилированный код и артефакты
cleanReports — удалить ранее сформированные отчеты
test — скомпилировать тесты и прогнать их
aggregate — построить отчёт
3. Выбираем Gradle project из выпадающего списка
Сохраняем изменения и запускаем тест при помощи Gradle. После прогона увидим результат:
Посмотрим, как выглядит упавший тест. Для этого поменяем в тесте код страны и запустим тест заново:
$ROOT_API_URL/country/get/iso2code/RU
на
$ROOT_API_URL/country/get/iso2code/US
В логе можно увидеть причину падения:
JSON path RestResponse.result.name doesn't match.
Expected: is "Russian Federation"
Actual: United States of America
Возвращаем код страны обратно на RU.
Теперь добавим несколько тестов, чтобы проверить поиск других стран.
Добавляем еще тесты
Добавим поиск США и Индии по их ISO-кодам:
@Test
fun verifyThatWeCanFindIndiaUsingTheCodeIN() {
RestAssured.`when`().get("$ROOT_API_URL/country/get/iso2code/IN")
.then().assertThat().statusCode(200)
.and().body("RestResponse.result.name", `is`("India"))
}
@Test
fun verifyThatWeCanFindUSAUsingTheCodeUS() {
RestAssured.`when`().get("$ROOT_API_URL/country/get/iso2code/US")
.then().assertThat().statusCode(200)
.and().body("RestResponse.result.name", `is`("United States of America"))
}
Полную версию можно посмотреть тут.
Если посмотреть на код, то можно увидеть, что в нём повторяются одни и те же инструкции для каждого теста:
- запрос ресурса
- проверка http статуса
- проверка результата
Чтобы избавиться от повторяющихся фрагментов кода, в нашем случае можно:
- Написать отдельную функцию, которая объединит в себе все однотипные шаги
- Воспользоваться понятием @Step из Serenity
- Параметризовать тест
Первый вариант хотя и является самым очевидным, но он не такой гибкий, как остальные. Что если нам понадобится добавить еще пару проверок для других тестов?
Рассмотрим второй вариант.
В Serenity тесты принято разбивать на небольшие блоки кода, которые затем можно использовать повторно при необходимости. Эти блоки называются “шагами”. Применяя подобный принцип на практике, можно перейти от использования технических формулировок (“код http-ответа = 200” или “выполни http запрос”) к выражениям, понятным обычному человеку: “действие было успешным” или “выполни поиск”. Поэтому всегда лучше начинать с реализации маленьких шагов, описывающих какие-либо действия, а затем использовать их для создания более сложных шагов и сценариев. В общем, чтобы сделать тесты легко поддерживаемыми необходимо использовать принцип DRY (Don’t Repeat Yourself)
Выносим шаги в отдельный пакет
Создаем новый файл src/test/kotlin/steps/CountriesSearchSteps.kt :
package steps
import io.restassured.response.Response
import net.serenitybdd.rest.SerenityRest
import net.thucydides.core.annotations.Step
import org.hamcrest.Matchers.`is`
open class CountriesSearchSteps {
private val ISO_CODE_SEARCH = "http://services.groupkt.com/country/get/iso2code/"
private var response: Response? = null
@Step("I try to search the country by {0} code")
open fun searchCountryByCode(code: String) {
response = SerenityRest.`when`().get(ISO_CODE_SEARCH + code)
}
@Step
open fun searchIsExecutedSuccesfully() {
response!!.then().statusCode(200)
}
@Step
open fun iShouldFindCountry(country: String) {
response!!.then().body("RestResponse.result.name", `is`(country))
}
@Step
open fun alfa3CodeIsEqual(code: String) {
response!!.then().body("RestResponse.result.alpha3_code", `is`(code))
}
}
Обратите внимание, все шаги, которые мы создали, ничего не возвращают, а для передачи параметров между ними используется приватная переменная.
Класс и его методы объявлены как open. Это необходимо, чтобы Serenity мог корректно сформировать отчёт, детализируя его пошагово.
Обновляем тесты:
package tests
import net.serenitybdd.junit.runners.SerenityRunner
import net.thucydides.core.annotations.Steps
import org.junit.Test
import org.junit.runner.RunWith
import steps.CountriesSearchSteps
@RunWith(SerenityRunner::class)
class CountriesSearchTests {
@Steps
lateinit var countriesSearchSteps: CountriesSearchSteps
@Test
fun verifyThatWeCanFindRussiaUsingTheCodeRU() {
countriesSearchSteps.searchCountryByCode("RU")
countriesSearchSteps.searchIsExecutedSuccesfully()
countriesSearchSteps.iShouldFindCountry("Russian Federation")
}
@Test
fun verifyThatWeCanFindIndiaCountryUsingTheCodeIN() {
countriesSearchSteps.searchCountryByCode("IN")
countriesSearchSteps.searchIsExecutedSuccesfully()
countriesSearchSteps.iShouldFindCountry("India")
}
@Test
fun verifyThatWeCanFindBrazilCountryUsingTheCodeBR() {
countriesSearchSteps.searchCountryByCode("BR")
countriesSearchSteps.searchIsExecutedSuccesfully()
countriesSearchSteps.iShouldFindCountry("Brazil")
}
}
Запускаем прогон тестов. Он должен завершиться успешно.
Теперь мы можем спокойно добавить дополнительный шаг и применить его в одном тесте.
// CountriesSearchSteps.kt@Step
open fun alfa3CodeIsEqual(code: String) {
response!!.then().body("RestResponse.result.alpha3_code", `is`(code))
}// GroupktAPITest.kt
@Test
fun verifyThatWeCanFindRussiaUsingTheCodeRU() {
countriesSearchSteps.searchCountryByCode("RU")
countriesSearchSteps.searchIsExecutedSuccesfully()
countriesSearchSteps.iShouldFindCountry("Russian Federation")
countriesSearchSteps.alfa3CodeIsEqual("RUS")
}
Параметризованные тесты
Библиотека Serenity имеет встроенные механизмы для создания параметризованных тестов. Тестовые данные можно подгружать из csv-файла или задать в коде. Рассмотрим второй вариант.
Что необходимо сделать:
- Заменить SerenityRunner на SerenityParameterizedRunner.
- Добавить конструктор по-умолчанию для тестового класса.
- Добавить объект-компаньон с функцией, которая возвращающает тестовые данные, в тестовый класс
В итоге Serenity при инициализации тестового класса будет передавать в него очередную порцию тестовых данных и запускать все тесты внутри класса. К тестовым данным можно получить доступ через атрибуты класса.
Пишем тест:
package tests
import net.serenitybdd.junit.runners.SerenityParameterizedRunner
import net.thucydides.core.annotations.Steps
import org.junit.Test
import org.junit.runner.RunWith
import steps.CountriesSearchSteps
import net.thucydides.junit.annotations.TestData
import java.util.*
@RunWith(SerenityParameterizedRunner::class)
class CountriesSearchTests(private var countryCode: String, private var expectedValue: String) {
companion object {
@JvmStatic
@TestData
fun testData(): Collection<Array<String>> =
Arrays.asList(
arrayOf("RU", "Russian Federation"),
arrayOf("IN", "India")
)
}
@Steps
lateinit var countriesSearchSteps: CountriesSearchSteps
@Test
fun verifyThatWeCanFindCountryByCode() {
countriesSearchSteps.searchCountryByCode(countryCode)
countriesSearchSteps.searchIsExecutedSuccessfully()
countriesSearchSteps.iShouldFindCountry(expectedValue)
}
}
Отчёты
Чтобы формировался отчет, необходимо использовать команду aggregate, которую мы добавили ранее при настройке запуска тестов через Gradle.
Сформированный отчет можно найти в каталоге target/site/serenity/. Переходим в этот каталог и открываем файл index.html:
В отчете можно увидеть шаги, которые выполнялись в каждом тесте и даже результаты http- запросов.
BDD
Добавляем в build.gradle в блок dependencies строчку для подключения к проекту библиотеки JBehve:
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
...
testCompile group: 'net.serenity-bdd', name: 'serenity-jbehave', version: '1.34.0'
testCompile('org.assertj:assertj-core:3.8.0')
testCompile('org.slf4j:slf4j-simple:1.7.25')
}
Создаем файл src/test/resources/stories/CountrySearch.story с описанием тестового сценария.
Searching by keyword
Scenario: Поиск по коду страны
Given У меня есть код страны [countryCode]
When Я выполняю поиск страны с помощью кода
Then Поиск должен завершиться успешно
And Я должен найти страну [countryName]
Examples:
|countryCode|countryName|
|RU|Russian Federation|
|US|United States of America|
Создаем файл CountrySearchStepsDefinitions.kt в каталоге src/test/kotlin/serenitytest/jbehave/, где будем писать код, реализующий конкретные шаги теста из сценария.
package serenitytest.jbehave
import net.thucydides.core.annotations.Steps
import org.jbehave.core.annotations.Given
import org.jbehave.core.annotations.Then
import org.jbehave.core.annotations.When
import serenitytest.serenity.CountriesSearchSteps
open class CountrySearchStepsDefinitions {
@Steps
lateinit var countriesSearchSteps: CountriesSearchSteps
@Given("У меня есть код страны \$countryCode")
open fun givenIHaveAnCountryCode(countryCode: String) {
countriesSearchSteps.saveCode(countryCode)
}
@When("Я выполняю поиск страны с помощью кода")
open fun whenISearchCode() {
countriesSearchSteps.searchCountryByCode()
}
@Then("Поиск должен завершиться успешно")
open fun thenSearchExecutedSuccess() {
countriesSearchSteps.searchIsExecutedSuccessfully()
}
@Then("Я должен найти страну \$countryName")
open fun thenIShouldFindCountry(countryName: String) {
countriesSearchSteps.iShouldFindCountry(countryName)
}
}
Файл CountriesSearchSteps.kt переносим в каталог src/test/kotlin/serenitytest/serenity/
Создаем файл src/test/kotlin/serenitytest/AcceptanceTestSuite.kt
package serenitytest
import net.serenitybdd.jbehave.SerenityStories
open class AcceptanceTestSuite : SerenityStories()
Запускаем тестирование, проверяем результат и отчет.
Заключение
Переписать тесты на Kotlin оказалось не так сложно. Все Java библиотеки, которые мы использовали, работают как надо. Единственное место, где потребовалось написать кода больше, чем на Java, это объект-компаньон, чтобы Serenity мог сформировать детальный отчёт.