QA Automation Блог

Тестируем REST API с Retrofit2

У меня полностью разработана система на Java, которая тестирует запросы в REST API, получая заранее подготовленные данные из json файлов. Здесь я покажу минимальные куски кода, которые помогут понять, как создать Java классы, которые будут содержать все ендпоинты вашего REST API. Для начала нужно установить Retrofit 2 и кажется он требует okhttp3:

<dependency>
            <groupId>com.squareup.retrofit2</groupId>
            <artifactId>retrofit</artifactId>
            <version>2.1.0</version>
</dependency>
<dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>3.4.1</version>
</dependency>

Создадим файл ApiService.java, которые будет содержать запросы в API:

public interface ApiService {
    @GET("api/user")
    Call<JsonElement> getUser(@Header("Accept-Language") String locale,
                              @Header("Authorization") String token);
}

GET это тип запроса, в скобках путь к ендпоинту апи. Внутри - заголовки которые мы передаем.

Ещё мы создадим класс, который тоже будет содержать методы, каждый из которых будет вызывать методы из класса ApiService, но с индивидуальными обработчиками. Зачем это нужно? Ну, например, каждый из запросов порождает определенного формата ответ от сервера, и модели этих ответов мы можем создать заранее, и легко работать потом с данными из этих ответов прямо в тестах. Вот пример метода из класса с запросами ApiEndpoints.java:

public class ApiEndpoints {
  public static UserToken getUser(String token) throws IOException {
        String locale = ConfigurationInstance.getInstance().getLocale(); // получение значения из конфигурации
        String authHeaderValue = "Bearer " + token;
        Call<JsonElement> call = RestClient.getInstance().getApiService().getUser(locale, authHeaderValue);
        Response response = call.execute();
        Object responseObject = response.body();
        Gson gson = new Gson();
        String json = responseObject.toString();
        UserToken responseUserToken = gson.fromJson(json, UserToken.class);
        return responseUserToken;
    }
}

Тут ответ преобразуется в тип UserToken. Это модель ответа сервера на такой запрос и она создана заранее.

Ну и наконец вызов запроса из самого теста:

@Test(description = "GET /api/user", groups = {"User"})
    public void getUser(ITestContext context) throws IOException, InterruptedException {
        String token = (String) context.getAttribute("token"); //токен берется из конфигураций или прошлых запросов
        UserToken userTokenResponse = ApiEndpoints.getUser(token); //вызов запроса
    }

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

Позже классы ApiService и ApiEndpoints разрастаются и содержат все ендпоинты вашего апи. В методы класса ApiEndpoints можно долбиться поочередно в рамках какого-то юзер-стори, ре-юзая данные из ответов одних запросов как входные данные для других запросов, и так далее. В общем простор для фантазий.

Сурсы в этом посте немного пообрезаны и могут не работать в чистом виде, но думаю разобраться что нужно добавить будет уже не сложно.

Нахрен CSV, давай парсить тест данные из json

CSV слишком простой и тупой для хранения данных тестов, предлагаю хранить эти данные в json, т.к. json огонь и его все любят.

1 Ставим json.simple через Maven:

<dependency>  
    <groupId>com.googlecode.json-simple</groupId>  
    <artifactId>json-simple</artifactId>  
    <version>1.1.1</version>  
</dependency>

2 Пишем метод, который принимает как сырые данные json формата, так и путь к .json файлу:

public static JSONObject parseJsonData(String data) throws ParseException, IOException {  
    boolean isJsonContent = data.substring(0, 1).matches("\\{");  
    Object object;  
    JSONParser parser = new JSONParser();  
    if (isJsonContent) object = parser.parse(data);  
    else object = parser.parse(new FileReader(data));  
    return (JSONObject) object;  
}

3 Он возвращает обьект типа JSONObject, с которым можно ковыряться удобными способами:

Object jsonRequests = jsonObject.get("Requests");  
  
int currentRequestId = 0;  
  
