http tests workshop
===
[TOC]
### 1.2 Настраиваем build.gradle
Переменные `group` и `version` не понадобятся, можно удалить.
В `repositories` нужно добавить ссылку на внутренние maven репозитории.
В `dependencies` добавить библиотеки [spock](https://github.com/spockframework/spock) и [tests-core](https://github.qiwi.com/qa/tests-core).
В `tests` настройки запуска тестов.
Должно получиться вот так:
```gradle=
plugins {
id 'java'
id 'groovy'
}
repositories {
maven { url 'http://maven.osmp.ru/nexus/content/groups/public' }
maven { url 'http://maven.osmp.ru/nexus/content/repositories/releases' }
maven { url 'http://maven.osmp.ru/nexus/content/repositories/qa'}
mavenCentral()
}
dependencies {
//spock and groovy libs
implementation 'org.codehaus.groovy:groovy:3.0.8'
testImplementation platform('org.spockframework:spock-bom:2.0-groovy-3.0')
testImplementation 'org.spockframework:spock-core'
testImplementation 'com.athaydes:spock-reports:2.1-groovy-3.0'
//qiwi qa tests-core
implementation 'com.qiwi.qa:tests-core:2.12.71'
}
test {
useJUnitPlatform()
maxParallelForks = 4
testLogging.showStandardStreams = true
}
```
### 1.3 Создаем пакеты
В main/java и test/groovy нужно создать пакеты со стандартным неймингом `com.qiwi.qa.workshop`

---
## 2 Создаем пробный тест
### 2.1 Spock specification
Если установлен spock плагин для идеи, то в меню New должен появиться тип файла Spock specification. В `test/groovy/com.qiwi.qa.workshop` создадим спеку `ExampleTest`

Тесты на споке выглядят примерно вот так:
```groovy=
package com.qiwi.qa.workshop
import spock.lang.Specification
class ExampleTest extends Specification {
def "Calculator test"() {
given: "X and Y"
def x = 1
def y = 2
when: "Adding x and y"
def result = x + y
then: "Result is correct"
result == 3
}
}
```
Запускаем, проверяем что окружение настроено и работает.
---
## 3 Пишем тест на GET /v1/person/{phone}
### 3.1 Создаем класс с запросом
В библиотеке tests-core есть классы-обертки для http запросов `com.qiwi.qa.fw.http.okhttp.Request` и `com.qiwi.qa.fw.http.okhttp.Response`. C их помощью в пакете request можно создать java класс GetPersonRequest который будет выглядеть примерно так:
```java=
package com.qiwi.qa.workshop.http;
import com.qiwi.qa.fw.http.okhttp.Request;
import com.qiwi.qa.fw.http.okhttp.Response;
public class GetPersonRequest {
public static Response send(String phone) {
return Request.get("https://study-workshop-service.ci.qiwi.com/v1/person/" + phone);
}
}
```
### 3.2 Создаем тест
В `test/groovy/com.qiwi.qa.workshop` создадим спеку `GetPersonTest`
Туда добавим код
```groovy=
package com.qiwi.qa.workshop
import com.qiwi.qa.workshop.http.GetPersonRequest
import spock.lang.Specification
class GetPersonTest extends Specification {
def "Get person"() {
given: "Person`s phone"
def phone
when: "Sending request"
def response = GetPersonRequest.send(phone)
then: "Check response"
}
}
```
__Задание__
На запрос GET `/v1/person/78008008080`
в теле ответа вернется `{"phone":78008008080,"fio":"Иванов Иван Иванович","birthDate":"01.01.1971"}`
Нужно
1. поправить given
2. в where проверить что код ответа равен 200 и что в теле запроса json написанны выше
3. Прогнать тест
Можно использовать поля `.code` и `.body` класса `Response` (переменная `def response`).
__Спойлер-спойлер__
```groovy=
package com.qiwi.qa.workshop
import com.qiwi.qa.workshop.http.GetPersonRequest
import spock.lang.Specification
class GetPersonTest extends Specification {
def "Get person"() {
given: "Person`s phone"
def phone = "78008008080"
when: "Sending request"
def response = GetPersonRequest.send(phone)
then: "Check response"
response.statusOK
response.body == '{"phone":78008008080,"fio":"Иванов Иван Иванович","birthDate":"01.01.1971"}'
}
}
```
## 4 Проверка полей ответа
### 4.1 Добавляем класс Person
Чтобы было удобнее проверять поля в ответе лучше десериализовать json в объект.
Для этого создаем в пакете `com.qiwi.qa.workshop.model` pojo класс Person.
```java=
package com.qiwi.qa.workshop.model;
public class Person {
}
```
__Задание__
1. В класс Person нужно добавить private String поля `fio`, `phone` и `birthDate`.
2. И геттеры/сеттеры для них. В идее геттеры сеттеры можно сгенерить: правой кнопкой внутри файла -> Generate -> Getters and Setters
__Спойлер-спойлер__
```java=
package com.qiwi.qa.workshop.model;
public class Person {
private String fio;
private String phone;
private String birthDate;
public String getFio() {
return fio;
}
public void setFio(String fio) {
this.fio = fio;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getBirthDate() {
return birthDate;
}
public void setBirthDate(String birthDate) {
this.birthDate = birthDate;
}
}
```
### 4.2 Десериализация и проверка полей
В классе `com.qiwi.qa.fw.http.okhttp.Response` есть метод для десериализации тела запроса из json в pojo класс - `json2Object`.
__Задание__
1. В тесте `GetPerson` в блоке `where:` с помощью метода `.json2Object(Person.class)` положить в переменную `person` объект Person
2. Используя методы `getXyz()` класса Person проверить что:
- в поле `phone` `78008008080`
- в поле `fio` `Иванов Иван Иванович`
- в поле `birthDate` `01.01.1971`
3. Запустить тест
__Спойлер-спойлер__
```groovy=
package com.qiwi.qa.workshop
import com.qiwi.qa.workshop.model.Person
import com.qiwi.qa.workshop.http.GetPersonRequest
import spock.lang.Specification
class GetPersonTest extends Specification {
def "Get person"() {
when: "Sending request"
def response = GetPersonRequest.send(expectedPerson.phone)
then: "Check response"
response.isStatusOK()
def person = response.json2Object(Person.class)
person.phone == "78008008080"
person.fio == "Иванов Иван Иванович"
person.birthDate == "01.01.1971"
}
}
```
## 5 Конфиг
### 5.1 Настраиваем конфиг в проекте
Чтобы не хардкодить сервисные данные в тестах их можно вынести в конфиг.
В build.gradle добавляем зависимость `implementation 'com.typesafe:config:1.4.1'`
В итоге файл должен получиться примерно такой:
```gradle=
plugins {
id 'java'
id 'groovy'
}
repositories {
maven { url 'http://maven.osmp.ru/nexus/content/groups/public' }
maven { url 'http://maven.osmp.ru/nexus/content/repositories/releases' }
maven { url 'http://maven.osmp.ru/nexus/content/repositories/qa'}
mavenCentral()
}
dependencies {
//spock and groovy libs
implementation 'org.codehaus.groovy:groovy:3.0.8'
testImplementation platform('org.spockframework:spock-bom:2.0-groovy-3.0')
testImplementation 'org.spockframework:spock-core'
testImplementation 'com.athaydes:spock-reports:2.1-groovy-3.0'
//qiwi qa tests-core
implementation 'com.qiwi.qa:tests-core:2.12.71'
//typesafe config
implementation 'com.typesafe:config:1.4.1'
}
test {
useJUnitPlatform()
maxParallelForks = 4
testLogging.showStandardStreams = true
}
```
В src/test/resources созаем файл application.conf

```hocon=
person {
default {
phone = "78008008080"
fio = "Иванов Иван Иванович"
birthDate = "01.01.1971"
}
}
```
### 5.2 Подтягиваем данные в тесты
В пакете `com.qiwi.qa.workshop` создаем класс `TestConfig`
```java=
package com.qiwi.qa.workshop;
import com.qiwi.qa.workshop.model.Person;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigBeanFactory;
import com.typesafe.config.ConfigFactory;
public class TestConfig {
private static Config config = ConfigFactory.load();
public static Person getDefaultPerson() {
Config personConfig;
}
}
```
__Задание__
1. С помощью метода `config.getConfig("person.default")` положить данные из конфига в переменную `personConfig`
2. С помощью метода `.getString("phone")` достать из personConfig поля `phone`, `fio` и `birthDate`
3. Создать новый объект `new Person()`
4. В переменную с `new Person()` положить `phone`, `fio` и `birthDate` и вернуть переменную
__Спойлер-спойлер__
```java=
package com.qiwi.qa.workshop;
import com.qiwi.qa.workshop.model.Person;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigBeanFactory;
import com.typesafe.config.ConfigFactory;
public class TestConfig {
private static Config config = ConfigFactory.load();
public static Person getDefaultPerson() {
Config personConfig = config.getConfig("person.default");
String phone = personConfig.getString("phone");
String fio = personConfig.getString("fio");
String birthDate = personConfig.getString("birthDate");
Person person = new Person();
person.setFio(fio);
person.setPhone(phone);
person.setBirthDate(birthDate);
return person;
}
}
```
### 5.3 Используем данные в тесте
В тесте добавляем блок `given: "Expected person"`
```groovy=
package com.qiwi.qa.workshop
import com.qiwi.qa.workshop.model.Person
import com.qiwi.qa.workshop.TestConfig
import com.qiwi.qa.workshop.http.GetPersonRequest
import spock.lang.Specification
class GetPersonTest extends Specification {
def "Get person"() {
given: "Expected person"
def expectedPerson
when: "Sending request"
def response = GetPersonRequest.send(expectedPerson.phone)
then: "Check response"
response.statusOK
def person = response.json2Object(Person.class)
person.fio == "78008008080"
person.phone == "Иванов Иван Иванович"
person.birthDate == "01.01.1971"
}
}
```
__Задание__
1. Получить ожидаемые данные из конфига
2. Сравнить что поля в ответе от сервиса соответствуют полям в конфиге
3. Прогнать тест
__Спойлер-спойлер__
```groovy=
package com.qiwi.qa.workshop
import com.qiwi.qa.workshop.http.GetPersonRequest
import com.qiwi.qa.workshop.model.Person
import com.qiwi.qa.workshop.TestConfig;
import spock.lang.Specification
class GetPersonTest extends Specification {
def "Get person"() {
given: "Expected person"
def expectedPerson = TestConfig.defaultPerson
when: "Sending request"
def response = GetPersonRequest.send(expectedPerson.phone)
then: "Check response"
response.statusOK
def person = response.json2Object(Person.class)
person.fio == expectedPerson.fio
person.phone == expectedPerson.phone
person.birthDate == expectedPerson.birthDate
}
}
```
## 6 Setup/cleanup
### 6.1 Добавляем Person'ов
Чтобы не зависеть от данных в БД лучше лучше добавлять нового Person перед каждым тестом.
Нового Person можно добавить запросом POST `/v1/person` с телом а-ля `{"phone":78008008081,"fio":"Петров Петр Петрович","birthDate":"11.11.1977"}`
Создаем в пакете `com.qiwi.qa.workshop.http` класс который будет отправлять запросы для создания Person'ов - `AddPersonRequest`
```java=
package com.qiwi.qa.workshop.http;
import com.qiwi.qa.fw.http.okhttp.Request;
import com.qiwi.qa.fw.http.okhttp.Response;
import com.qiwi.qa.fw.json.JsonUtility;
import com.qiwi.qa.workshop.TestConfig;
import com.qiwi.qa.workshop.model.Person;
import okhttp3.Headers;
public class AddPersonRequest {
public static Response send(Person person) {
String token;
String body;
Headers headers = Headers.of(
"Authorization", token,
"content-type", "application/json");
}
}
```
В запросе POST `/v1/person` нужна авторизация. Токен авторизации задается в хедере `Authorization`.
__Задание__
1. Добавить в application.conf токен `296630060b6ab7837ce25eb83095bc52`
2. Добавить в TestConfig метод для получения токена из application.conf
3. Добавить в AddPersonRequest получение токена
4. Добавить в AddPersonRequest сериализацию объекта Person в json с помощью `JsonUtility.toJson(person)`
__Спойлер-спойлер__
application.conf
```hocon=
workshop-service {
token = "296630060b6ab7837ce25eb83095bc52"
}
```
TestConfig
```java=
public static String getToken() {
return config.getString("workshop-service.token");
}
```
AddPersonRequest
```java=
public static Response send(Person person) {
String token = TestConfig.getToken();
String body = JsonUtility.toJson(person);
Headers headers = Headers.of(
"Authorization", token,
"content-type", "application/json");
return Request.post("https://study-workshop-service.ci.qiwi.com/v1/person/", headers, body);
}
```
### 6.2 Добавляем setup
Чтобы убедиться что тесты не упадут если кто-то сотрет данные в базе добавим перед тестом рандомные данные
В `com.qiwi.qa.workshop` создаем класс `Utility` где с методами для генерации рандомных данных
```java=
package com.qiwi.qa.workshop;
import com.qiwi.qa.fw.random.RandomGenerator;
import org.joda.time.DateTime;
public class Utility {
public static String getRandomDate() {
return DateTime.now()
.minusDays(Integer.parseInt(RandomGenerator.getNumber(3)))
.toString("dd.MM.yyyy");
}
public static String getRandomPhone() {
return "7" + RandomGenerator.getNumber(10);
}
}
```
В тесте удаляем получение пользователя из конфига и добавляем метод `def setup()`
```groovy=
package com.qiwi.qa.workshop
import com.qiwi.qa.fw.random.RandomGenerator
import com.qiwi.qa.workshop.http.AddPersonRequest
import com.qiwi.qa.workshop.http.DeletePersonRequest
import com.qiwi.qa.workshop.http.GetPersonRequest
import com.qiwi.qa.workshop.model.Person
import spock.lang.Shared
import spock.lang.Specification
class GetPersonTest extends Specification {
@Shared Person expectedPerson
def setupSpec() {
given: "Random person"
expectedPerson =
and: "Random person is added"
AddPersonRequest.send(expectedPerson)
}
def "Get person"() {
when: "Sending request"
def response = GetPersonRequest.send(expectedPerson.phone)
then: "Check response"
response.statusOK
def person = response.json2Object(Person.class)
person.phone == expectedPerson.phone
person.birthDate == expectedPerson.birthDate
}
}
```
__Задание__
1. В `setupSpec()` создать Person с рандомными полями сгенеренными методами из класса `Utility` (для фио можно использовать `RandomGenerator.getString(10)`)
2. Прогнать тест
__Спойлер-спойлер__
```groovy=
package com.qiwi.qa.workshop
import com.qiwi.qa.fw.random.RandomGenerator
import com.qiwi.qa.workshop.http.AddPersonRequest
import com.qiwi.qa.workshop.http.DeletePersonRequest
import com.qiwi.qa.workshop.http.GetPersonRequest
import com.qiwi.qa.workshop.model.Person
import spock.lang.Shared
import spock.lang.Specification
class GetPersonTest extends Specification {
@Shared Person expectedPerson
def setupSpec() {
given: "Random person"
expectedPerson = new Person()
expectedPerson.setFio(RandomGenerator.getString(10))
expectedPerson.setBirthDate(Utility.randomDate)
expectedPerson.setPhone(Utility.randomPhone)
and: "Random person is added"
AddPersonRequest.send(expectedPerson)
}
def "Get person"() {
when: "Sending request"
def response = GetPersonRequest.send(expectedPerson.phone)
then: "Check response"
response.statusOK
def person = response.json2Object(Person.class)
person.fio == expectedPerson.fio
person.phone == expectedPerson.phone
person.birthDate == expectedPerson.birthDate
}
}
```
### 6.3 Добавляем cleanup
Чтобы данные не копились в БД можно их удалять после теста.
Удалить Person можно запросом DELETE `/v1/person/{phone}`
Создаем в пакете `com.qiwi.qa.workshop.http` класс который будет отправлять запросы для уаления Person'ов - `DeletePersonRequest`
```java=
package com.qiwi.qa.workshop.http;
import com.qiwi.qa.fw.http.okhttp.Request;
import com.qiwi.qa.fw.http.okhttp.Response;
public class DeletePersonRequest {
public static Response send(String phone) {
return Request.delete("https://study-workshop-service.ci.qiwi.com/v1/person/" + phone);
}
}
```
В тесте добавляем метод `cleanupSpec()`
```groovy=
package com.qiwi.qa.workshop
import com.qiwi.qa.fw.random.RandomGenerator
import com.qiwi.qa.workshop.http.AddPersonRequest
import com.qiwi.qa.workshop.http.DeletePersonRequest
import com.qiwi.qa.workshop.http.GetPersonRequest
import com.qiwi.qa.workshop.model.Person
import spock.lang.Shared
import spock.lang.Specification
class GetPersonTest extends Specification {
@Shared Person expectedPerson
def setupSpec() {
given: "Random person"
expectedPerson = new Person()
expectedPerson.setFio(RandomGenerator.getString(10))
expectedPerson.setBirthDate(Utility.randomDate)
expectedPerson.setPhone(Utility.randomPhone)
and: "Random person is added"
AddPersonRequest.send(expectedPerson)
}
def "Get person"() {
when: "Sending request"
def response = GetPersonRequest.send(expectedPerson.phone)
then: "Check response"
response.statusOK
def person = response.json2Object(Person.class)
person.fio == expectedPerson.fio
person.phone == expectedPerson.phone
person.birthDate == expectedPerson.birthDate
}
def cleanupSpec() {
expect: "Delete person"
}
}
```
__Задание__
1. Добавить в `cleanupSpec` удаление `expectedPerson`
2. Прогнать тест
__Спойлер-спойлер__
```groovy=
def cleanupSpec() {
expect: "Delete person"
DeletePersonRequest.send(expectedPerson.phone)
}
```
## 7 Data providers
### 7.1 Добавляем негативные тесты
Добавляем тест на удаление `GetPersonNegativeTest`
```groovy=
package com.qiwi.qa.workshop
import com.qiwi.qa.fw.random.RandomGenerator
import com.qiwi.qa.workshop.http.GetPersonRequest
import org.springframework.http.HttpStatus
import spock.lang.Specification
import spock.lang.Unroll
class GetPersonNegativeTest extends Specification {
def "Get person with random phone should fail"() {
given: "Random numeric phone"
def phone =
when: "Getting person"
def response = GetPersonRequest.send(phone)
then: "Check failure"
response.code ==
}
def "Get person with invalid phone should fail"() {
given: "Random letters phone"
def phone =
when: "Getting person"
def response = GetPersonRequest.send(phone)
then: "Check failure"
response.code ==
}
}
```
__Задание__
1. Для первого теста добавить генерацию рандомного десятисимвольного числа в поле phone
2. Добавить проверку что код ответа `HttpStatus.NOT_FOUND.value()`
3. Для второго теста добавить генерацию рандомной десятисимвольной строки в поле phone
4. Добавить проверку что код ответа `HttpStatus.BAD_REQUEST.value()`
5. Прогнать тесты
__Спойлер-спойлер__
```groovy=
package com.qiwi.qa.workshop
import com.qiwi.qa.fw.random.RandomGenerator
import com.qiwi.qa.workshop.http.GetPersonRequest
import org.springframework.http.HttpStatus
import spock.lang.Specification
import spock.lang.Unroll
class GetPersonNegativeTest extends Specification {
def "Get person with random phone should fail"() {
given: "Random numeric phone"
def phone = RandomGenerator.getNumber(10)
when: "Getting person"
def response = GetPersonRequest.send(phone)
then: "Check failure"
response.code == HttpStatus.NOT_FOUND.value()
}
def "Get person with invalid phone should fail"() {
given: "Random letters phone"
def phone = RandomGenerator.getString(10)
when: "Getting person"
def response = GetPersonRequest.send(phone)
then: "Check failure"
response.code == HttpStatus.BAD_REQUEST.value()
}
}
```
### 7.3 Используем дата провайдер
Можно объеденить два теста в один. Копипастим код:
package com.qiwi.qa.workshop
```groovy=
import com.qiwi.qa.fw.random.RandomGenerator
import com.qiwi.qa.workshop.http.GetPersonRequest
import org.springframework.http.HttpStatus
import spock.lang.Specification
import spock.lang.Unroll
class GetPersonNegativeTest extends Specification {
def "Get person with [phoneType] phone should fail"() {
when: "Getting person"
def response = GetPersonRequest.send(phone)
then: "Check failure"
response.code == expectedStatus
where:
phoneType | phone || expectedStatus
"random numbers" | "" || ""
"random string" | "" || ""
}
}
```
__Задание__
1. В колонки phone и expectedStatus вместо пустых строк добавить соотвествующие значения как в предыдущем задании
2. Прогнать тесты
3. Посмотреть на описание тестов в репорте (в идее слева внизу или build/spock-reports/index.html в папке с проектом)
4. Заменить описание теста на
```groovy=
@Unroll
def "Get person with [#phoneType] phone should fail"() {
```
5. Прогнать тесты
6. Посмотреть на описание тестов в репорте (в идее слева внизу или build/spock-reports/index.html в папке с проектом)
__Спойлер-спойлер__
```groovy=
package com.qiwi.qa.workshop
import com.qiwi.qa.fw.random.RandomGenerator
import com.qiwi.qa.workshop.http.GetPersonRequest
import org.springframework.http.HttpStatus
import spock.lang.Specification
import spock.lang.Unroll
class GetPersonNegativeTest extends Specification {
@Unroll
def "Get person with [#phoneType] phone should fail"() {
when: "Getting person"
def response = GetPersonRequest.send(phone)
then: "Check failure"
response.code == expectedStatus
where:
phoneType | phone || expectedStatus
"random numbers" | RandomGenerator.getNumber(10) || HttpStatus.NOT_FOUND.value()
"random string" | RandomGenerator.getString(10) || HttpStatus.BAD_REQUEST.value()
}
}
```
## 8 Заливаем на гитхаб
### 8.1 Создаем репозиторий
В корне проекта создаем файл .gitignore, копируем туда:
```
target
.idea
*.iml
.settings
.project
.gradle
.groovy
.classpath
/target
/build
.sonar
/sonar
src.zip
result.txt
pom(damage-control).xml
.DS_Store
src/DS_Store
infer-out/
/bin/
out/
```
В терминале идеи инициируем репозиторий командой `git init`. Добавляем все файлы командой `git add -A`. Проверяем какие файлы добавились командой `git status`. Должно получиться примерно так:

Делаем коммит - `git commit -m "init commit"`
Открываем группу https://github.qiwi.com/http-workshop и создаем новый репозиторий с каким-нибудь уникальным названием и с настройками как на скриншоте.

Создаем форк репозитория для своего аккаунта.

### 8.2 Заливаем
В копируем ссылку на форк репозитория

В консоли идеи добавляем удаленный репозиторий командой git remote add origin <ссылка на репозиторий>

Апдейтим локальный репозиторий `git fetch origin master`, ребейзимся `git rebase origin/master`, заливаем коммит в форк `git push origin master`.
В гихабе создаем пулл реквест

Ревьюим и мержим в основную репу
