1 автотест на 10 языках программирования

8eece3eb77c9488a808d54d9434ad057.jpg

Всем привет. Меня зовут Ирек, и я в профессиональном IT с 2012 года. Прошел путь от специалиста службы поддержки до разработчика. На данный момент занимаюсь автоматизацией тестирования в компании РТК ИТ.

В статье постараюсь показать на одном кейсе как выглядят автотесты на разных языках, кратко расскажу про свой опыт и ощущения от использования другого языка. 

Компания у нас большая, проекты разные и стек технологий на каждом проекте свой. Встречаются проекты на Go, Kotlin, Ruby, Java, Python, С++ и так далее. В каждом проекте есть процесс тестирования и желательно не усложнять стек добавлением еще одного языка. К тому же если писать автотесты на языке разрабатываемого приложения, то можно консультироваться у разработчиков, а разработчикам показывать код автотестов, как говорится «вместо тысячи слов».

Для чего все это?

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

Не только разработчики переживают за смену стека, но и работодатели не всегда готовы брать людей с другим основным языком. Хотя мой опыт показывает, что это все напрасно. За время работы мы успели перевести много специалистов в свою веру.

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

Статья будет полезна в качестве обзорной, например если вы выбираете дополнительный язык для обучения. Или хотите сделать первые шаги, но не знаете с чего начать.

Приступим

В качестве подопытного в очередной раз выбран petstore (да прибудут в здравии их сервера во веки веков). 

Кейс очень простой:

  1. Делаем Post и создаем зверушку в БД виртуального зоомагазина

  2. В ответ мы получаем Json из которого выдираем ID

  3. Дергаем Get используя ID

  4. Убеждаемся, что вернулся именно наш Барсик)

Графически это выглядит так:

Диаграмма последовательности взаимодействия клиента и сервера petstore

Диаграмма последовательности взаимодействия клиента и сервера petstore

Каждый раздел по языку будет содержать текст автотеста, вариант успешного прогона, прогон с ошибкой, небольшие субъективные комментарии и возможные альтернативы выбранному фреймворку.

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

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. :)

Примечание: Данная статья была написана человеком при использовании естественного интеллекта.

© Habrahabr.ru