JSONObject request = (JSONObject) jsonRequests.get(currentRequestId);  
String requestName = (String) request.get("Name");  
JSONArray testCases = (JSONArray) request.get("TestCases");

Из JSONObject можно выковыривать любые данные, а с JSONArray работать как с обычным массивом.

Как через testng.xml передать параметр только одному методу?

На сайте нашел кучу любых методов передачи параметров из testng.xml:

<?xml version="1.0" encoding="UTF-8"?>  
<suite name="ParameterSuite">  
  <parameter name="suiteParam" value="suiteScoped"></parameter>  
  <parameter name="commonParam" value="suiteValue"></parameter>  
  <test name="MethodParamScopedTest">  
    <parameter name="testParam" value="testScoped"></parameter>  
    <classes>  
      <parameter name="classesParam" value="classesScoped"></parameter>  
      <class name="com.javarticles.testng.TestClass1">  
        <parameter name="classParam" value="classScoped"></parameter>  
        <parameter name="commonParam" value="overridenClassValue"></parameter>  
      </class>  
      <class name="com.javarticles.testng.TestClass2"/>  
    </classes>  
  </test>  
</suite>

Но, что за подстава, самый глубокий из них передает параметры классу тестов, а как передать только одному методу? А вот как:

<test verbose="1" name="addCard" annotations="JDK" preserve-order="true">  
    <classes>  
        <class name="app.passenger.User">  
            <methods>  
                <include name="changeUserInfo"/>  
            </methods>  
        </class>  
        <class name="app.passenger.Payments">  
            <methods>  
                <include name="addCard"/>  
                <include name="addCard">  
                    <parameter name="testCase" value="Success2ndCard"/>  
                </include>  
            </methods>  
        </class>  
    </classes>  
</test>

Запилил виджет JIRA для Dashing-JS

Что такое Dashing? Это охуительный дашборд для отображения любой статистики, полученной из любого места в реальном времени. Вот красивенький пример с официального сайта:

Дашинг отображает любые виджеты, запиленные на HTML и SCSS, а данные получает скриптом на CoffeeScript.
А чо такое Dashing-JS? Это порт дашинга на Node.js. Т.е. данные он получает скриптом на Node.js.
На Гитхабе разместил виджет, который получает из JIRA список задач по запросу JQL и отображает их на виджете.
https://github.com/vglushonkov/dashing-js-list-jira-issues

CI & CD

"Давайте следующие два месяца проведем над работой с автоматизацией тестирования и инфраструктурой деплоя, и переделаем наше приложение. Пока отложим разработку новых фич"

- сказал однажды несуществующий в реальности продукт менеджер.

Как передать параметр из Jenkins -> ANT -> TestNG

  1. В Jenkins идем в настройки Джоба и выставляем "This build is parameterized"
  2. Называем параметр "additionalScreens" например
  3. В ant.xml проставляем
    <property name="additionalScreens" value=""/>
    
  4. В ant.xml в таргете не забываем передать этот параметр в TestNG:
    <target name="Test.Real" depends="compile">  
            <testng outputdir="${testdir}" classpathref="all.classpath" haltOnFailure="true" testnames="Buy">  
                <xmlfileset dir="${basedir}" includes="${testngxml}"/>  
                <jvmarg value="-DadditionalScreens=${additionalScreens}" />  
            </testng>  
    </target>
    
  5. В TestNG.xml параметр никак не фигурирует, там просто вызывается тест:
    <test verbose="1" name="Buy" annotations="JDK">  
        <classes>  
            <class name="TestNG.Buy">  
                <methods>  
                    <include name="shop"/>  
                    <include name="Scan"/>  
                    <include name="Buy"/>  
                </methods>  
            </class>  
        </classes>  
    </test>
    
  6. В методе @BeforeClass принимаем параметр и дальше работаем с ним как хотим:
    @Parameters({ "additionalScreens" })  
        @BeforeClass  
        public void prepare(String additionalScreens, ITestContext context) throws Exception {  
            context.setAttribute("additionalScreens", additionalScreens);  
            //......  
    }
    
  7. Запускаем Джоб в Дженкинсе и заполняем или выбираем параметр "additionalScreens" - он передается в тест. Это может быть, например, урл сервера, или тип проверки, и т.д.
    Не благодари!

