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` ![](https://hackmd.qiwi.com/uploads/upload_a73a3087e3697dfbcb111641e6e135d2.png) --- ## 2 Создаем пробный тест ### 2.1 Spock specification Если установлен spock плагин для идеи, то в меню New должен появиться тип файла Spock specification. В `test/groovy/com.qiwi.qa.workshop` создадим спеку `ExampleTest` ![](https://hackmd.qiwi.com/uploads/upload_ce217d6db558e6e1716f6ff63865e360.png) Тесты на споке выглядят примерно вот так: ```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 ![](https://hackmd.qiwi.com/uploads/upload_4a96214098d8810b03003299303d7ff6.png) ```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`. Должно получиться примерно так: ![](https://hackmd.qiwi.com/uploads/upload_6047a58f73de4e0da588d2918202a8a1.png) Делаем коммит - `git commit -m "init commit"` Открываем группу https://github.qiwi.com/http-workshop и создаем новый репозиторий с каким-нибудь уникальным названием и с настройками как на скриншоте. ![](https://hackmd.qiwi.com/uploads/upload_88f5f97b7a10d73263ddf008d018e70f.png) Создаем форк репозитория для своего аккаунта. ![](https://hackmd.qiwi.com/uploads/upload_94e35894ebef36b27dcb3382124890bc.png) ### 8.2 Заливаем В копируем ссылку на форк репозитория ![](https://hackmd.qiwi.com/uploads/upload_e98755ab47ef95328258ab681819a179.png) В консоли идеи добавляем удаленный репозиторий командой git remote add origin <ссылка на репозиторий> ![](https://hackmd.qiwi.com/uploads/upload_18cda59d598b59b0c799c6efbd500005.png) Апдейтим локальный репозиторий `git fetch origin master`, ребейзимся `git rebase origin/master`, заливаем коммит в форк `git push origin master`. В гихабе создаем пулл реквест ![](https://hackmd.qiwi.com/uploads/upload_af29045a1f21f5dba2b41e3783e20efc.png) Ревьюим и мержим в основную репу ![](https://hackmd.qiwi.com/uploads/upload_8aee024f3800fcbdb4d32ced08eee328.png)