Всем привет. Меня зовут Ирек, и я в профессиональном IT с 2012 года. Прошел путь от специалиста службы поддержки до разработчика. На данный момент занимаюсь автоматизацией тестирования в компании РТК ИТ.
В статье постараюсь показать на одном кейсе как выглядят автотесты на разных языках, кратко расскажу про свой опыт и ощущения от использования другого языка.
Компания у нас большая, проекты разные и стек технологий на каждом проекте свой. Встречаются проекты на Go, Kotlin, Ruby, Java, Python, С++ и так далее. В каждом проекте есть процесс тестирования и желательно не усложнять стек добавлением еще одного языка. К тому же если писать автотесты на языке разрабатываемого приложения, то можно консультироваться у разработчиков, а разработчикам показывать код автотестов, как говорится “вместо тысячи слов”.
Для чего все это?
Было много статей про HelloWorld на разных языках программирования, но с автотестами подобных не встречал. Вообще смену стека многие воспринимают очень болезненно, хотя казалось бы, что кардинально ничего не меняется.
Не только разработчики переживают за смену стека, но и работодатели не всегда готовы брать людей с другим основным языком. Хотя мой опыт показывает, что это все напрасно. За время работы мы успели перевести много специалистов в свою веру.
Да - будет другой синтаксис, да - нужно будет чуть глубже изучить нюансы, но синтаксис учится за час, а через 3 месяца вы гарантированно сможете решать любую задачу по автоматизации тестирования используя новый стек.
Статья будет полезна в качестве обзорной, например если вы выбираете дополнительный язык для обучения. Или хотите сделать первые шаги, но не знаете с чего начать.
Приступим
В качестве подопытного в очередной раз выбран petstore (да прибудут в здравии их сервера во веки веков).
Кейс очень простой:
Делаем Post и создаем зверушку в БД виртуального зоомагазина
В ответ мы получаем Json из которого выдираем ID
Дергаем Get используя ID
Убеждаемся, что вернулся именно наш Барсик)
Графически это выглядит так:
Каждый раздел по языку будет содержать текст автотеста, вариант успешного прогона, прогон с ошибкой, небольшие субъективные комментарии и возможные альтернативы выбранному фреймворку.
Java
JUnit5 + RestAssured
import io.restassured.http.ContentType;
import org.json.JSONObject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
public class PetStoreTests {
@Test
void createNewPetTest() {
String uri = "https://petstore.swagger.io/v2/pet/";
String petName = "Барсик";
JSONObject bodyJO = new JSONObject()
.put("name", petName)
.put("status", "available");
String newPetId = given()
.when()
.log().all()
.contentType(ContentType.JSON)
.body(bodyJO.toString())
.post(uri)
.then()
.log().all()
.statusCode(200)
.extract()
.jsonPath()
.getString("id");
String actualPetName = given()
.when()
.log().all()
.contentType(ContentType.JSON)
.get(uri + newPetId)
.then()
.log().all()
.statusCode(200)
.extract()
.jsonPath()
.getString("name");
Assertions.assertEquals(petName, actualPetName);
}
}
Примеры прогонов
Комментарии
Одна из классик автоматизированного тестирования. RestAssured выглядит многословным, но это только в сыром виде. В реальных проектах все оборачивается в хелперы и подобные тесты превращаются в элегантные шорты реальные 3 строки: по одной строке на запрос и одна строка на assert. Запуск через Gradle довольно лаконичен, но если хочешь получить чуть более подробный отчет, то тянется весь стектрейс, что очень мешает, особенно при полном прогоне в ci.
Java мой основной язык сейчас и не могу оценить на сколько легко на нем писать, но переход с python был безболезненным.
Если говорить об альтернативах JUnit, то это TestNG. Остальные фреймворки менее распространены.
Python
Pytest + requests
import requests
def test_create_new_pet():
uri = 'https://petstore.swagger.io/v2/pet/'
pet_name = 'Барсик'
body = {'name': pet_name, 'status': 'available'}
post_response = requests.post(uri, json=body)
assert post_response.status_code == 200, 'Неверный статус код при создании зверушки'
get_response = requests.get(uri+str(post_response.json().get('id')))
assert get_response.status_code == 200, 'Неверный статус код при получении зверушки'
assert get_response.json().get('name') == pet_name
Примеры прогонов
Комментарии
До неприличия лаконичный код из коробки. Первое время в работе с языком напрягает понимание принципом работы виртуального окружения. Вещь хорошая, но так как (почти) отсутствует в других языках, то вызывает определенные затруднения на старте. Ну и стоит отметить магию однострочников в Python. Их много, они разные и их приходится заучивать. В остальном тут все прекрасно.
На рынке наметилась тенденция по переходу с python на другие языки. Связано это по большей части с проблемами скорости прогона тестов. Но анонс на новые версии python гласил, что будут его ускорять всеми силами. Будем посмотреть.
Помимо распространенного Pytest, есть не менее популярный Robot framework.
JavaScript
Playwright
const { test, expect } = require("@playwright/test");
const JSONbig = require("json-bigint");
const URI = "https://petstore.swagger.io/v2/pet/";
const PET_NAME = "Барсик";
test("create new pet test", async ({ request }) => {
const respPost = await request.post(URI, {
data: {
name: PET_NAME,
status: "available",
},
});
expect(respPost.ok()).toBeTruthy();
const respJson = (await respPost.body()).toString();
const id = JSONbig.parse(respJson).id;
const respGet = await request.get(URI + id);
expect(respGet.ok()).toBeTruthy();
expect(await respGet.json()).toHaveProperty("name", PET_NAME);
});
Примеры прогонов
Комментарии
Не самый простой вариант начинать с playwright свой путь в JS тестирование. Тут из коробки есть асинхронность, и я не разобрался, как запускать тесты без нее.
Попил немного крови идентификатор, который не поместился в integer и округлял последние 3 цифры. Быстрое гугление показало, что можно парсить через JSONbig и будет тебе счастье. Не долго думая - согласился.
Playwright не самый популярный фреймворк, но активно ее набирает. Можно посмотреть еще в сторону Jest, как на старожила и хорошо распространенного.
Rust
Reqwest
use reqwest::blocking::Client;
use reqwest::{self, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
#[derive(Serialize, Deserialize)]
struct Pet {
id: i64,
name: String,
}
#[test]
fn test_create_new_pet() -> Result<(), reqwest::Error> {
let uri = "https://petstore.swagger.io/v2/pet/";
let pet_name = "Барсик";
let body = json!({"name": pet_name, "status": "available"});
let client = Client::new();
let post_res = client.post(uri).json(&body).send()?;
assert_eq!(post_res.status(), StatusCode::OK);
let res_body = post_res.text()?;
let root: Value = serde_json::from_str(&res_body).unwrap();
let pet_id = root.get("id");
let pet_uri = format!("{}{}", uri, pet_id.unwrap());
let get_res = client.get(pet_uri).send()?;
assert_eq!(get_res.status(), StatusCode::OK);
let actual_pet: Pet = get_res.json()?;
assert_eq!(actual_pet.name, pet_name);
Ok(())
}
Примеры прогонов
Комментарии
Вряд ли кто-то использует Rust в качестве основного языка для api тестов, но было очень интересно его пощупать.
Низкоуровневые языки отличаются сложной реализацией обусловленной более плотной работой с системой и памятью в частности. Если неправильно обработать переменную в Rust, то она перестает существовать и потом гадай почему ничего не компилируется. Извечный unwrap тоже подбешивает, но без него еще хуже)
По языку очень много документации и вполне адекватное коммьюнити. Rust для тестов это конечно оверкилл.
C#
NUnit + RestSharp
using NUnit.Framework;
using RestSharp;
using System.Text.Json;
namespace NUnit.Tests;
public class Tests
{
[Test]
public void CreateNewPetTest()
{
var uri = "https://petstore.swagger.io/v2/pet/";
var petName = "Барсик";
var client = new RestClient();
var postRequest = new RestRequest(uri)
.AddHeader("Content-Type", "application/json")
.AddJsonBody(new { name = petName, status = "available" });
var postResponse = client.Post(postRequest);
Assert.AreEqual(200, (int)postResponse.StatusCode);
var id = JsonDocument.Parse(postResponse.Content).RootElement.GetProperty("id").ToString();
var getRequest = new RestRequest(uri+id);
var getResponse = client.Get(getRequest);
Assert.AreEqual(200, (int)getResponse.StatusCode);
var actualName = JsonDocument.Parse(getResponse.Content).RootElement.GetProperty("name").ToString();
Assert.AreEqual(petName, actualName);
}
}
Примеры прогонов
Комментарии
Dotnet на удивление с легкостью заработал на моем линуксовом ноутбуке. Помню, что раньше нужно было заморачиваться с Mono, а сейчас все ставится напрямую из репки.
Для джавистов язык покажется чем-то знакомым и родным. Учится легко, читается легко.
Go
Resty + Testify
package tests
import (
"encoding/json"
"fmt"
"testing"
"github.com/go-resty/resty/v2"
"github.com/stretchr/testify/assert"
)
type Pet struct {
Id int `json:"id"`
Name string `json:"Name"`
}
func TestCreateNewPet(t *testing.T) {
uri := "https://petstore.swagger.io/v2/pet/"
petName := "Барсик"
body := `{"name": "` + petName + `", "status": "available"}`
myPet := Pet{}
client := resty.New()
postResponse, _ := client.R().
SetHeader("Content-Type", "application/json").
SetBody(body).
Post(uri)
assert.Equal(t, 200, postResponse.StatusCode(), "Неверный статус код при создании зверушки")
json.Unmarshal(postResponse.Body(), &myPet)
getResponse, _ := client.R().
Get(fmt.Sprintf("%s%d", uri, myPet.Id))
json.Unmarshal(getResponse.Body(), &myPet)
assert.Equal(t, 200, getResponse.StatusCode(), "Неверный статус код при получении зверушки")
assert.Equal(t, petName, myPet.Name, "Неверное имя зверушки")
}
Примеры прогонов
Коментарии
Один из новых стандартов в тестировании. Все больше команд переходят на Golang, появляются новые вакансии. Пришлось поломать голову над тем как правильно ставить зависимости, но в итоге все удалось. Хотелось бы побольше документации, но жить можно.
У меня так и не получилось парсить json без использования десериализации(
Сам язык оставил положительные впечатления. На Хабре есть большая и довольно подробная статья про тестирование на Go.
PHP
Codeception
<?php
namespace Tests;
use Tests\Support\ApiTester;
class PetStoreCest
{
public function createNewPet(ApiTester $I)
{
$URI = 'https://petstore.swagger.io/v2/pet/';
$PET_NAME = 'Барсик';
$I->sendPostAsJson($URI, [
'name' => $PET_NAME,
'status' => 'available'
]);
$I->seeResponseCodeIs(200);
$id = $I->grabDataFromResponseByJsonPath('$.id');
$I->sendGet($URI . strval($id[0]));
$I->seeResponseCodeIs(200);
$I->seeResponseIsJson();
$I->seeResponseContainsJson(array('name' => $PET_NAME));
}
}
Примеры прогонов
Комментарии
Последний раз сталкивался еще в универе с php и рад, что язык не стоит на месте. Автотесты очень лаконичные и приятные глазу, хоть и с привкусом BDD.
Среда разворачивается быстро, все ставится прямо из репки.
Как альтернативу можно рассмотреть фреймворк PHPUnit. Он довольно старенький, но еще в ходу.
Kotlin
Kotest + fuel
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.json.responseJson
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
class PetStoreTest : ShouldSpec({
val uri = "https://petstore.swagger.io/v2/pet/"
val petName = "Барсик"
val jsonBody = """
{ "name": "$petName",
"status": "available"}
"""
should("create new pet") {
val (_, responsePost, resultPost) = Fuel.post(uri)
.header("Content-Type" to "application/json; charset=utf-8")
.body(jsonBody)
.responseJson()
responsePost.statusCode shouldBe 200
val newPetId = resultPost.get().obj().get("id")
val (_, responseGet, resultGet) = Fuel.get(uri + newPetId).responseJson()
responseGet.statusCode shouldBe 200
resultGet.get().obj().get("name") shouldBe "Мурзик"
}
})
Примеры прогонов
Комментарии
Почему-то ждал, что на Kotlin не найду интересный фреймворков, но рад. что ошибался. Kotest работает хорошо, код приятный. Долго искал приличный клиент и остановился на Fuel, так как чаще остальных встречался в поисковике.
Вроде та же Java, но которая все делает за тебя. Даже не знаю, хорошо это или плохо.
Ruby
Rspec
require 'rspec'
require 'net/http'
require 'json'
describe "Pet store test" do
it "create new pet" do
uri = URI('https://petstore.swagger.io/v2/pet/')
pet_name = 'Барсик'
body = { name: pet_name, status: 'available' }
headers = { 'Content-Type': 'application/json' }
post_resp = Net::HTTP.post(uri, body.to_json, headers)
expect(post_resp.code).to eq('200')
uri.path += JSON.parse(post_resp.body)['id'].to_s
get_resp = Net::HTTP.get_response(uri)
expect(get_resp.code).to eq('200')
expect(JSON.parse(get_resp.body)['name']).to eq(pet_name)
end
end
Примеры прогонов
Комментарии
Самый неоднозначный язык по ощущениям. Субъективно не особо жалую пробелы в выражениях и сокращения. Почему например to_json полностью, а to_string обрезали до to_s? Конечно ко всему можно привыкнуть, но на первых порах с читабельностью кода на Ruby прям сложно.
При гуглении нужно уметь не уползти на сайты с рельсами, так как все называется примерно одинаково и одно с другим не работает. Пришлось повозиться, когда случайно поставил рельсовую библиотеку вместо обычной.
1C
1testrunner
Перем юТест;
Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт
юТест = ЮнитТестирование;
ВсеТесты = Новый Массив;
ВсеТесты.Добавить("Тест_Должен_СоздатьНовуюЗверушку");
Возврат ВсеТесты;
КонецФункции
Процедура Тест_Должен_СоздатьНовуюЗверушку() Экспорт
ИмяЗверушки = "Барсик";
СоединениеHTTP = Новый HTTPСоединение("https://petstore.swagger.io/v2/pet/");
СтруктураДанныхJSON = Новый Структура();
СтруктураДанныхJSON.Вставить("name", ИмяЗверушки);
СтруктураДанныхJSON.Вставить("status", "available");
ЗаписьJSON = Новый ЗаписьJSON();
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, СтруктураДанныхJSON);
СтрокаJSON = ЗаписьJSON.Закрыть();
ЗапросPost = Новый HTTPЗапрос();
ЗапросPost.Заголовки.Вставить("Content-type", "application/json");
ЗапросPost.УстановитьТелоИзСтроки(СтрокаJSON);
ОтветPost = СоединениеHTTP.ОтправитьДляОбработки(ЗапросPost);
юТест.ПроверитьРавенство(ОтветPost.КодСостояния, 200);
ЧтениеJSON = Новый ЧтениеJSON();
ЧтениеJSON.УстановитьСтроку(ОтветPost.ПолучитьТелоКакСтроку("UTF-8"));
ТелоОтвета = ПрочитатьJSON(ЧтениеJSON);
ЗапросGet = Новый HTTPЗапрос(ТелоОтвета.id);
ЗапросGet.Заголовки.Вставить("Content-type", "application/json");
ОтветGet = СоединениеHTTP.Получить(ЗапросGet);
юТест.ПроверитьРавенство(ОтветGet.КодСостояния, 200);
ЧтениеJSON = Новый ЧтениеJSON();
ЧтениеJSON.УстановитьСтроку(ОтветGet.ПолучитьТелоКакСтроку("UTF-8"));
ТелоОтвета = ПрочитатьJSON(ЧтениеJSON);
юТест.ПроверитьРавенство(ТелоОтвета.name, ИмяЗверушки);
КонецПроцедуры
Примеры прогонов
Комментарии
Да, одинесникам тоже нужны автотесты. В сыром виде все довольно многословно, но пара оберток - и будет хорошо.
Сама платформа без лицензии не запускается, но чтобы пощупать достаточно и 1Script. Вообще код на 1С не обязательно писать именно на русском языке и если с русским у вас плохо еще со школы, то не страшно, так как для каждой команды и ключевого слова есть вариант на английском.
Альтернативных фреймворков найти не удалось.
Вместо заключения
Надеюсь вы извлекли что-то полезное для себя из статьи.
Все примеры можно скачать с Github и запустить локально.
Для каждого языка есть readme, где описано как поставить окружение, как запустить скачанный тест и как начать писать свои тесты.
Буду рад комментариям, вопросам, issue, pr. :)
Примечание: Данная статья была написана человеком при использовании естественного интеллекта.
Комментарии (11)
Free_ze
17.05.2023 14:39+1Извечный unwrap тоже подбешивает, но без него еще хуже
В примере они заменяют отдельные вызовы assert, вполне лаконично. Кроме того, непонятно, почему некоторые ошибки пробрасываются наверх, а некоторые — распаковываются явно? Здесь вполне допустимо было бы сделать типом возврата
Result<(), Box<dyn std::error::Error>>
и пробрасывать всё. Или все черезunwrap
.ErikNas Автор
17.05.2023 14:39+2Спасибо большое за замечание! Обязательно поправлю. Мне не хватило времени глубже разобраться, а вообще rust book замечательный и ведет тебя за ручку от helloworld к многопоточному веб-серверу.
Helltraitor
17.05.2023 14:39Поскольку Result - это конректный тип, лучше его вообще не возвращать (типы могут называться одинаково, но быть разными), а использовать `.expect("текст ошибки")` (или его вариант с вычисляемой лямбдой для строки).
ErikNas Автор
17.05.2023 14:39+1Спасибо.
Получается, что вместоserde_json::from_str(&res_body).unwrap();
лучше использовать
serde_json::from_str(&res_body).expect("Ошибка при десереализации JSON");
Правильно вас понял?
aborouhin
17.05.2023 14:39Для C# в реальном приложении, в котором наверняка был бы класс Pet, тест был бы ещё лаконичнее за счёт использования PostJson / GetJson с автоматической (де)сериализацией. Так что с питоном за первенство в лаконичности ещё можно посоревноваться :)
ErikNas Автор
17.05.2023 14:39+3Да, в сыром виде многие языки выглядят громоздко, а в реальных проектах все подобные тесты превращаются в 3-4 строчки читабельного кода)
savostin
17.05.2023 14:39Так и не понял при чем тут языки программирования к тестированию API endpoint... Почему тогда нет примера на bash + curl + jq? ;)
Я думал будет какой-нибудь один универсальный unit-test, который может тестировать код на 10 языках...
ErikNas Автор
17.05.2023 14:39+1Хотел добавить bash, но не нашел хорошего тестового фреймворка, либо плохо искал)
А что должен тестировать универсальный unit test?
Free_ze
Разве компилятор не даст исчерпывающий ответ с советами по исправлению?