Как заставить выполняться @AfterClass в TestNG

Заметил, что метод @AfterClass не выполняется, если хоть один из тестов к классе завалился или пропущен. Так как заставить её выполняться, ведь там идет и driver.quit() и весь репортинг в стороннюю мониторинг систему?
Похоже на "Как высыпаться за 30 минут в день - читайте в моей новой книге "Никак, блядь".
Лучше сделаем из этого метода такой:

@AfterGroups("thisClassGroup")  
    public void report() throws Exception{  
        driver.quit();  
        if (Objects.equals(fullErrorMessage, "")) fullErrorMessage = "OK";  
        Log.report(fullErrorMessage, "thisClassGroup", finalErrorType);  
    }
А всем тест методам пропишем
@Test(description = "Test1", groups = "thisClassGroup")

Как проверить наличие элемента, не завалив тест

Запиши себе эту функцию, очень пригодится. Вызываем её, чтобы проверить наличие элемента, а потом, например, кликаем его. Отсутствие элемента не завалит тест. Передаем туда конструкцию By и драйвер.
elementExists(By.xpath(nestedCoupon), driver)
public static boolean elementExists(By by, WebDriver driver) {  
    try {  
        driver.findElement(by);  
    } catch (NoSuchElementException e) {  
        return false;  
    }  
    return true;  
}

Как получать данные из CSV для тестов на TestNG

В TestNG Есть клевая приблуда для предоставления данных тестам под названием DataProvider. Но удобнее ведь хранить данные во внешних файлах. Например, в случае .csv файла новый тест кейс таким образом создается тупо добавлением новой строчки в .csv таблицу. Написанный ниже DataProvider запускает тест каждый раз, как находит новую строчку в .csv файле, пропуская первую строчку, т.к. мы принимаем её за заголовок.

@DataProvider(name = "Data")  
    public Iterator<Object []> dataReader() throws InterruptedException {  
        List<Object []> dataLines = new ArrayList<>();  
        boolean notDataHeader = false;  
        String[] data;  
        BufferedReader br;  
        String line;  
        String workingDirectory = System.getProperty("user.dir");  
        String filePath = workingDirectory + "\\TestNG\\data\\" + dataFile;  
  
        try {  
            br = new BufferedReader(new FileReader(filePath));  
            while ((line = br.readLine()) != null) {  
                data = line.split(";");  
                if (notDataHeader) dataLines.add(data);  
                notDataHeader = true;  
            }  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        return dataLines.iterator();  
    }

Как подсунуть DataProvider к тесту и так ясно:

@Test(description="Описание теста", dataProvider = "Data")  
    public void pay(String uin, String price) throws Exception {  
       dotest();  
    }

Это .csv файл с двумя значениями, подходящий к данному примеру. Если нужно добавить значений, не забываем добавить столько же входных параметров для функции public void pay:

uin;price  
12;990

Как заставить TestNG и ANT разговаривать по русски

Вчера мучался с этим полдня, обгуглился до дрожи в пальцах, но так и не нашел решение. TestNG при запуске через IDEA и своих отчетах выдавал мне ??? вместо русских букв. Самый фак был ещё и в том, что в testng.xml присутствовали параметры на русском языке и ничего не работало. Докопался до того, что при запуске сборки Ant она сама запускает JVM со своими параметрами, такими как classpath и ea (enable assertions), но как туда впихнуть -Dfile.encoding=UTF-8 ?

Оказывается, в таргете внутри ant.xml нужно всего лишь указать следующее (см. строку 4):

<target name="test" depends="compile">  
    <testng outputdir="${testdir}" classpathref="all.classpath" haltOnFailure="true">  
        <xmlfileset dir="${basedir}" includes="testng.xml"/>  
        <jvmarg value="-Dfile.encoding=UTF-8" />  
    </testng>  
</target>


Страницы