Русский
Русский
English
Статистика
Реклама

Автоматизация тестирования

Автотесты на базе playwright и jest

15.11.2020 22:16:27 | Автор: admin

Долгое время основным инструментом для автоматизации тестирования был Selenium. Однако в настоящее время на рынке представлено несколько достойных альтернатив, таких как Cypress, Puppeteer и Playwright. Playwright мы и рассмотрим в данной статье.


Playwright это node js библиотека для автоматизации тестирования с единым API для различных браузеров (Chromium, Firefox and WebKit). Разработанная компанией Microsoft. По моему мнению основным преимуществом Playwright является его тесная интерграция с браузерами и возможность взаимодействовать с браузерами на недоступном для Selenium уровне.


В качестве объекта тестирования развернут open source продукт Kanboard.


Для тестирования будем использовать node js, playwright, jest, jest-playwright-preset и jest-html-reporters. Playwright используем для взаимодействия с браузерами. Jest используем, как тест ранер. Jest-html-reporters нужен для генерации HTML репорта.


Первым шагом создадим node проект и установим все необходимые зависимости:
npm init
npm i -D playwright
npm install --save-dev jest
npm install -D jest-playwright-preset
npm install jest-html-reporters --save-dev


После выполнения этих команд мы получаем package.json и node_modules с необходимыми зависимостями. Для того, чтобы настроить репорт и фолдер с тестами вносим изменения в package.json для jest:


{  "name": "kb-playwright-tests",  "version": "1.0.0",  "description": "An automation test framework which is based on playwright.",  "main": "index.js",  "scripts": {    "test": "jest"  },  "author": "",  "license": "ISC",  "jest": {    "testMatch": [      "**/tests/**/*.[jt]s?(x)",      "**/?(*.)+(spec|test).[jt]s?(x)"    ],    "reporters": [      "default",      "jest-html-reporters"    ]  },  "devDependencies": {    "jest": "^26.6.3",    "jest-html-reporters": "^2.1.0",    "jest-playwright-preset": "^1.4.0",    "playwright": "^1.6.1"  }}

Следующим шагом создаем page objects:


const { DashboardPage } = require("./DashboardPage");var config = require('../config');class SignInPage {  constructor(page) {    this.page = page;  }  async openSignInPage() {    await this.page.goto(config.web.url);  }  async signInAs(user) {    await this.page.fill("css=#form-username", user.username);    await this.page.fill("css=#form-password", user.password);    await this.page.click("css=button[type='submit']");    return new DashboardPage(this.page);  }}module.exports = { SignInPage };

 class DashboardPage {  constructor(page) {    this.page = page;  }}module.exports = { DashboardPage };

Тест будет выглядеть следующим образом:


const { chromium } = require("playwright");const { SignInPage } = require("../pageobjectmodels/SignInPage");const { roles } = require("../enums/roles");const assert = require("assert");var config = require("../config");let browser;let page;beforeAll(async () => {  console.log("headless : " + config.web.headless);  console.log("sloMo : " + config.web.sloMo);  browser = await chromium.launch({    headless: config.web.headless == "true",    slowMo: parseInt(config.web.sloMo, 10),  });});afterAll(async () => {  await browser.close();});beforeEach(async () => {  page = await browser.newPage();  if (config.web.networkSubscription) {    page.on("request", (request) =>      console.log(">>", request.method(), request.url())    );    page.on("response", (response) =>      console.log("<<", response.status(), response.url())    );  }});afterEach(async () => {  await page.close();});test("An admin is able to see a dashboard", async () => {  const signInPage = new SignInPage(page);  await signInPage.openSignInPage();  const dashboardPage = await signInPage.signInAs(roles.ADMIN);  const dashboard = await dashboardPage.page.$("#dashboard");  assert(dashboard);});

Строка browser = await chromium.launch({headless: config.web.headless == "true",slowMo: parseInt(config.web.sloMo, 10),}); позволяет настроить headless режим и задержку.


Блок кода beforeEach позволяет настроить запись network, который выглядит следующим образом:


>> GET http://localhost/kanboard/<< 302 http://localhost/kanboard/>> GET http://localhost/kanboard/?controller=AuthController&action=login<< 200 http://localhost/kanboard/?controller=AuthController&action=login>> GET http://localhost/kanboard/assets/css/vendor.min.css?1576454976>> GET http://localhost/kanboard/assets/css/app.min.css?1576454976>> GET http://localhost/kanboard/assets/js/vendor.min.js?1576454976....

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


var config = {};config.web = {};config.web.url = process.env.URL || "http://localhost/kanboard/";config.web.headless = process.env.HEADLESS || false;config.web.sloMo = process.env.SLOMO || 50;config.web.networkSubscription = process.env.NETWORK;module.exports = config;

Теперь для запуска тестов можно использовать следующие команды:


npm run test прогон тестов с значениями по умолчанию (Headless false, sloMo 50, networking off)


NETWORK = 'on' npm run test прогон тестов с значениями Headless false, sloMo 50, networking on


HEADLESS = 'true' npm run test прогон тестов с значениями Headless true, sloMo 50, networking off


После прогона тестов будет сгенерирован репорт jest_html_reporters.html


image


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

Подробнее..

Перевод Не используйте фикстуры в Cypress и юнит-тесты используйте фабричные функции

02.02.2021 18:09:11 | Автор: admin

Для будущих учащихся на курсе JavaScript QA Engineer и всех интересующихся темой автоматизацией тестирования подготовили перевод полезной статьи.

Также приглашаем принять участие в открытом вебинаре на тему Что нужно знать о JS тестировщику. На занятии участники вместе с экспертом рассмотрят особенности JS, которые нужно держать в голове при написании тестов.


Юнит-тесты это здорово когда они надежно работают! На самом деле, есть старая поговорка, что плохой тест это хуже, чем вообще никакой тест. Я могу подтвердить, что недели, проведенные в погоне за случайно ложным отрицательным тестом, не эффективны. Вместо этого можно было использовать это время для написания рабочего кода, который поможет пользователю.

Так что поговорим об одной из этих простейших методик написания менее нестабильных тестов: тестирование фабричных данных.

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

Аспекты тестов, которых мы хотим избежать

  1. Высокое зацепление

  2. Отсутствие типобезопасности (что приводит к длительному рефакторингу и ошибкам)

  3. Огромные папки фикстур

Факторные функции все это исправят.

Так что же такое фабричные функции?

Фабричная функция это функция, которая создает объект. Вот так просто. Да, существует шаблон абстрактная фабрика, популяризированный книгой "Gang Of Four's Design Pattern" несколько десятилетий назад. Давайте сделаем функцию красивой и простой.

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

Вот самый простой пример в мире:

interface ISomeObj {  percentage: string;}export const makeSomeObj = () => {  return {    percentage: Math.random()  };}

Посмотрим, как такой простой шаблон может быть использован для исправления аспектов нестабильных тестов, описанных выше.

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

Пример того, как проводятся нестабильные тесты в реальном мире

Все начинается невинно. Представим, вы или другой мотивированный разработчик в команде хотели бы поделиться опытом и добавить юнит-тест для одной из страниц. Для тестирования функции вы сохраняете некоторые тестовые данные в JSON-файле. Cypress (самая удивительная на момент написания этой статьи библиотека для тестирования пользовательского интерфейса), которая даже поощряет вас использовать JSON файл с тестовыми данными. Но проблема в том, что это даже удаленно не типобезопасность. Поэтому у вас может быть опечатка в вашем JSON и вы будете тратить часы на поиски проблемы.

Чтобы проиллюстрировать это, давайте рассмотрим пример кода для бизнеса и кода для автоматизации тестирования. Для большинства этих примеров мы предположим, что вы работаете в страховой компании, которая объясняет, как работают правила в каждом штате США.

// This file is "src/pages/newYorkInfo.tsx"import * as React from 'react';interface IUser {    state: string;    address: string;    isAdmin: boolean;    deleted: boolean | undefined;}export const NewYorkUserPage: React.FunctionComponent<{ user: IUser }> = props => {    if (props.user.state === 'NY' && !props.user.deleted) {        const welcomeMessage = `Welcome`;        return <h1 id="ny-dashboard">{welcomeMessage}</h1>;    } else {        return <div>ACCESS DENIED</div>;    }};

Код выглядит неплохо, так что давайте напишем JSON для хранения положительного тестового примера.

// fixtures/user.json{    state: 'NY',    isAdmin: true,    address: '55 Main St',}

А теперь тестовый код. Я продемонстрирую проблему, используя какой-нибудь psuedo-код для теста Cypress, но вы можете представить себе, что это происходит с любым тестовым кодом, в котором вы загружаете фикстуры и запускаете тестовое утверждение.

// When the UI calls the user endpoint, return the JSON as the mocked return valuecy.route('GET', '/user/**', 'fixture:user.json');cy.visit('/dashboard');cy.get('#ny-dashboard').should('exist')

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

Плохое решение если один файл заработал, продолжайте создавать JSON-файлы

Стоит ли просто создать еще один JSON-файл фикстуры? К сожалению, это простое решение возникает постоянно, потому что оно самое простое (поначалу). Но с увеличением количества случаев, растет и количество JSON-файлов. Вам понадобится 52 различных JSON-файла, чтобы протестировать каждую страницу для каждого пользователя в США. Когда вы начнете тестирование, если пользователь является или не является администратором, вам придется создать 104 файла. Это много файлов!

Но у вас все равно есть проблема типобезопасности. Допустим, Product Owner приходит в команду и говорит: Давайте будем отображать имя пользователя, когда мы его приветствуем.

Таким образом, вы добавляете свойство name в интерфейс и обновляете пользовательский интерфейс для работы в этом примере.

// This file is "src/pages/newYorkInfo.tsx"import * as React from 'react';interface IUser {    name: string;    state: string;    address: string;    isAdmin: boolean;    deleted: boolean | undefined;}export const NewYorkUserPage: React.FunctionComponent<{ user: IUser }> = props => {    if (props.user.state === 'NY' && !props.user.deleted) {        const welcomeMessage = `Welcome ${props.user.name.toLowerCase()}!`;        return <h1 id="ny-dashboard">{welcomeMessage}</h1>;    } else {        return <div>ACCESS DENIED</div>;    }};

Здорово, что вы обновили код для бизнеса, но фикстура JSON устарела. А так как у фикстуры JSON нет свойства name, вы получаете следующую ошибку:

Uncaught TypeError: Cannot read property 'toLowerCase' of undefined

Теперь вы должны добавить свойство name ко всем 52 пользовательским JSON фикстурам. Это можно решить с помощью Typescript.

Немного лучшее решение: Переместите его в файл TypeScript

Переместив JSON из файла исправления в .ts файл, компилятор Typescript найдет для вас ошибку:

// this file is "testData/users"import {IUser} from 'src/pages/newYorkInfo';// Property 'name' is missing in type '{ state: string; isAdmin: true; address: string; deleted: false; }' but required in type 'IUser'.ts(2741)export const generalUser: IUser = {    state: 'NY',    isAdmin: true,    address: '55 Main St',    deleted: false,};

И мы обновим тестовый код, чтобы использовать этот новый объект.

import { generalUser } from 'testData/users';// When the UI calls the user endpoint, return the JSON as the mocked return valuecy.route('GET', '/user/**', generalUser);cy.visit('/dashboard');cy.get('#ny-dashboard').should('exist')

Спасибо Typescript! Как только вы решите проблему с компилятором, добавив name: 'Bob Smith' в GeneralUser:, код компилируется чисто, а лучше всего то, что ваш тест снова пройдет!

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

Например, что происходит, когда появляется разработчик, который еще новичок в юнит-тестировании. Все, о чем думается, это то, что надо проверить основное свойство, которое включает в себя удаленного пользователя. Поэтому они добавляют deleted: false в объект generalUser.

Бабах! Ваш тест не проходит, и их тест проходит. Вот что значит быть высоко сцепленным.

Поэтому разработчик тратит несколько минут (или часов) на дебаггинг и понимает, что оба теста имеют одни и те же базовые данные. Таким образом, разработчик использует простое (но недальновидное решение) из предыдущих и создает другой объект deletedUser, так что есть 1 объект на тест. Это может быстро выйти из-под контроля я видел файлы тестовых данных длиной 5000 строк.

Здесь можно увидеть, как странно это может выглядеть.

// this file is "testData/users"import {IUser} from 'src/pages/newYorkInfo';export const nonAdminUser: IUser = {    name: 'Bob',    state: 'NY',    isAdmin: false,    address: '55 Main St',    deleted: false,};export const adminUser: IUser = {    name: 'Bob',    state: 'NY',    isAdmin: true,    address: '55 Main St',    deleted: false,};export const deletedAdminUser: IUser = {    name: 'Bob',    state: 'NY',    isAdmin: true,    address: '55 Main St',    deleted: true,};export const deletedNonAdmin: IUser = {    name: 'Bob',    state: 'NY',    isAdmin: false,    address: '55 Main St',    deleted: true,};// and on and on and on again...

Должен быть путь получше.

Хорошее решение: Фабричная Функция

Так как же нам рефакторить огромный файл объектов? Сделаем одну функцию!

// src/factories/userimport faker from 'faker';import {IUser} from 'src/pages/newYorkInfo';export const makeFakeUser = (): IUser => {    return {        name: faker.name.firstName() + ' ' + faker.name.lastName(),        state: faker.address.stateAbbr(),        isAdmin: faker.random.boolean(),        address: faker.address.streetAddress(),        deleted: faker.random.boolean(),    }}

Теперь каждый тест может просто вызвать makeFakeUser(), когда он хочет создать пользователя.

И самое лучшее в этом то, что, делая все случайным в фабричной функции, это показывает, что ни один отдельный тест не принадлежит этой функции. Если тест является особым видом IUser, то позже придется модифицировать его самостоятельно.

И это легко сделать. Давайте представим себе удаленный пользовательский тест, где нас не волнует имя пользователя или что-то в этом роде. Нас волнует только то, что они удалены.

import { makeFakeUser } from 'src/factories/user';import {IUser} from 'src/pages/newYorkInfo';// Arrangeconst randomUser = makeFakeUser();const deletedUser: IUser = { ...randomUser, ...{  deleted: true};cy.route('GET', '/user/**', deletedUser);// Actcy.visit('/dashboard');// Assertcy.find('ACCESS DENIED').should('exist')

Для меня, прелесть этого подхода в том, что он сам себя документирует. Любой, кто смотрит на этот тестовый код, должен понимать, что когда API возвращает удаленного пользователя, мы должны найти "Access Denied" на странице.

Но я думаю, что мы сделаем это еще чище.

Лучшее решение: просто переопределить mergePartially

Выше допускалось использование оператор spread, так как это был небольшой объект. Но это может быть более раздражающим, когда это вложенный объект, как этот:

interface IUser {    userName: string;    preferences: {        lastUpdated?: Date;        favoriteColor?: string;        backupContact?: string;        mailingAddress: {            street: string;            city: string;            state: string;            zipCode: string;        }     }}

Вы не захотите, чтобы сотни таких объектов возникали.

Так что если мы позволим пользователям переопределять только то, что они хотят, мы сможем создать действительно простой и базовый код DRY. Представьте себе, что есть очень специфический тест, в котором должен быть пользователь, живущий на "Main Street".

const userOnMainSt = makeFakeUser({    preferences: {        mailingAddress: {            street: 'Main Street'        }    }});

Ого, нужно было только указать то, что нужно для теста, а не другие 7 свойств. И нам не пришлось хранить одноразовый объект в каком-то огромном тестовом файле. И мы также достигли своих целей.

И как нам улучшить нашу функцию makeFakeUser для поддержки такого рода частичного переопределения?

Посмотрите, насколько легко это делает библиотека mergePartially (полное раскрытие: я сопровождающий mergePartially).

const makeFakeUser = (override?: NestedPartial<IDeepObj>): IDeepObj => {        const seed: IDeepObj = {          userName: 'Bob Smith',          preferences: {            mailingAddress: {              street: faker.address.streetAddress(),              city: faker.address.city(),              state: faker.address.stateAbbr(),              zipCode: faker.address.zipCode(),            },          },        };        return mergePartially.deep(seed, override);      };

Подведение итогов

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

Я был бы рад услышать от вас, что вы думаете об этом подходе.


Узнать больше о курсе JavaScript QA Engineer.

Зарегистрироваться на открытый вебинар на тему Что нужно знать о JS тестировщику.

Подробнее..

Внедрение E2E-тестирования с Puppeteer и Jest

18.02.2021 10:20:32 | Автор: admin

Привет, Хабр!

Хотим поделиться краткой историей о том, как мы на одном из проектов Рексофт пришли к написанию автотестов, и почему сделали акцент именно на e2e-тестах.

Речь пойдет о работе с порталом крупного федерального заказчика, работающего в сфере B2B и B2C, имеющего серьезный трафик из всех регионов. Сайт писал не Рексофт. Ресурс попал к нам на поддержку какое-то время назад. Изначально проект содержал довольно много легаси кода, тестов не было, как и документации. Требовалось сделать редизайн по новым макетам, параллельно расширяя существующую функциональность. Обычная история.

В какой-то момент мы поняли, что ручное прокликивание интерфейса стало занимать очень много времени, и решили, что пришло время для автотестов.

Перед выбором подхода к автотестам классифицировали баги по типу: их большая часть касалась именно бизнес-логики (Javascript). Багов, связанных с кроссбраузерностью или вёрсткой, было не очень много.

Итак, цель была поставлена автоматизировать проверку бизнес-логики, т.е. заполнение и отправку форм, работу слайдеров и т.д.

В качестве базового фреймворка для тестов был выбран Jest, а для имитации действий пользователя Puppeteer. Последний выбрали благодаря тому, что он поддерживается Google и имеет отличную документацию. Нам было вполне достаточно тестов в одном браузере (Chrome) (хотя, есть инструменты, позволяющие с помощью Puppeteer запускать тесты и в других браузерах, например, Firefox и IE). Playwright тогда ещё не было, а Selenium, чисто субъективно, казался более сложным в развертывании.

Далее кратко опишу преимущества и недостатки e2e-тестов.

Недостатки e2e-тестов

Относительно высокая сложность написания. Нужно обладать определёнными навыками, чтобы написать компактный и легкочитаемый e2e-тест.

Проблема закопаться. Я точно знаю, что желание проверить всё и вся, может привести к написанию огромного количества избыточных тестов. Как побороть эту проблему? Не стоит пытаться протестировать абсолютно все возможные кейсы, достаточно сосредоточиться на основных.

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

Хрупкость e2e-тестов. Что я имею ввиду? Случай, когда тест падает без видимых на то причин, а при повторном запуске успешно выполняется, или же когда к падению теста приводит незначительное изменение в верстке. С хрупкостью тестов можно бороться с помощью автоматического перезапуска теста после падения. Помогут в этом раннер jest-circus и параметр retryTimes. Это практически полностью решает проблему ложных падений тестов.

Преимущества e2e-тестов

Гарантии. С e2e-тестами вы получаете определенные гарантии того, что система в целом работает корректно. Юниты такой гарантии не дают. Разумеется, это не значит, что вы получаете 100% гарантию того, что у вас работает абсолютно всё. Но вы будете уверены, что по крайней мере протестированные сценарии точно работают.

Снижение рабочей нагрузки. Без тестов разработчик вынужден думать как бы чего не сломать. Ведь не прокликивать же весь проект после очередного коммита с правками! С тестами вы узнаете о поломке еще до того, как код будет залит на стенд.

Ускорение разработки. Благодаря снижению количества багов и устранению ручных проверок интерфейса, серьезно экономится время. Причём e2e-тесты экономят не только ваше время, но и время тестировщиков (позволяя им заводить меньше багов, меньше коммуницировать, выясняя их детали и т. д.), время аналитиков (понятные тесты отличная документация, отвечать на вопросы аналитиков вы будете намного быстрее).

Ускорение рефакторинга. При рефакторинге js, e2e-тесты не придется переписывать. Да и рефакторинг в вёрстке тоже редко приводит к их переписыванию.

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

Лайфхаки

1. Для ускорения написания первых тестов можно записывать действия пользователя с помощью Headless recorder или с помощью встроенных в Chrome инструментов.

2. Если требуется проверить несколько наборов данных в рамках одного кейса и руки тянутся написать for внутри теста, используйте jest-each. Эта библиотека позволит прогнать несколько наборов данных в одном кейсе, сохранив читаемость кода:

each` url | selector | expectedIsVisible ${'/page1.html?land'} | ${'.js-order-button'} | ${true} ${'/page1.html'} | ${'.js-order-button'} | ${false} ${'/page2.html?land'} | ${'.js-order-button'} | ${true} ${'/page2.html'} | ${'.js-order-button'} | ${false}`.it('Отображение кнопок заказа на странице $url', async ({ url, selector, expectedIsVisible }) => { await page.goto(`${SERVER_URL}${url}`, {   waitUntil: 'networkidle2',   timeout: 0 }); expect(await isVisible(page, selector)).toEqual(expectedIsVisible);});

3. В случае возникновения ошибок, puppeteer не всегда корректно подсвечивает строку с ошибкой. Поэтому следует проверить код перед ошибочной строкой, часто ошибка кроется именно там.

4. Puppeteer позволяет запускать браузер с различными флагами, что иногда очень полезно (chrome://flags/). Ниже пример запуска браузера с отключенным флагом SameSite by default cookies

puppeteer.launch({ headless: true, slowMo: 80, args: [   '--disable-features=SameSiteByDefaultCookies' ]});

В качестве заключения

Написание e2e-тестов занимают около 5-10% от времени написания фичи. На фиксы багов уходило сильно больше времени, так что оно того стоило. Несмотря на ряд недостатков, e2e-тесты позволяют значительно ускорить процесс разработки и обрести уверенность в том, что основная функциональность системы работает.

Разумеется, при внедрении e2e-тестов для фронтенда необходимо учитывать специфику вашего проекта. Спасибо за внимание!

Подробнее..

Перевод Как проводить сквозное(end-to-end) тестирование вашего приложения используя Cypress.io

04.05.2021 08:16:25 | Автор: admin
Изображение от https://unsplash.com/@kellysikkemaИзображение от https://unsplash.com/@kellysikkema

В этой статье вы узнаете:

  • Что такое Cypress и когда его стоит использовать

  • Основы тестирования с использованием Cypress

  • Расширенные команды Cypress

  • Взаимодействие с элементами пользовательского интерфейса

  • Лучшие практики с использованием Cypress


Введение

Чтобы протестировать свои приложения, вам потребуется сделать следующие шаги:

  • Запустить приложение

  • Подождать пока сервер запустится

  • Провести ручное тестирование приложения(нажать на кнопки, ввести случайные текст в поля ввода или отправить форму)

  • Проверить, что результат вашего теста корректен(изменения заголовка, части текста и т.д.)

  • Повторить эти шаги ещё раз после простых изменений кода

Повторение этих шагов снова и снова становится утомительным и отнимает у вас слишком много времени и энергии. Что, если бы мы могли автоматизировать этот процесс тестирования Благодаря этому вы можете сосредоточиться на более важных вещах и не тратить время на тестирование пользовательского интерфейса снова и снова.

Именно здесь в игру вступает Cypress. При использовании Cypress единственное, что вам нужно сделать, это:

  • Написать код вашего теста(нажатие на кнопку, ввод текста в поля ввода и т.п.)

  • Запустить сервер

  • Запустить или перезапустить тест

Только и всего!Библиотека Cypress выполняет все тесты за вас.И самое приятное, что она не только сообщает вам все ли ваши тесты успешны или нет, но также сообщает вам, какой тест не удался.

Помимо этого, тестирование вашего кода отличная практика, поэтому вам придется позже изучить соответствующий фреймворк.Cypress позволяет запускать тесты за считанные минуты.

Теперь, когда мы обсудили преимущества Cypress, давайте узнаем об основах этой библиотеки.


Начало

Установка и настройка Cypress

Сначала создайте отдельную папку для вашего проекта, а затем инициализируйте ее:

Инициализация проектаИнициализация проекта

Наконец, чтобы установить библиотеку Cypress:

Установка CypressУстановка Cypress

Примечание. Если вы используете дистрибутив Linux, перейдите кэтим инструкциям,прежде чем продолжить установку Cypress через NPM.

Теперь, когда Cypress установлен, попробуйте запустить его с помощью следующей команды:

Открытие CypressОткрытие Cypress

Она открывает запускалку тестов(Test Runner):

Интерфейс Test RunnerИнтерфейс Test Runner

А теперь давайте перейдём к написанию тестов.


Основы Cypress

Создание файла

Cypress требует, чтобы все наши тесты находились вкаталоге cypress/integration.Сначала перейдите в этот каталог:

Переход к cypress/integrationПереход к cypress/integration

Теперь создайте файл JavaScript с именемbasicTest.js:

Создание JavaScript файлаСоздание JavaScript файла

Если вы не отключили сервер Cypress, ваши новые файлы появятся в Test Runner в реальном времени:

Обновление структуры файлов в реальном времениОбновление структуры файлов в реальном времени

Теперь давайте напишем наш первый тест.

Простые тесты с утверждением и ожиданием значения

В вашем файле /cypress/integration/basicTest.js напишите следующий код:

Код к файлу basicTest.jsКод к файлу basicTest.js
  • Строка 1:Функция describe сообщает Cypress название набора наших тестов.

  • Строка 2: Функцияit, обозначает название теста.

  • Строка 3: Создаём утверждение.Здесь мы подтверждаем, что 2 + 2 равно 4. Если тест вернётfalse, то он будет немедленно остановлен.

Чтобы запустить вашу программу, щёлкните поbasicTest.js в вашем сервере Cypress.

Щелчок по basicTest.js в Test RunnerЩелчок по basicTest.js в Test Runner

Результат запуска:

Результат запуска тестаРезультат запуска теста

Отлично!Значит, наше утверждение было успешным.

Что, если мы сделаем заведомо ложное утверждение?Теперь в/cypress/integration/basicTest.js добавьте следующий код впределах функцииdescribe:

Код для добавление в basicTest.jsКод для добавление в basicTest.js
  • Строка 2: Если сумма 4 и 5 равна 10, тест будет пройден. В противном случае, незамедлительно остановлен.

Снова запустите код.Результат будет:

Результат нашего второго тестаРезультат нашего второго теста

Обратите внимание, как наш второй тест не удался.Если бы результат был правильным, тест прошел бы успешно.

Давайте больше поиграем с утверждениями.Добавьте в basicTest.jsследующий код:

Код для добавления в basicTest.jsКод для добавления в basicTest.js
  • Строка 2: Если сумма 5 и 5неравна 100, то тест должен пройти.

Результат выполнения теста:

Результат теста: успешно!Результат теста: успешно!

Отлично!Наш тест прошел. Функцияexpect выполняетBDD (behavior-driven) утверждения.В следующем разделе мы выполним утверждения, основанные на тестировании(test-driven assertions).

Сейчас/cypress/integration/basicTest.jsдолжен выглядеть так:

Написание утверждений основанных на тестировании(test-driven assertions) с явным использованием assert

Мы даже можем писать утверждения на основе TDD с использованиемassert.

В вашем файлеbasicTest.js напишите следующий код:

  • Строка 2: Создаём объект со свойствамиnameи age.

  • Строка 6: ФункцияisObjectподтверждает,что переменнаяpersonявляется объектом.Если результатtrue, то будет напечатаноvalue is object.В противном случае будет показано, что этот тест не прошел.

  • Строка 10: Убеждаемся, что переменнаяnameсодержитстроковое значение.

  • Строка 14: Убеждаемся, что переменнаяname неявляется целым числом.

Запустите код.Результатом будет:

Результат запуска нашего тестаРезультат запуска нашего теста

Отлично!Наш код работает.В следующем разделе мы научимся работать с сайтами через Cypress.

Сейчас нашbasicTest.jsдолжен выглядеть так:

Запуск веб-сайтов

Здесь мы попробуем запуститьDemoblaze, сайт, созданный для проведения тестов.

В своей папке/cypress/integration/ создайте файл с именемbasicCommandsTest.js.В этом файле напишите следующий код:

Код для basicCommandsTest.jsКод для basicCommandsTest.js
  • Строка 3: Используем методvisit, чтобы сообщить Cypress о переходе на веб-сайт Demoblaze.

Сохраните свой код и нажмите наbasicCommandsTest.js в меню Test Runner:

Клик по basicCommandsTest.js вTest RunnerКлик по basicCommandsTest.js вTest Runner

Результат запуска:

Отлично!Наш код работает.В следующем разделе мы более глубоко погрузимся в тестирование с помощью Cypress.

В итогеbasicCommandsTest.jsдолжен выглядеть так:


Cypress: Расширенные команды

В этом разделе мы попытаемся взаимодействовать с элементами на странице.Однако, прежде чем продолжить этот процесс, нам нужно сначала научиться идентифицировать элементы HTML в Cypress.

Как идентифицировать элементы

Cypress используетселекторы JQueryдля идентификации компонентов на веб-странице.

Например, чтобы получить элемент myButton используя id, мы должны написать следующий код:

Получение элемента через id элементаПолучение элемента через id элемента

В качестве альтернативы, чтобы идентифицировать элемент myButton используя имя класса, могли бы использовать следующую строку:

Получение элемента через имя классаПолучение элемента через имя класса

Давайте теперь поработаем с взаимодействием с пользовательским интерфейсом нашего сайта.

Нажатие кнопки

В этом разделе мы будем использоватьстраницу The-Internetдля запуска наших тестов.На этом веб-сайте мы будем использоватьразделдобавления/удаленияэлементов.

Давайте сначала попробуем идентифицировать нашу кнопку Добавить элемент.

Страница для тестированияСтраница для тестирования

Используя DevTools, заметьте, что уbuttonесть свойствоonclick, имеющее значениеaddElement().

Скриншот из DeveloperToolsСкриншот из DeveloperTools

Соответствующий селектордля этой кнопки будет выглядеть так:

Идентификация элементаИдентификация элемента

В папке/cypress/integration создайте файл с именемrunningClickCommand.js.В этом файле напишите следующий код:

  • Строка 2: Переходим на веб-страницу.

  • Строка 6: Связываем в одну цепочку получение элемента button и нажатие на эту кнопку.

Запустите код.Результат:

Вывод результатаВывод результата

Отлично, наш код работает!Обратите внимание, что как только страница загрузилась, в нашем тесте автоматически происходит нажатие на кнопкуAdd Element.

Давайте теперь поработаем с вводом текста в текстовое поле.

Ввод текста

В этом разделе мы будем использоватьстраницуThe-Internets login.Нам нужен способ сначала идентифицировать элементы.

Скриншот сайта для тестированияСкриншот сайта для тестированияСкриншот из DeveloperToolsСкриншот из DeveloperToolsСкриншот из DeveloperToolsСкриншот из DeveloperTools

Поле username имеетid равноеusername, а полеpassword имеетid равноеpassword.Кроме того, кнопка Login имеет свойствоtype равноеsubmit.Таким образом, для определения полейusername иpassword, нам понадобитсяселектор JQuery id:

Идентификация элемента через его idИдентификация элемента через его id

Более того, чтобы получить кнопкуbutton, нам понадобитсяселектор атрибутов, например:

В своей папке/cypress/integration создайте файл с именемrunningTypeCommand.js.В этом файле напишите следующий код:

  • Строка 3: Переходим на страницу входа в систему.

  • Строка 6: Переходим в полеusername и добавляем методtype вцепочку вызовов, чтобы напечатать в этом текстовом поле значениеtomsmith.

  • Строка 7: Переходим в полеpassword и вводимSuperSecretPassword.

  • Строка 10: Нажимаем на кнопку Отправить.

Запустите код.Результатом будет:

Вывод результата запуска кодаВывод результата запуска кода

И мы закончили!В следующем разделе мы узнаем о работе с чекбоксами.

Переключение чекбоксов

В этом разделе мы будем использоватьраздел чекбоксов на странице The-Internet.

Давайте сначала посмотрим на DevTools:

Developer ToolsDeveloper Tools

Оба этих чекбокса имеют свойствоtypeсо значениемcheckbox.Кроме того, они также являются дочерними элементамидля элементаform сid равнымcheckboxes.В этом случае мы бы использовалиселектор JQuery родитель-потомок:

Идентификация наших чекбоксовИдентификация наших чекбоксов

В каталоге/cypress/integration/ создайте файл с именемrunningCheckCommand.js и напишите следующий код:

  • Строка 4: Находим обе группы чекбоксов, а затем используем методcheck, чтобы отметить их выбранными.

  • Строка 7: Просим Cypress приостановить процесс тестирования на одну секунду.

  • Строка 8: Получаем список отмеченных чекбоксов.Затем используем методuncheck, чтобы снять выбор.

Запустите код.Результат:

Результат запуска тестаРезультат запуска теста

Отлично!Наш код работает.Давайте теперь поработаем над неявными утверждениями с помощью Cypress.

Неявные утверждения

Ранее мы выполняли утверждения для переменных и объектов.Однако в реальном мире мы хотели бы выполнять утверждения для текста, расположенного в нашем элементе HTML, или проверять, есть ли у нашего элементаul дочерние элементыli или нет.

Для выполнения таких утверждений мы будем использовать ключевое словоshould.В этом разделе мы будем делать неявные утверждения настраницедобавления элемента The-Internets Add Element

Скриншот тестируемой страницыСкриншот тестируемой страницыDeveloper ToolsDeveloper Tools

Наша кнопкаDelete имеет классadded-manually.Мы хотим выполнить следующее утверждение для этого элементаbutton:

Наше утверждениеНаше утверждение

Для этого мы должны использовать следующий синтаксис:

Получение элемента и за тем утверждениеПолучение элемента и за тем утверждение

Альтернативно, можем выполнить такое утверждение:

Наше утверждениеНаше утверждение

Мы можем использовать эту строку кода:

Получение элемента и затем утверждениеПолучение элемента и затем утверждение

Перейдите в/cypress/integration/runningClickCommand.jsи добавьте следующий код:

Код для runningClickCommand.jsКод для runningClickCommand.js
  • Строка 1: Получаем элементы с классомadded-manually.Затем проверяем, что их количество(have.length)равно единице.

  • Строка 3: Получаем кнопкуAdd Element, а затем проверяем, что текст на кнопке(have.text)действительно будетAdd Element.

Запустите код.Результат в конце теста должен быть следующим:

Результат запускаРезультат запуска

Отлично!Наш код работает.Теперь перейдем к изучению командыeach.

В итогеcypress/integration/runningClickCommand.js должен выглядеть так:

Команда each

Взглянитееще раз наThe-Internets Add Elements page:

Скриншот тестового сайтаСкриншот тестового сайта

Чтобы удалить все эти элементы, мы можем вручную прокликать все кнопкиDelete.Это возможно;однако рассмотрим ситуацию, когда кнопокDeleteбольше сотни.Следовательно, удаление всех их вручную займет много времени.

Вот здесь-то и появляется командаeach. Она позволяет вам перебирать серию элементов, а затем выполнять любую функцию для каждого из них.В этом случае мы хотим перебрать все наши кнопкиDelete и запустить функциюclick на всех них.

Откройте Developer Tools:

Все наши кнопкиDelete имеют свойствоclass равноеadded-manually.В этом случае мы будем использоватьселектор классови соединять его с командойeach, например:

Получение элемента и использование each затемПолучение элемента и использование each затем

Перейдите в/cypress/integration/runningClickCommand.jsи добавьте следующий фрагмент кода:

Код для runningClickCommand.jsКод для runningClickCommand.js
  • Строка 2: Получаем все элементы, у которых есть класс.added-manually.Каждый элемент в серии будет представлен параметром$el.

  • Строка 3: Обёртываем этот элемент, чтобы мы могли выполнять с ним команды Cypress.Здесь мы отправляем команду щёлкнуть по этим элементам.

Результат выполнения кода должен быть следующим:

Результат выполнения кодаРезультат выполнения кода

Наш код работает!Поскольку наш тест был быстрым, давайте попробуем добавить на страницу больше элементов.

Найдите следующий фрагмент кода:

Измените это так:

  • Строка 2: Запускаем цикл, чтобы сообщить Cypress, что нужно нажать кнопкуAdd Element 20 раз.

Запустите код еще раз.Результат должен быть таким:

Результат выполнения кодаРезультат выполнения кода

Как видите, наша программа автоматически удалила все элементы на странице без каких-либо ручных усилий.Отлично!

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

В итогеcypress/integration/runningClickCommand.jsдолжен выглядеть так:


Лучшие практики

Держите тесты изолированными

Рассмотрим ситуацию, когда вы тестируете свое приложение.Структура вашего проекта будет выглядеть примерно так:

Не самая лучшая структураНе самая лучшая структура

В вашем Test Runner это будет выглядеть так:

Отображение тестовой структуры в Test RunnerОтображение тестовой структуры в Test Runner

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

Хорошая структура проектаХорошая структура проекта

Следовательно, это выглядело бы так:

По возможности используйте собственные команды

Взгляните на этот фрагмент кода:

Пример кодаПример кода

Обратите внимание, что мы вынужденымногократноиспользоватькомандыgetиtype.Здесь мы можем реализоватьсобственные команды,чтобы сделать их короче.

В вашемcypress/support/commands.jsнапишите этот код:

  • Строка 1: Создаём собственную команду, которая будет иметь два параметраidentifier иdata.

  • Строка 2: Получаем элемент с соответствующим идентификатором, а затем вводим в него данные.

Примечание. Считается хорошей практикой записывать свои собственные команды в файл/cypress/support/commands.js.

Теперь вернитесь в свой тестовый файл и замените его вот так:

Пример кодаПример кода

Как видите, наш код выглядит значительно короче.

Избегайте атомарных тестов

Взгляните на этот фрагмент кода:

Здесь мы повторно выполняем тесты на HTML элементе сid равнымfirst.

Cypress не одобряет такого поведения.Это неэффективно, и есть способ лучше переписать этот код, например:

Мы можем использовать методand для связывания дополнительных командshould с нашим элементом.

Не запускайте сервер в Cypress

Команда execприсутствует для запуска команд в терминале.Но запускать сервер с помощью этой команды крайне не рекомендуется.

Если вы хотите протестировать свое приложение наlocalhost, сначала запустите сервер,а затемзапустите свой тест Cypress.

Команды терминалаКоманды терминала

Репозиторий GitHub и дополнительные ресурсы

Код GitHub

Дальнейшее чтение


Заключение

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

Спасибо, что дожили до конца!Если вы почувствовали какое-либо замешательство, я советую вам поиграть с кодом и разобрать примеры.

Подробнее..

Быстрый старт гайд по автоматизированному тестированию для Android-разработчика. JVM

14.12.2020 14:08:42 | Автор: admin

Привет! Меня зовут Сергей Иванов, я ведущий разработчик Android в Redmadrobot. С 2016 использую автотесты различных категорий и успел в этом набить немало шишек. Именно поэтому решил поделиться опытом. Возможно, что кому-то статья поможет систематизировать знания или начать применять эту практику в работе.

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

В статье подсвечу основные аспекты автоматизированного тестирования, его специфику на Android, дам рекомендации для решения популярных вопросов и эффективного внедрения практики на проекте то, к чему я сам пришел на текущий момент.

Подробнее расскажу про тесты на JVM, а не про UI-тесты, о которых в последнее время пишут часто. Материал будет хорошей отправной точкой для изучения темы, а также поможет дополнить уже имеющиеся знания.

Дисклеймер: статья получилась большой, поэтому указал основные темы, которые рассмотрю.

  • базовые понятие автоматизированного тестирования;

  • категории тестов их специфика на Android;

  • как писать тестируемый код;

  • как и какие инструменты использовать для тестирования;

  • как писать полезные и поддерживаемые тесты;

  • что тестировать;

  • как и когда применять методологию Test Driven Development.


При производстве приложений автотесты помогают:

  1. Находить баги на раннем этапе разработки. Это позволяет раньше устранять проблемы, при этом расходуя меньше ресурсов.

  2. Локализовать проблему. Чем более низкоуровневым является тест, тем более точно он способен указать на причину ошибки.

  3. Ускорить разработку. Это вытекает из предыдущих пунктов и из того, что благодаря автотестам разработка разных частей фичи может быть оперативно разделена на несколько разработчиков. Установив контракты между компонентами приложения, разработчик может разработать свой компонент и проверить его корректность при отсутствии остальных (например, при полном отсутствии UI).

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

Но есть и проблемы:

  1. Нужно время на внедрение, написание и поддержку.

  2. При некорректном внедрении практики могут принести больше вреда, чем пользы.

Важные базовые понятия автоматизированного тестирования

System Under Test (SUT) тестируемая система. В зависимости от типа теста системой могут быть разные сущности (о них подробнее написал в разделе категории тестов).

Для различия уровня тестирования по использованию знаний о SUT существуют понятия:

Black box testing тестирование SUT без знания о деталях его внутреннего устройства.

White box testing тестирование SUT с учётом деталей его внутреннего устройства.

Выделяют также Gray box testing, комбинацию подходов, но ради упрощения он будет опущен.

Для обеспечения базового качества автотестов важно соблюдать некоторые правила написания. Роберт Мартин сформулировал в книге "Clean Code" глобальные принципы F.I.R.S.T.

Fast тесты должны выполняться быстро.

Independent тесты не должны зависеть друг от друга и должны иметь возможность выполняться в любом порядке.

Repeatable тесты должны выполняться с одинаковым результатом независимо от среды выполнения.

Self-validating тесты должны однозначно сообщать о том, успешно их прохождение или нет.

Timely тесты должны создаваться своевременно. Unit-тесты пишутся непосредственно перед кодом продукта.

Структура теста состоит как минимум из двух логических блоков:

  • cовершение действия над SUT,

  • проверка результата действия.

Проверка результата заключается в оценке:

  • состояния SUT или выданного ею результата,

  • cостояний взаимодействующих с SUT объектов,

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

При необходимости также добавляются блоки подготовки и сброса тестового окружения, отчасти связанные с первыми тремя принципам F.I.R.S.T.

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

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

Зачастую для настройки окружения применяются тестовые дублеры.

Test doubles (Тестовые дублёры) фиктивные объекты, заменяющие реальные объекты, от которых зависит SUT, для достижения целей теста.

Тестовые дублеры позволяют:

  • зафиксировать тестовое окружение, имитируя неважные, нереализованные, нестабильные или медленные внешние объекты (например, БД или сервер),

  • совершать проверки своих вызовов (обращений к функциям, свойствам).

Самая популярная классификация включает 5 видов тестовых дублеров, различных по своим свойствам: Dummy, Fake, Stub, Spy, Mock.

Stub объект, который при вызовах его функций или свойств возвращает предустановленные (hardcoded) результаты, а не выполняет код реального объекта. Если же функция не имеет возвращаемого значения, то вызов просто игнорируется.

Mock объект, позволяющий проверять поведение SUT путём отслеживания обращений к функциям и свойствам объекта: были ли в ходе теста вызваны функции мока, в правильном ли порядке, ожидаемые ли аргументы были в них переданы и т.д. Может также включать функциональность Stub.

Почитать об этих и остальных видах дублеров можно в первоисточнике.

Эта классификация не является стандартом, и в фреймворках для создания тестовых дублёров часто ради удобства API несколько типов обобщают термином Mock. А вот чем они на самом деле будут являться, зависит от их последующей конфигурации и применения в тесте. Например, при использовании фреймворка Mockito, экземпляр тестового дублера может быть создан как Dummy, а потом превращен в Stub и в Mock.

При именовании созданных с помощью фреймворка дублеров уместно использовать именования, продиктованные фреймворком. Вообще, в мировом сообществе многие оперируют термином Mock и вне кода, подразумевая на самом деле дублёры разных типов. Бывает, что это путает. Но, в большинстве случаев в тестах используются стабы, а вовсе не моки.

В русскоязычной среде встречается мнение, что разница между Stub-ом и Mock-ом заключается в том, что первый это дублер, написанный вручную, а второй созданный с помощью специального фреймворка. Но это заблуждение.

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

Категории тестов

Есть разные версии категоризации тестов, по разным характеристикам, поэтомусуществует некоторая путаница.

Покажу основные категории уровней тестов, на которых тестируется система, на примере одного из самых распространенных вариантов пирамиды тестирования:

Unit-тесты проверяют корректность работы отдельного unit-а (модуля). Unit-ом (то есть SUT данного типа тестирования) может быть класс, функция или совокупность классов.

Integration-тесты (в приложении) проверяют корректность взаимодействия модулей или наборов этих модулей (компонентов). Определение SUT данной категории является еще более расплывчатым, т.к. в свою очередь зависит от того, что считается модулем.

Грань между Unit- и Integration-тестированием довольно тонкая. Интеграционными тестами, в зависимости от масштаба и контекста, в принципе могут называть тесты, проверяющие взаимодействие чего-либо с чем-либо с определенной долей абстракции: приложение(клиент)-сервер, приложение-приложение, приложение-ОС и др. Но в дальнейшем я буду говорить об интеграционном тестировании в рамках приложения.

End-to-end-тесты (E2E) интеграционные тесты, которые воздействуют на приложение и проверяют результат его работы через самый высокоуровневый интерфейс (UI), то есть на уровне пользователя. Использование тестовых дублеров на этом уровне исключено, а значит обязательно используются именно реальные сервер, БД и т.д.

Кстати, визуализация автоматизированных тестов в виде пирамиды говорит о том, что тесты более низкого уровня основа более высокоуровневых, а также о рекомендуемом количественном соотношении тестов того или иного уровня в проекте.

Вернёмся к категориям. В Android сложность категоризации автотестов усугубляется еще и тем, что они могут работать на JVM или в Instrumentation-среде (эмулятор или реальное устройство). Последние называют инструментальными.

Чтобы было удобнее ориентироваться в видах тестов, не путаясь в терминологии, предлагаю такую категоризацию для мобильного приложения на Android:

JVM Integration tests интеграционные тесты, проверяющие взаимодействие модулей или совокупностей модулей без использования Instrumentation. Характеризуются они высокой скоростью исполнения, сравнимой с Unit-тестами, также выполняющимися на JVM.

Instrumentation Integration non-UI tests интеграционные тесты, исполняемые уже в реальной Android-среде, но без UI.

Component UI tests интеграционные инструментальные тесты с использованием UI и фиктивных сервера и БД, если таковые требуются. Тест может состоять как из одного экрана, запущенного в изоляции, так и из нескольких экранов с соблюдением их реального флоу.

E2E UI tests интеграционные инструментальные UI-тесты без тестовых дублеров только с реальным флоу экранов. Максимально приближены к ручным тестам.

Если Unit-тесты являются сильно завязанными на детали реализации, очень быстро выполняются, относительно легко пишутся и наиболее точно при поломке указывают на причину ошибки, то в случае E2E UI ситуация противоположная. Изменение этих характеристик происходит постепенно от низа к верху пирамиды.

При переходе от тестов на JVM к тестам на Instrumentation из-за использования настоящей Android-среды происходит резкое падение скорости выполнения этих тестов. Это становится серьезным ограничением. Особенно когда тесты необходимо запускать часто и много раз подряд. Поэтому к написанию инструментальных тестов следует прибегать лишь в случаях, когда использование настоящих Android-зависимостей действительно необходимо.

UI-тесты

Несмотря на малую зависимость от низкоуровневых деталей реализации SUT, UI-тесты являются самыми хрупкими. Вызвано это их зависимостью от самого UI. Изменение разметки, реализации отображения, анимации и т.д. могут потребовать длительных манипуляций для обеспечения работоспособности теста.

Часто они оказываются нестабильны в своём поведении и могут то выполняться, то падать, даже если не вносилось никаких изменений в реализацию (нестабильные тесты называют Flaky). Мало того, UI-тесты могут совершенно по-разному себя вести на разных устройствах, эмуляторах и версиях Android. Когда же UI-тесты являются еще и E2E, добавляется хрупкость и снижается скорость выполнения из-за реальных внешних зависимостей. Причем в случае ошибки найти её причину бывает затруднительно, поскольку проверки в таких тестах осуществляются на уровне состояния UI. В таких ситуациях выгоднее обойтись силами QA-инженеров.

Конечно, UI-тесты способны приносить и весьма существенную пользу. Мобильные приложения имеют свойство разрастаться, и в какой-то момент их ручное регрессионное тестирование выходит за адекватные временные рамки. Тогда часть проверок может быть делегирована E2E UI-тестам, что при удачном исполнении может здорово сократить время тестирования.

Поэтому, для написания UI-тестов желательно иметь разработчиков или QA-инженеров-автоматизаторов, которые будут заниматься именно ими бльшую часть времени.

Unit-тесты

Unit-тесты тоже в определенной мере хрупкие, но уже из-за того, что они больше связаны с деталями реализации, которым свойственно периодически меняться. При сильном изменении реализации SUT и связанных с нею сущностей может потребоваться почти полностью переписать unit-тест. Но unit-тесты стабильны.

Степень хрупкости же можно снизить за счет использования black box-стиля написания даже на этом уровне, когда возможно. Но не следует злоупотреблять применением тестовых дублеров: если уже реализованная сущность имеет тривиальную логику или наличие логики не подразумевается, стоит использовать ее настоящую реализацию.

А заменять дублером следует только то, что действительно необходимо для приемлемой изоляции SUT в конкретном случае. Иногда (но далеко не всегда!) бывает оптимальнее сделать переиспользуемый рукописный дублер, чем конфигурировать его фреймворком для создания дублеров в множестве мест.

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

Подытожим

  • Как я отметил несколько пунктов назад: тесты более низкого уровня основа тестов более высокого уровня. Проверять высокоуровневыми тестами всё то, что спокойно проверяется низкоуровневыми, может быть слишком сложно, долго и невыгодно. Каждая категория тестов должна решать свою задачу и применяться на соответствующем этапе создания приложения чем выше уровень, тем позже.

  • Ручные тесты самые достоверные и важные тесты. Unit-тесты, имеющие меньше всего общего с ручными, могут позволить проверить такие ситуации, краевые кейсы, которые проверять вручную будет чрезвычайно дорого. Unit-тесты являются наиболее важными среди автоматизированных.

  • Лучше делать акцент на быстро выполняющиеся тесты. Так, после Unit-тестов рекомендую проверять JVM Integration-тестами интеграцию в том масштабе, который можно комфортно обеспечить без использования Instrumentation от ViewModel до слоя данных.

Дальше я буду говорить преимущественно о тестах на JVM. Но некоторые моменты актуальны и для остальных категорий.

Инструментарий

Раньше для написания JVM-тестов наши разработчики использовали фреймворки Junit 4 и Junit 5, но потом переключились на молодой перспективный Spek 2. Junit 4 нужен для инструментальных тестов с другими фреймворками они не работают.

Для проверок (assert) используем AssertJ отличную библиотеку с богатым набором читабельных ассертов и удобных дополнительных функций.

Для создания тестовых дублеров применяем Mockito-Kotlin 2 Mockito 2, адаптированный для Kotlin.

Для стаббинга и мокирования сервера MockWebServer библиотеку от Square, рассчитанную на работу с OkHttp.

Фреймворки PowerMock и Robolectric не используем из соображений скорости выполнения тестов и их надёжности. Кроме того, эти фреймворки поощряют плохо пахнущий код это дополнительные зависимости, без которых вполне можно обойтись. Для этого код должен быть тестируемым.

Дизайн кода

Признаки нетестируемого кода:

  • Наличие неявных зависимостей, сильная связанность. Это затрудняет изолированное unit-тестирование, тестирование на раннем этапе развития фичи, распараллеливание разработки. Использование статических функций, создание сложных объектов внутри класса, ServiceLocator исключают возможность использования тестовых дублеров.

  • Обилие Android-зависимостей. Они требуют Instrumentation или объемную подготовку среды на JVM с тестовыми дублерами, если их использование вообще возможно (см. прошлый пункт).

  • Наличие явного управления асинхронным и многопоточным поведением. Если результат работы SUT зависит от выполнения асинхронной работы, особенно порученной другому потоку (или нескольким), то не получится просто так гарантировать правильность и стабильность выполнения тестов. Тест может совершить проверки и завершиться раньше, чем асинхронная работа будет выполнена, и результат не будет соответствовать желаемому. При этом принудительное ожидание в тестах (в первую очередь на JVM) плохая практика, поскольку нарушается принцип Fast.

Пример
class ExampleViewModel constructor(val context: Context) : BaseViewModel() {    private lateinit var timer: CountDownTimer    fun onTimeAccepted(seconds: Long) {        val milliseconds = MILLISECONDS.convert(seconds, SECONDS)        // Неявная зависимость, Android-зависимость, запуск асинхронной работы        timer = object : CountDownTimer(milliseconds, 1000L) {            override fun onTick(millisUntilFinished: Long) {                showTimeLeft(millisUntilFinished)            }            override fun onFinish() {                // Неявная зависимость. Вызов статической функции с Android-зависимостью                WorkManager.getInstance(context)                    .cancelUniqueWork(SeriousWorker.NAME)            }        }        timer.start()    }

Как сделать код тестируемым

Следовать принципам SOLID, использовать слоистую архитектуру. Грамотное разделение и реализация сущностей позволит писать изолированные тесты именно на интересующую часть функционала, не допускать чрезмерного разрастания тестового файла и, при необходимости, осуществлять распараллеливание разработки. DI позволит подменять настоящие реализации тестовыми дублёрами.

Стремиться к чистоте функций. Это функции, которые:

  1. При одинаковом наборе входных данных возвращают одинаковый результат.

  2. Не имеют побочных эффектов, т.е. не модифицируют внешние переменные (класса, глобальные) и переданные в качестве входных данных параметры.

Пример теста такой функции:

val result = formatter.toUppercase("адвокат")assertThat(result).isEqualTo("АДВОКАТ")

Минимизировать количество Android-зависимостей. Часто прямое использование Android-зависимостей в SUT не является необходимым. Тогда их следует выносить вовне, оперируя в SUT типами, поддерживающимися на JVM.

Самая распространенная Android-зависимость в потенциально тестируемых классах ресурсы, и их выносить из, скажем, ViewModel, ну, совсем не хочется. В таком случае можно внедрить Resources во ViewModel, чтобы стаббить конкретные ресурсы (их id актуальны на JVM) и проверять конкретные значения:

mock<Resources> { on { getString(R.string.error_no_internet) } doReturn "Нет интернета" }

Но лучше поместить Resources во Wrapper, предоставляющий только необходимый функционал работы с ресурсами, и сделать его тестовую реализацию. Это избавит SUT от прямой зависимости от фреймворка и упростит подготовку окружения в тестах:

interface ResourceProvider {    fun getString(@StringRes res: Int, vararg args: Any): String}class ApplicationResourceProvider(private val resources: Resources) : ResourceProvider {    override fun getString(res: Int, vararg args: Any): String {        return resources.getString(res, *args)    }}class TestResourceProvider : ResourceProvider {    override fun getString(res: Int, vararg args: Any): String = "$res"}

При таком поведении TestResourceProvider по умолчанию правильность строки в ожидаемом результате можно сверять по id ресурса:

val string = TestResourceProvider().getString(R.string.error_no_internet)assertThat(string).isEqualTo(R.string.error_no_internet.toString())

В общем случае лучше вообще не заменять дублерами типы, принадлежащие сторонним библиотекам и фреймворкам. Это может привести к проблемам при обновлении их API. Обезопасить себя можно также с помощью Wrapper. Подробнее ситуация разобрана в статье Dont Mock Types You Dont Own.

Использовать Wrapper-ы для статический функций, управления асинхронным и многопоточным поведением. Существует немало стандартных статических функций или Android-зависимостей в виде таких функций. Если нужно иметь с ними дело, то следует помещать их во Wrapper-ы и внедрять в SUT для последующей подмены.

Это поможет и при работе с асинхронностью и многопоточностью: инкапсулирующий управление ими Wrapper можно заменить тестовым дублером, который позволит проверяемому коду выполняться в одном потоке и синхронно вызвать асинхронный код. Для RxJava и Kotlin Coroutines есть стандартные решения от их авторов.

Дизайн тестов

Важно оформлять тесты качественно. Иначе они помогут в момент написания, но в будущем будет уходить много времени на их понимание и поддержку.

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

Spoiler
public void testSubClassSerializerInvokedForBaseClassFieldsHoldingArrayOfSubClassInstances() {    Gson gson = new GsonBuilder()            .registerTypeAdapter(Base.class, new BaseSerializer())            .registerTypeAdapter(Sub.class, new SubSerializer())            .create();    ClassWithBaseArrayField target = new ClassWithBaseArrayField(new Base[] {new Sub(), new Sub()});    JsonObject json = (JsonObject) gson.toJsonTree(target);    JsonArray array = json.get("base").getAsJsonArray();    for (JsonElement element : array) {        JsonElement serializerKey = element.getAsJsonObject().get(Base.SERIALIZER_KEY);        assertEquals(SubSerializer.NAME, serializerKey.getAsString());    }}

Чтобы достичь желаемого эффекта от тестов, необходимо уделить внимание качеству их дизайна.

Наименование теста и разделение на блоки

Чтобы сделать содержимое теста более читабельным, его следует разделять на блоки соответствующих этапов. Я выбрал BDD-стиль, где есть этапы:

  • Given настройка SUT и среды;

  • When действие, инициирующее работу SUT, результат работы которой нужно проверить;

  • Then проверка результатов на соответствие ожиданиям.

Пример разделения тела теста:

@Testfun `when create - while has 1 interval from beginning of day and ending not in end of day - should return enabled and disabled items`() {    // given    val intervalStart = createDateTime(BEGINNING_OF_DAY)    val intervalEnd = createDateTime("2019-01-01T18:00:00Z")    val intervals = listOf(        ArchiveInterval(startDate = intervalStart, endDate = intervalEnd)    )    // when    val result = progressItemsfactory.createItemsForIntervalsWithinDay(intervals)    // then    val expected = listOf(        SeekBarProgressItem.createEnabled(intervalStart, intervalEnd),        SeekBarProgressItem.createDisabled(intervalEnd, createDateTime(END_OF_DAY))    )    assertThat(result).isEqualTo(expected)}

Лицо теста его название. Оно должно быть читабельным и ёмко передавать информацию о содержимом, чтобы для ориентации не приходилось каждый раз анализировать это самое содержимое.

В тестах на JVM Kotlin позволяет использовать пробел и дефис при обрамлении названия функции обратными кавычками. Это здорово повышает читабельность. В инструментальных тестах это не работает, поэтому текст пишется в CamelCase, а вместо дефисов используются нижние подчеркивания.

Для тестов на Junit применим следующий паттерн именования в простых случаях:

  • when - should

    when аналогично блоку When;

    should аналогично блоку Then.

В более сложных случаях, когда есть дополнительные условия:

  • when - while/and - should , где

    while предусловие до вызова целевой функции SUT;

    and условие после вызова функции SUT.

Пример:

@Testfun `when doesValueSatisfyRegex - while value is incorrect - should return false`() {

Так имя теста написано в виде требования, и в случае падения будет сразу видно, какой сценарий отработал некорректно:

Фреймворк Spek 2 выводит всё это на новый уровень. Он предоставляет из коробки DSL в стиле Gherkin (BDD).

object GetCameraGroupsInteractorTest : Spek({    Feature("Transform cached cameras to groups of cameras") {        ...        Scenario("subscribe while has non-grouped camera and unsorted by groups order cameras") {            ...            Given("non-grouped camera and unsorted by groups order cameras") {                ...            }            When("subscribe") {                ...            }            Then("should return four groups") {                ...            }            ...        }    }})

Блоки Given, When, Then подтесты глобального теста, описанного с помощью блока Scenario. Теперь нет необходимости ставить всё описание в названии, можно просто расположить все части в соответствующих блоках.

Результат выполнения имеет иерархический вид:

Эти блоки могут присутствовать внутри Scenario в любом количестве, а для придания еще более человекочитаемого вида можно использовать блок And. Теперь насыщенный сценарий можно оформить, не растянув при этом название теста далеко за границу экрана:

Благодаря блокам типа Feature можно удобно разделять тесты для разных фич, если в одном тестовом файле их несколько.

Чтобы добиться схожего разделения и отображения результатов с помощью Junit 5, понадобилось бы написать в тестах много бойлерплейта с аннотациями.

Устранение лишнего кода

Чтобы сделать содержимое тестов читабельнее, нужно следовать нескольким правилам:

1. Если проверки результатов выполнения одного действия над SUT тесно связаны, допустимо иметь несколько проверок в тесте. В противном случае это должны быть отдельные тесты. Основная проблема в том, что если в тесте несколько проверок и одна из них фейлится, то последующие проверки осуществлены не будут.

В Spek 2 вместо создания полностью отдельных тестов, если они концептуально относятся к одному сценарию, разделение проверок можно сделать с помощью блоков Then/And внутри Scenario:

...Then("should return four groups") {...}And("they should be alphabetically sorted") {...}And("other group should contain one camera") {...}And("other group should be the last") {...}...

В Junit 4 такой возможности нет. На помощь приходит механизм SoftAssertions из AssertJ, который гарантирует выполнение всех assert в тесте. Например:

// thenassertSoftly {    it.assertThat(capabilityState)        .describedAs("Capability state")        .isInstanceOf(Available::class.java)    it.assertThat((capabilityState as Available).disclaimer)        .describedAs("Disclaimer")        .isNull()}

2. Если проверки объемные, нежелательные к разделению и повторяющиеся, следует выносить их в отдельную функцию с говорящим названием.

3. Использовать обобщающие конструкции тестового фреймворка для одинаковой настройки окружения, если настройка повторяется для большого количества тестов, находящихся на одном уровне иерархии (например, beforeEachScenario и afterEachScenario в случае Spek 2). Если настройка одинакова для нескольких тестовых файлов, можно использовать Extension для Junit 5, Rule для Junit 4, а для Spek 2 подобного механизма из коробки нет, поэтому нужно обходиться конструкциями before/after.

4. Объемные схожие настройки тестового окружения следует также выносить в отдельную функцию.

5. Использовать статические импорты для повсеместно применяемых функций вроде функций проверок AssertJ и Mockito.

6. Если создание вспомогательных объектов объемное, используется в разных тестовых файлах и с разными параметрами, следует завести генератор с дефолтными значениями:

Пример генератора
object DeviceGenerator {    fun createDevice(        description: String? = null,        deviceGroups: List<String> = emptyList(),        deviceType: DeviceType = DeviceType.CAMERA,        offset: Int = 0,        id: String = "",        photoUrl: String? = null,        isActive: Boolean = false,        isFavorite: Boolean = false,        isPublic: Boolean = false,        model: String? = null,        vendor: String? = null,        title: String = "",        serialNumber: String = "",        streamData: StreamData? = null    ): Device {        return Device(            description = description,            deviceGroups = deviceGroups,            deviceType = deviceType,            offset = offset,            id = id,            photoUrl = photoUrl,            isActive = isActive,            isFavorite = isFavorite,            isPublic = isPublic,            model = model,            vendor = vendor,            title = title,            serialNumber = serialNumber,            streamData = streamData        )    }}Given("initial favorite camera") {    val devices = listOf(        createDevice(id = deviceId, isFavorite = true)    )    ...}

Очень важно не переборщить с созданием вспомогательных функций и архитектурных изысков, поскольку KISS и единообразие в автотестах важнее, чем DRY. Когда все тесты в проекте написаны однотипно и прозрачно, они гораздо лучше воспринимаются.

Тесты как документация

Когда предыдущие пункты соблюдены, тесты уже можно применять как документацию, свернув тестовые функции в IDE.

Для сворачивания и разворачивания всех блоков кода в файле в случае Mac используются комбинации клавиш Shift + + - и Shift + + +, для управления конкретным блоком + - и + + соответственно.

В тестах на Junit 4 можно сделать еще лучше, сгруппировав тесты по регионам, ведь их тоже можно сворачивать.

Пример

В тестах на Spek 2 нет нужды делать разделение тестов по регионам, поскольку их можно хорошо сгруппировать с помощью блоков Scenario и Feature.

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

Наконец пример тестов на Spek 2 в режиме документации

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

Она лучше обычной текстовой, поскольку в отличие от тестов, обычную документацию можно забыть актуализировать. Чем тесты более высокоуровневые, тем более близкими к составленным аналитиком функциональным требованиям будут их названия. Это будет заметно в разделе "JVM Integration Testing".

Параметрические тесты

Если нужно протестировать корректность работы SUT с разнообразным набором входных данных, но при этом основная реализация тестов меняться не должна, можно использовать параметрический тест.

Он может быть запущен много раз, каждый раз принимая разные аргументы. Поэтому отпадает надобность писать множество одинаковых тестов, у которых отличаются только входные данные и ожидаемый результат. Достаточно написать один тест и указать набор данных, которые будут поочередно в него передаваться. Часто параметрические тесты оказываются подходящим выбором для тестирования валидаторов, форматтеров, конвертеров и т.д.

В документации Spek 2 не написано о возможности написания параметрических тестов, хотя она есть, и писать их проще, чем в Junit 4 и Junit 5. Для этих целей удобно использовать стиль тестов Specification.

Пример параметрического теста в Speck 2
class OrientationTypeTest : Spek({    describe("Orientation type") {        mapOf(            -1 to Unknown,            -239 to Unknown,            361 to Unknown,            2048 to Unknown,            340 to Portrait,            350 to Portrait,            360 to Portrait,            0 to Portrait,            ...        ).forEach { (tiltAngle, expectedOrientation) ->            describe("get orientation by tilt angle $tiltAngle") {                val result = OrientationType.getOrientation(tiltAngle)                it("return $expectedOrientation type") {                    assertThat(result).isEqualTo(expectedOrientation)                }            }        }    }})

Результат выполнения:

Снижение хрупкости non-UI тестов

Я писал, что степень хрупкости unit-тестов при изменениях исходного кода, обусловленную их привязкой к деталям реализации модуля, можно снизить. Это применимо для всех non-UI тестов.

Написание тестов в стиле White box искушает расширять видимость функций/свойств SUT для проверок или установки состояний. Это простой путь, который влечет за собой не только увеличение хрупкости тестов, но и нарушение инкапсуляции SUT.

Избежать этого помогут правила. Можно сказать, что взаимодействие с SUT будет в стиле Black box.

  1. Тестировать следует только публичные функции. Если SUT имеет приватную функцию, логику которой нужно протестировать, делать это следует через связанную с ней публичную функцию. Если сделать это проблематично, то, возможно, код приватной функции так сложен, что должен быть вынесен в отдельный класс и протестирован напрямую.

  2. Нужно стараться делать функции чистыми. Об этом я говорил выше.

  3. Проверки в тесте следует осуществлять по возвращаемому значению вызываемой публичной функции, публичным свойствам или, в крайнем случае, по взаимодействию с mock-объектами (с помощью функции verify() и механизма ArgumentCaptor в Mockito)

  4. Делать только необходимые проверки в рамках теста. Например, если в тесте проверяется, что при вызове функции A у SUT происходит вызов функции X у другого класса, то не следует до кучи проверять значения её публичных полей, особо не имеющих отношения к делу, и что у SUT не будет более никаких взаимодействий с другими функциями связанного класса (функция verifyNoMoreInteractions() в Mockito).

  5. Если для проведения определенного теста невозможно привести SUT в требуемое предварительное состояние с помощью аргументов целевой функции, моков/стабов или изменения полей, то следует вызвать другие публичные функции, вызов которых приводит SUT в интересующее состояние в условиях реальной работы приложения. Например, вызвать функции onLoginInputChanged и onPasswordInputChanged для подготовки теста onEnterButtonClick во ViewModel

Существует аннотация-маркер @VisibleForTesting для выделения функций/свойств, модификатор доступа которых расширили для тестирования. Благодаря этому маркеру Lint подскажет разработчику, обратившемуся к функции/свойству в таком месте исходного кода, в котором они на самом деле не должны быть доступны, что видимость функции расширена только для тестирования. Несмотря на возможность использования такого маркера, прибегать к расширению видимости всё равно не рекомендуется.

Тестирование асинхронного кода с RxJava

Лучше избегать прямого управления асинхронным и многопоточным поведением в SUT. Для тестирования же кода, использующего RxJava или Coroutines, применяются специфичные решения. Сейчас в большинстве наших проектов используется RxJava, поэтому расскажу именно про нее.

Для тестирования SUT, осуществляющей планирование Rx-операций, нужно произвести замену реализаций Scheduler-ов так, чтобы весь код выполнялся в одном потоке. Также важно иметь в виду, что на JVM нельзя использовать AndroidSchedulers.mainThread().

В большинстве случаев все Scheduler-ы достаточно заменить на Schedulers.trampoline(). В случаях, когда нужен больший контроль над временем события, лучше использовать io.reactivex.schedulers.TestScheduler с его функциями triggerActions(), advanceTimeBy(), advanceTimeTo().

Замену реализаций можно совершить двумя способами:

  • RxPlugins (RxJavaPlugins & RxAndroidPlugins);

  • Подход Schedulers Injection.

Первый способ официальный и может быть применен независимо от того, как спроектирована SUT. Он имеет не самое удачное API и неприятные нюансы работы, усложняющие применение в некоторых ситуациях (например, когда внутри тестового файла в одних тестах нужно использовать Schedulers.trampoline(), а в других TestScheduler).

Суть подхода Schedulers Injection заключается в следующем: экземпляры Scheduler-ов попадают в SUT через конструктор, благодаря чему в тесте они могут быть заменены на иные реализации. Этот подход является очень прозрачным и гибким. Также он останется неизменным независимо от выбранного тестового фреймворка (Junit 4, Junit 5, Spek 2) чего нельзя сказать об RxPlugins, которыми придется в каждом управлять по-своему.

Из минусов Shedulers Injection можно выделить необходимость внедрения дополнительного аргумента в SUT и необходимость использования вместо rx-операторов с Sheduler по умолчанию (таких как delay()) их перегруженные варианты с явным указанием Scheduler.

Есть две неплохие статьи на тему обоих подходов: раз, два. Но там упомянуты не все нюансы RxPlugins.

Я предпочитаю второй подход. Чтобы упростить внедрение и подмену реализаций в тесте, я написал SchedulersProvider:

Реализация и применение SchedulersProvider
interface SchedulersProvider {    fun ui(): Scheduler    fun io(): Scheduler    fun computation(): Scheduler}class SchedulersProviderImpl @Inject constructor() : SchedulersProvider {    override fun ui(): Scheduler = AndroidSchedulers.mainThread()    override fun io(): Scheduler = Schedulers.io()    override fun computation(): Scheduler = Schedulers.computation()}fun <T> Single<T>.scheduleIoToUi(schedulers: SchedulersProvider): Single<T> {    return subscribeOn(schedulers.io()).observeOn(schedulers.ui())}// другие необходимые функции-расширения...

Его применение в коде:

class AuthViewModel(    ...    private val schedulers: SchedulersProvider) : BaseViewModel() {    ...    loginInteractor        .invoke(login, password)        .scheduleIoToUi(schedulers)    ...

А вот и его тестовая реализация с Scheduler-ами по умолчанию, вместо которых при надобности можно передать TestScheduler:

class TestSchedulersProvider(    private val backgroundScheduler: Scheduler = Schedulers.trampoline(),    private val uiScheduler: Scheduler = Schedulers.trampoline()) : SchedulersProvider {    override fun ui(): Scheduler = uiScheduler    override fun io(): Scheduler = backgroundScheduler    override fun computation(): Scheduler = backgroundScheduler}

Применение в тесте:

authViewModel = AuthViewModel(    ...    router = mock(),    schedulers = TestSchedulersProvider(),    loginInteractor = loginInteractor,    ...)

Вообще, RxJava из коробки имеет и другие полезные инструменты для тестирования (TestObserver, TestSubscriber), но они не входят в рамки статьи.

JVM Integration Testing

JVM Integration-тесты проверяют взаимодействие модулей или совокупностей модулей на JVM. Какие именно связки стоит тестировать, зависит от конкретных случаев.

В самых масштабных тестах этого типа проверяется взаимодействие всей цепочки модулей от ViewModel до Http-клиента, поскольку в этом промежутке обычно располагается основная логика, требующая проверки. Обеспечивать работу View на JVM обычно накладно и не имеет большого смысла.

Тест взаимодействует с SUT через ViewModel, инициируя действия и проверяя результат.

Чтобы достичь максимальной степени проверки SUT в данном случае следует заменять тестовыми реализациями только те сущности, которые действительно в этом нуждаются. Типичный набор таких сущностей:

  • android.content.res.Resources или собственный Wrapper. Обычно достаточно стаба, обеспечивающего исправный возврат строк из ресурсов.

  • androidx.arch.core.executor.TaskExecutor. Требуется в любых тестах на JVM, у которых SUT использует LiveData, поскольку стандартная реализация имеет Android-зависимость. Подробнее можно почитать в этой статье. Google предлагает готовое решение этой проблемы в форме Rule лишь для Junit 4, поэтому для Spek 2 и Junit 5 использую рукописный класс, содержащий код из того самого решения:

object TestLiveDataExecutionController {    fun enableTestMode() {        ArchTaskExecutor.getInstance()            .setDelegate(object : TaskExecutor() {                override fun executeOnDiskIO(runnable: Runnable) = runnable.run()                override fun postToMainThread(runnable: Runnable) = runnable.run()                override fun isMainThread(): Boolean = true            })    }    fun disableTestMode() {        ArchTaskExecutor.getInstance().setDelegate(null)    }}

Соответствующие функции достаточно вызывать перед первым и после последнего теста в тестовом файле. Пример применения в Spek 2:

object DeviceDetailViewModelIntegrationTest : Spek({    beforeGroup { TestLiveDataExecutionController.enableTestMode() }    afterGroup { TestLiveDataExecutionController.disableTestMode() }...
  • Сервер. Для имитации сервера используется MockWebServer от создателей OkHttp. Он позволяет предустанавливать ответы на конкретные запросы, проверять состав запросов, факты их вызова и др.

  • Interceptors с Android-зависимостями. Не следует пренебрегать добавлением интерцепторов в тестовую конфигурацию клиента OkHttp, соблюдая тот же порядок, что и в настоящем клиенте, чтобы серверные запросы и ответы правильно обрабатывались. Однако некоторые интерцепторы могут иметь Android-зависимости их следует подменить. Например, это могут быть интерцепторы логирования. Интерцепторы последовательно передают данные друг другу и эту цепочку нельзя прерывать, поэтому фиктивный интерцептор должен выполнять это минимальное требование:

// StubInterceptorInterceptor { chain ->    return@Interceptor chain.proceed(chain.request().newBuilder().build())}
  • Персистентные хранилища данных (SharedPreferences, Room и т.д.)

Базовая логика управления тестовым сетевым окружением сконцентрирована в классе BaseTestNetworkEnvironment. Он используется на JVM и в Instrumentation. За специфическую конфигурацию под каждую из сред отвечают его классы-наследники: JvmTestNetworkEnvironment и InstrumentationTestNetworkEnvironment.

Сервер запускается при создании экземпляра *NetworkEnvironment до запуска теста и отключается функцией shutdownServer() после завершения теста (в случае Gherkin-стиля Spek 2 до и после Scenario соответственно).

Для удобной настройки ответов на конкретные запросы используется функция dispatchResponses. При необходимости к mockServer можно обратиться напрямую.

Реализация BaseTestNetworkEnvironment
abstract class BaseTestNetworkEnvironment {    companion object {        private const val BASE_URL = "/"        private const val ENDPOINT_TITLE = "Mock server"    }    val mockServer: MockWebServer = MockWebServer().also {         it.startSilently()     }    // класс, специфичный для инфраструктуры проекта    protected val mockNetworkConfig: NetworkConfig    init {        val mockWebServerUrl = mockServer.url(BASE_URL).toString()        mockNetworkConfig = TestNetworkConfigFactory.create(mockWebServerUrl, BASE_URL)    }    /**     * Используется для предустановки фиктивных ответов на конкретные запросы к [MockWebServer].     *     * [pathAndResponsePairs] пара путь запроса - ответ на запрос.     *     * Если [MockWebServer] получит запрос по пути, которого нет среди ключей [pathAndResponsePairs],     * то будет возвращена ошибка [HttpURLConnection.HTTP_NOT_FOUND].     */    fun dispatchResponses(vararg pathAndResponsePairs: Pair<String, MockResponse>) {        val pathAndResponseMap = pathAndResponsePairs.toMap()        val dispatcher = object : Dispatcher() {            override fun dispatch(request: RecordedRequest): MockResponse {                val mockResponse = request.path?.let {                   pathAndResponseMap[it]                 }                return mockResponse ?: mockResponse(HttpURLConnection.HTTP_NOT_FOUND)            }        }        mockServer.dispatcher = dispatcher    }    fun shutdownServer() {        mockServer.shutdown()    }    /**     * Запуск сервера с отключенными логами     */    private fun MockWebServer.startSilently() {        Logger.getLogger(this::class.java.name).level = Level.WARNING        start()    }}

Содержимое JvmTestNetworkEnvironment уже сильно зависит от специфики конкретного проекта, но цель его неизменна заменить некоторые сущности локального сетевого окружения тестовыми дублерами, чтобы код работал на JVM.

Пример реализации JvmTestNetworkEnvironment
// Если не передавать в конструктор класса специфические экземпляры тестовых дублеров, то будут использоваться// стабы с минимальным предустановленным поведением, необходимым для функционирования сетевого флоу.class JvmTestNetworkEnvironment(    val mockPersistentStorage: PersistentStorage = mockPersistentStorageWithMockedAccessToken(),    val mockResources: ResourceProvider = TestResourceProvider()) : BaseTestNetworkEnvironment() {    private val nonAuthZoneApiHolderProvider: NonAuthZoneApiHolderProvider    private val authZoneApiHolderProvider: AuthZoneApiHolderProvider    init {        val moshiFactory = MoshiFactory()        val serverErrorConverter = ServerErrorConverter(moshiFactory, mockResources)        val stubInterceptorProvider = StubInterceptorProvider()        val interceptorFactory = InterceptorFactory(            ErrorInterceptorProvider(serverErrorConverter).get(),            AuthInterceptorProvider(mockPersistentStorage).get(),            stubInterceptorProvider.get(),            stubInterceptorProvider.get()        )        nonAuthZoneApiHolderProvider = NonAuthZoneApiHolderProvider(            interceptorFactory,            moshiFactory,            mockNetworkConfig        )        authZoneApiHolderProvider = AuthZoneApiHolderProvider(            interceptorFactory,            moshiFactory,            UserAuthenticator(),            mockNetworkConfig        )    }    fun provideNonAuthZoneApiHolder() = nonAuthZoneApiHolderProvider.get()    fun provideAuthZoneApiHolder() = authZoneApiHolderProvider.get()}

Функции для упрощения создания серверных ответов:

fun mockResponse(code: Int, body: String): MockResponse = MockResponse().setResponseCode(code).setBody(body)fun mockResponse(code: Int): MockResponse = MockResponse().setResponseCode(code)fun mockSuccessResponse(body: String): MockResponse = MockResponse().setBody(body)

Тела фиктивных серверных ответов сгруппированы по object-ам, соответствующим разным запросам. Это делает тестовые файлы чище и позволяет переиспользовать ответы и значения их полей в разных тестах. Одни и те же ответы используются тестами на JVM и Instrumentation (в том числе UI).

После добавления комментария "language=JSON" IDE подсвечивает синтаксис JSON. Подробнее о Language injections можно почитать тут.

С помощью выноса значений интересующих полей ответов в константы, можно использовать их при проверках в тестах, не дублируя строки. Например, убедиться, что описание ошибки из серверного ответа корректно передано в Snackbar. Тело ответа получается посредством вызова функции с параметрами, которые при надобности позволяют конфигурировать ответ из теста.

Пример object с фиктивными серверными ответами
object LoginResponses {    const val INVALID_CREDENTIALS_ERROR_DESCRIPTION = "Неверный логин или пароль"        fun invalidCredentialsErrorJson(        errorDescription: String = INVALID_CREDENTIALS_ERROR_DESCRIPTION    ): String {        // language=JSON        return """            {              "error": {                "code": "invalid_credentials",                "description": "$errorDescription",                "title": "Введены неверные данные"              }            }            """.trimIndent()    }...}

Схожим образом вынесены и пути запросов:

const val LOGIN_REQUEST_PATH = "/auth/login"object GetCameraRequest {    const val DEVICE_ID = "1337"    const val GET_CAMERA_REQUEST_PATH = "/devices/camera/$DEVICE_ID"}...

Общие для JVM и Instrumentation файлы должны находиться в директории, доступной обоим окружениям. Доступ настраивается в build.gradle:

android {    sourceSets {        // Instrumentation        androidTest {            java.srcDirs += 'src/androidTest/kotlin'            java.srcDirs += 'src/commonTest/kotlin'        }        // JVM        test {            java.srcDirs += 'src/test/kotlin'            java.srcDirs += 'src/commonTest/kotlin'        }    }}

Взаимодействие View и ViewModel построено особым способом, благодаря которому очень удобно писать unit-тесты ViewModel и integration-тесты. Публичные функции ViewModel представляют события со стороны View (обычно они соответствуют действиям со стороны пользователя) и именуются в событийном стиле:

ViewModel воздействует на View посредством двух LiveData:

  • state описание состояния View

  • events однократные события, не сохраняющиеся в state

Этот подход в более удобном виде реализован в нашей библиотеке.

Пример организации ViewModel, ViewState и ViewEvents
class AuthViewModel(...) {    val state = MutableLiveData<AuthViewState>()    val events = EventsQueue<ViewEvent>()    ...}sealed class AuthViewState {    object Loading : AuthViewState()    data class Content(        val login: String = "",        val password: String = "",        val loginFieldState: InputFieldState = Default,        val passwordFieldState: InputFieldState = Default,        val enterButtonState: EnterButtonState = Disabled    ) : AuthViewState() {        sealed class InputFieldState {            object Default : InputFieldState()            object Error : InputFieldState()            object Blocked : InputFieldState()        }...    }}class EventsQueue<T> : MutableLiveData<Queue<T>>() {    fun onNext(value: T) {        val events = getValue() ?: LinkedList()        events.add(value)        setValue(events)    }}// ViewEvents:interface ViewEventdata class ShowSnackbarError(val message: String) : ViewEventclass OpenPlayStoreApp : ViewEvent...
Наконец, пример JVM Integration-теста
object AuthViewModelIntegrationTest : Spek({    Feature("Login") {        // region Fields and functions        lateinit var authViewModel: AuthViewModel        lateinit var networkEnvironment: JvmTestNetworkEnvironment        val login = "log"        val password = "pass"        fun setUpServerScenario() {            networkEnvironment = JvmTestNetworkEnvironment()            val authRepository = networkEnvironment.let {                AuthRepositoryImpl(                    nonAuthApi = it.provideNonAuthZoneApiHolder(),                    authApi = it.provideAuthZoneApiHolder(),                    persistentStorage = it.mockPersistentStorage,                    inMemoryStorage = InMemoryStorage()                )            }            val clientInfo = ClientInfo(...)            val loginInteractor = LoginInteractor(authRepository, clientInfo)            authViewModel = AuthViewModel(                resources = networkEnvironment.mockResources,                schedulers = TestSchedulersProvider(),                loginInteractor = loginInteractor                analytics = mock()            )        }        beforeFeature { TestLiveDataExecutionController.enableTestMode() }        afterFeature { TestLiveDataExecutionController.disableTestMode() }        beforeEachScenario { setUpServerScenario() }        afterEachScenario { networkEnvironment.shutdownServer() }        // endregion        Scenario("input credentials") {...}        Scenario("click enter button and receive invalid_credentials error from server") {            Given("invalid_credentials error on server") {                networkEnvironment.dispatchResponses(                    LOGIN_REQUEST_PATH to mockResponse(HTTP_UNAUTHORIZED, invalidCredentialsErrorJson())                )            }            When("enter not blank credentials") {                authViewModel.onCredentialsChanged(login, password)            }            And("click enter button") {                authViewModel.onEnterButtonClick(login, password)            }            Then("reset password, mark login and password input fields as invalid and disable enter button") {                val state = authViewModel.state.value                val expectedState = Content(                    login = login,                    password = "",                    loginFieldState = Content.InputFieldState.Error,                    passwordFieldState = Content.InputFieldState.Error,                    enterButtonState = Content.EnterButtonState.Disabled                )                assertThat(state).isEqualTo(expectedState)            }            And("create snackbar error event with message from server") {                val expectedEvent = authViewModel.events.value!!.peek()                assertThat(expectedEvent).isEqualTo(ShowSnackbarError(INVALID_CREDENTIALS_ERROR_DESCRIPTION))            }        }        ...    }    ...})

Так тестируется основная логика пользовательских сценариев. Эти сценарии с теми же данными затем могут быть проверены в UI-тестах.

Что в итоге нужно тестировать?

Не нужно тестировать чужие библиотеки это ответственность разработчиков библиотек (исследовательское тестирование исключение). Тестировать нужно свой код.

Unit-тесты следует писать на логику, в которой есть реальная вероятность совершения ошибки. Это могут быть ViewModel, Interactor, Repository, функции форматирования (денег, дат и т.д.) и другие стандартные и нестандартные сущности. Тривиальную логику тестировать не стоит. Но нужно следить за изменением непокрытой тестами логики, если она при очередном изменении перестанет быть тривиальной, то тогда её нужно протестировать.

100%-е покрытие кода тестами несёт с собой вред: трата лишнего времени на написание бесполезных тестов, боль при изменении реализации, при поддержке бесполезных тестов, иллюзия хорошо протестированной системы. Процент покрытия не отражает реальной картины того, насколько хорошо система протестирована.

Я предпочитаю не устанавливать минимальный порог тестового покрытия для нового кода. Однако для разработчика всё же может быть полезной точечная проверка покрытия SUT, над которой он работает.

Тестами нужно проверять не только основные сценарии, но и краевые. Обычно там кроется наибольшее число багов.

При исправлении бага в коде, который не покрыт тестами, следует его ими покрыть.

JVM Integration-тесты от ViewModel до слоя данных следует писать для каждого экрана. Менее масштабные JVM Integration при надобности. Возможны случаи, когда большинство модулей, включая ViewModel, сами по себе являются слишком простыми, чтобы их стоило покрывать unit-тестами. Однако создание масштабного JVM integration-теста на всю цепочку будет очень кстати, тем более что пишутся такие тесты достаточно просто и однотипно.

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

Тесты Instrumentation Integration non-UI только когда нужно проверить что-то, что нельзя адекватно проверить на JVM.

E2E UI- и Component UI-тесты нужны для замены части ручных тестов при регрессионном тестировании. Разумно доверить их написание QA-инженерам. В настоящее время мы с коллегами ищем оптимальный подход к тому, как организовывать UI-тесты, в каком количестве их писать и как сочетать с более низкоуровневыми тестами.

Test Driven Development

Можно подумать, что о написании тестов уже известно достаточно и пора идти в бой, но есть еще один момент Вы, вероятно, собрались написать очередную фичу и затем покрыть её тестами? Замечательная идея. Именно так и стоит делать, пока навык написания тестов не будет более менее отработан. Такой подход называют Test Last. Конечно же, среди пишущих тесты разработчиков он наиболее распространен. Но он имеет серьезные недостатки:

  • несмотря на знания о том, каким должен быть тестируемый код, всё равно может получиться реализация, протестировать которую сходу не получится. Может понадобиться рефакторинг, чтобы появилась возможность написать тесты и сделать их не корявыми :)

  • по моим наблюдениям, при покрытии кода тестами, разработчику свойственно подстраивать тесты под существующую реализацию, которая уже засела у него в голове. Поэтому вероятность упустить какой-то кейс возрастает. И это чревато пропущенными багами.

  • тесты остаются на последнюю очередь и на них зачастую не остается времени.

Решить эти проблемы можно, используя принцип Test First, придуманным Кентом Беком. Он основан на идее: "Never write a single line of code unless you have a failing automated test" (не стоит писать код реализации, пока для него не написан падающий тест).

На базе этого принципа Кент Бек создал методологию Test Driven Development (TDD, разработка через тестирование). Согласно ей, разработка должна вестись итеративно, путем цикличного повторения шагов Red-Green-Refactor (микро-цикл):

  • написать тест на логику, которую предстоит реализовать, и убедиться, что он падает;

  • написать простейшую реализацию, чтобы тест выполнился успешно;

  • провести рефакторинг реализации, не сломав тесты.

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

Позже Роберт Мартин развил TDD, сформулировав Three Laws of TDD (нано-цикл):

  • перед написанием какого-либо кода реализации необходимо написать падающий тест;

  • тест не должен содержать больше, чем нужно для его падения или провала компиляции;

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

Из-за второго правила работа сводится к поочерёдному написанию строчки реализации SUT за строчкой теста. Нано-циклы заставляют фокусироваться на очень маленькой подзадаче. Это также помогает, когда не знаешь, с чего начать реализацию.

Со временем Робертом были сформулированы еще два более масштабных цикла. Про всех них можно почитать в его статье.

Технику применения нано-циклов я использую далеко не всегда. Частые перескакивания в сложной задаче приводят к потере фокуса и снижают продуктивность. Несмотря на то, что более крупные циклы должны помогать этого избегать. Считаю, что эту технику лучше применять точечно и комфортный размер шага следует выбирать каждому самостоятельно. Жесткое следование всем правилам на практике не всегда идёт на пользу, но полезно при обучении.

Я несколько отступился от канонов и нашел эффективным такой алгоритм работы при реализации новой фичи:

1. Вникнуть в задачу, спроектировать связи между модулями, определить их ответственность.

2. Создать SUT, описать его интерфейс.

  • Если функции должны возвращать какой-то результат, можно писать в их теле TODO(), чтобы код мог скомпилироваться, тогда при вызове функции тест будет прерван эксепшеном. Другой вариант хардкодить возврат простого объекта или null. Так тесты смогут совершить проверки после вызова функции, но тут лучше быть поаккуратнее.

fun doSomething(): Boolean { TODO() }

3. Создать тестовый файл для SUT, объявить тесты-требования.

  • Описать столько кейсов, сколько получится. Нормально, если в ходе написания реализации на ум придут еще какие-то кейсы.

    В пустые тесты/блоки можно добавлять вызов функции fail() (из Junit или AssertJ), чтобы не забыть реализовать какой-то из тестов, поскольку пустой тест при запуске выдает положительный результат.

@Testfun `when invoke - should do something`() {    fail { "not implemented" }}

4. Реализовать тест(ы)

  • Методология подразумевает написание только одного теста и необходимой реализации SUT для его прохождения за микроцикл, но это может быть не продуктивно, если несколько тестов-требований тесно связаны. Вполне нормально написать несколько тестов и потом перейти к реализации. Если же тестов у SUT получается много и они направлены на проверку разных аспектов её работы, то написание всех тестов перед началом реализации будет приводить к потере фокуса с подзадач, которые предстоит в ходе реализации решить. Как именно поступать, стоит определять в зависимости от конкретного случая и от того, как получается комфортнее.

5. Реализовать SUT, чтобы реализованные тесты успешно выполнились.

  • По умолчанию в момент времени стоит фокусироваться на прохождении одного конкретного теста.

6. Отрефакторить SUT, сохранив успешность выполнения реализованных тестов.

7. Если остались нереализованные тесты, перейти к пункту #4.

Алгоритм доработки SUT, которая уже покрыта тестами:

  1. Объявить новые тесты согласно новым требованиям,

  2. Реализовать новые тесты,

  3. Реализовать доработку в SUT, чтобы новые тесты выполнились успешно

  4. Если старые тесты упали:

    • Они актуальны при новых требованиях исправить реализацию SUT и/или эти тесты,

    • Они неактуальны удалить.

  5. Отрефакторить SUT, сохранив успешность выполнения реализованных тестов,

  6. Если остались нереализованные тесты, перейти к пункту 2.

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

Приступаем к реализации по принципу Test Last, но как только достаточное представление сформируется, делаем паузу и переходим к тестам, продолжая дальнейшую разработку уже по TDD. Незачем также применять TDD для реализации тривиальной логики, если вы не собираетесь покрывать её тестами.

По итогу получаем от подхода следующие преимущества:

  • Предварительное написание тестов вынуждает реализовывать SUT заведомо тестируемой. Тесты оказываются слабо связанными с деталями реализации.

  • Тесты являются спецификацией SUT (если при этом соблюдать правила их именования). Часто они вытекают напрямую из функциональных требований к задаче. Сфокусированное перечисление тестов-требований до начала реализации помогает лучше понять и запомнить требования, лучше продумать детали интерфейса SUT. Увеличивается вероятность заблаговременного выявления всех необходимых краевых кейсов. Это само по себе уже помогает допускать меньше ошибок в будущей реализации, а возможность запуска готовых тестов в любой момент дает уверенность в том, что реализация осуществляется правильно.

  • Наличие тестов делает рефакторинг реализации безопасным. После каждого изменения реализации можно быстро прогнать все тесты SUT и в случае обнаружения поломки сразу же её устранить. Время, затрачиваемое на отладку, очень сильно сокращается.

  • На тесты хватает времени, ведь они неотъемлемая часть процесса разработки

  • Все эти факторы в совокупности сокращают время, затрачиваемое на разработку и на развитие приложения в будущем.

  • Приятно видеть, как красные тесты один за другим превращаются в зелёные

TDD это в первую очередь подход к разработке. Методология замечательно показывает себя при реализации SUT с unit- и JVM integration-тестами, поскольку их можно быстро и часто запускать. С Instrumentation non-UI-тестами применять её можно, но из-за длительности запуска придется запускать тесты реже. Применять же TDD с UI-тестами крайне не рекомендуется.

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

Заключение

Применение автоматизированного тестирования способно вывести разработку ПО на качественно новый уровень. Главное подходить к делу осознанно.

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

Можно извлекать существенную пользу за счет применения TDD с тестами на JVM. Применять его следует не всегда. Разработчику нужно самостоятельно подобрать комфортный размер шага в цикле разработки по TDD.

Полезные материалы

Подробнее..

Перевод Рекомендации по использованию IF в Java

02.12.2020 14:12:13 | Автор: admin

Привет, хабровчане. Прямо сейчас в OTUS открыт набор на курс "Java QA Automation Engineer". Всех желающих приглашаем принять участие в открытом уроке на тему "Http. Postman, newman, fiddler (charles), curl, soap. Soapui".


А также делимся с вами переводом полезной статьи.


При написании автотестов на Java не обойтись без блоков с условным оператором if. Всегда есть какие-то условия, истинность которых нужно проверять, чтобы выполнялся тот или иной код в зависимости от результата проверки. Однако если использовать слишком много ветвлений с операторами if и else, это может засорить код теста. Как же писать блоки if, которые будут выполнять свою функцию, сохранив при этом читаемость кода тестов? Мы рассмотрим несколько примеров кода, который может встретиться в тестах, и найдем лучший способ написания аналогичного кода.

1. Сегодня выходной?

Представим, что в тесте есть строковая переменная, которая используется для обозначения дня недели. Нам нужен вспомогательный метод, который сообщит, является ли день, обозначенный этой переменной, выходным. Метод должен возвращать true, если строковой переменной присвоено значение Saturday или Sunday, и false в остальных случаях.

Неудачный вариант

Рассмотрим первый вариант решения этой задачи. Не рекомендую его использовать и сейчас объясню почему.

private boolean isWeekend_no(String weekDay) {    weekDay = weekDay.toUpperCase();    if (weekDay.equals("SATURDAY"))        return true;    if (weekDay.equals("SUNDAY"))        return true;    if (weekDay.equals("MONDAY"))        return false;    if (weekDay.equals("TUESDAY"))        return false;    if (weekDay.equals("WEDNESDAY"))        return false;    if (weekDay.equals("THURSDAY"))        return false;    if (weekDay.equals("FRIDAY"))        return false;    return false;}

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

Код включает несколько блоков if. Каждый блок сравнивает значение параметра с днем недели до тех пор, пока не будут проверены все дни (с понедельника по воскресенье). То есть выполняется 7 операций сравнения в 7 блоках if.

Внутри каждого блока if используется оператор return. Значение true могут вернуть только первые два блока, то есть блоки, в условии которых записаны выходные дни. Остальные блоки, в условии которых записаны рабочие дни, вернут значение false. Затем добавлен оператор return, расположенный вне блоков if. Он вернет указанное после него значение, если не будет выполнено ни одно из условий, проверяемых в блоках if.

Предположим, что мы передали в метод параметр со значением Saturday. Как только соответствующий блок if сравнит параметр со значением SATURDAY, метод вернет значение true и завершится. Оставшийся код выполняться не будет.

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

Удачный вариант

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

private boolean isWeekend_yes(String weekDay) {    if (weekDay.toUpperCase().equals("SATURDAY") || weekDay.toUpperCase().equals("SUNDAY"))        return true;    return false;}

В этом варианте у нас лишь один блок if. Нас интересуют только те значения параметра, которые соответствуют субботе или воскресенью, поэтому только при совпадении с ними метод вернет значение true. Если в метод будет передан параметр с другим значением (другим днем недели или случайным набором символов), вернется значение false.

Не забывайте, что оператор return завершает выполнение метода. Поэтому, как только метод определит, что значение параметра совпадает с названием выходного дня, он вернет значение true и больше выполняться не будет. В этом случае инструкция return false не выполняется. Если переданная в метод строковая переменная не является равной null и содержит значение, отличное от интересующих нас двух значений, то вернется значение false.

2. Получаем текст веб-элемента (WebElement)

В этой задаче нам нужно извлечь и вернуть текст веб-элемента. Для этого мы используем метод getText(), который будет считывать данные из веб-элемента и, конечно же, возвращать строку.

Неудачный вариант

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

private String getElementText_no(WebElement element) {    if (!element.getText().isEmpty())        return element.getText();    return "";}

В этом варианте блок if проверяет, содержит ли веб-элемент какой-либо текст, и возвращает результат выполнения метода getText() только в том случае, если текст веб-элемента не является пустым. В этом случае последующий код в методе не выполняется. Но если строка, которую возвращает метод getText(), является пустой, инструкция return в блоке if выполняться не будет. Выполняться будет последняя инструкция return, которая в этом примере возвращает пустую строку. С логической точки зрения это неправильно.

Удачный вариант

Наша задача вернуть текст веб-элемента вне зависимости от его значения. В примере, который мы рассмотрели выше, код выполняет что-то странное. Сначала он проверяет, является ли текст веб-элемента пустым. Если нет, то возвращается этот самый текст. А если да, код возвращает пустую строку, которая по сути представляет собой то же самое, что и значение полученного текста веб-элемента. И в том и в другом случае возвращаемый результат это результат выполнения метода getText(). Поэтому для решения этой задачи лучше написать код без блоков if и вспомогательных методов, например такой:

element.getText();

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

3. Является ли текст веб-элемента пустым?

В этой задаче нужно проверить, пуст ли текст веб-элемента, и вернуть соответствующее логическое значение.

Неудачный вариант

В первом варианте решения этой задачи мы впервые встречаемся с условным ветвлением if/else. До этого у нас были только блоки if.

private boolean isElementTextEmptyno(WebElement element) {if (element.getText().isEmpty()) {return true;} else return false;}

Инструкция if проверяет, является ли пустым текст веб-элемента, полученный с помощью метода getText(). Если это так, то метод, который мы создали для решения задачи, вернет логическое значение true. Блок else вернет значение false, потому что полученная строка, очевидно, не является пустой.

Удачный вариант

Рассмотренный нами пример неудачен, и вот почему. Метод isEmpty() возвращает логическое значение. Суть этого кода можно кратко изложить так: если получено истинное значение, возвращаем true, иначе возвращаем false. Для решения этой задачи можно написать одну строку кода без создания новых методов:

element.getText().isEmpty();

Мы просто возвращаем значение, полученное в результате выполнения метода isEmpty(). Если в него передан пустой текст, возвращается значение true, иначе возвращается false. Задача решена.

4. Запуск браузера при определенном значении параметра

Для решения этой задачи нужно создать метод, который запустит экземпляр браузера через соответствующий WebDriver. В качестве параметра метод принимает строку, в которой записано название браузера. Этот параметр определяет, какой браузер нужно запустить.

Неудачный вариант

Рассмотрим пример, как не надо писать код. В нем используется классическое условное ветвление if-else-if-else-if Тут слишком много блоков, которые делают код трудным для восприятия и понимания. С такой сложной структурой бывает нелегко понять, все ли возможные сценарии мы предусмотрели.

public WebDriver startBrowser_no(String browserName) {    if (browserName.equals("Chrome") || browserName.equals("chrome") ||                                             browserName.equals("CHROME")) {        System.out.println("Chrome will start!");        return new ChromeDriver();    } else if (browserName.equals("Firefox") || browserName.equals("firefox") ||                                            browserName.equals("FIREFOX")) {        System.out.println("Firefox will start!");        return new FirefoxDriver();    } else if (browserName.equals("Edge") || browserName.equals("edge") ||                                             browserName.equals("EDGE")) {        System.out.println("Edge will start!");        return new EdgeDriver();    } else throw new RuntimeException("Unsupported browser! Will not start any browser!");}

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

Затем нужно изменить структуру кода, избавившись от ветвления if-else-if-else-if- Поскольку мы сравниваем только одно значение параметра с несколькими другими значениями, можно использовать конструкцию switch.

Удачный вариант

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

public WebDriver startBrowser_yes(String browserName) {    switch (browserName.toLowerCase()) {        case "chrome":            System.out.println("Chrome will start!");            return new ChromeDriver();        case "firefox":            System.out.println("Firefox will start!");            return new FirefoxDriver();        case "edge":            System.out.println("Edge will start!");            return new EdgeDriver();        default:            throw new RuntimeException("Unsupported browser! Will not start any browser!");    }}

Здесь четко видно, что будет происходить при каждом значении параметра. Например, не составит труда найти код, который будет выполняться, если параметр имеет значение Chrome. Поскольку в каждом блоке case мы используем инструкцию return, можно быть уверенными в том, что запустится только один браузер: как только мы нашли нужный браузер, метод запускает новый экземпляр браузера и прекращает выполняться. Также несложно понять, что произойдет, если значение параметра не совпадет ни с одним известным значением: смотрим блок default.

5. До обеда или после?

Предположим, что нам нужно вернуть строковую переменную, которая содержит одно из двух значений: Before Lunch (До обеда) или After Lunch (После обеда). Ее значение будет зависеть от текущего времени: если текущий час больше 12, то переменной будет присвоено значение After Lunch. Это, конечно, странный пример, но с его помощью я хочу показать, как использовать упрощенную конструкцию if. Вместо удачного и неудачного вариантов мы рассмотрим два возможных. Они оба хороши, но один чуть лучше другого.

И в том и в другом случае нам нужно объявить строковую переменную и присвоить ей соответствующее значение (Before Lunch или After Lunch). Нам также нужно вычислить текущее время, точнее текущий час. Для этого мы будем использовать класс LocalDateTime:

int currentHour = LocalDateTime.now().getHour();

В этой строке кода мы сначала получаем текущую дату и время, вызвав метод now(), а затем с помощью метода getHour() извлекаем целое число (int), соответствующее значению текущего часа. Переменной currentHour будет присвоено значение от 0 до 23, соответствующее текущему часу.

Возможный вариант 1

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

String momentOfDay1;if (currentHour > 12)      momentOfDay1 = "After Lunch";else momentOfDay1 = "Before Lunch";

Здесь мы сначала объявили строковую переменную momentOfDay1, которая будет хранить искомое значение. Затем в блоке if/else мы сравниваем значение текущего часа с числом 12 и присваиваем переменной momentOfDay1 значение в зависимости от результата сравнения. Все довольно просто.

Единственный недостаток этого варианта состоит в том, что в нем слишком много кода для решения такой простой задачи.

Возможный вариант 2

В этом варианте вместо 4 строк кода мы напишем всего одну.

String momentOfDay2 = (currentHour > 12) ? "After Lunch" : "Before Lunch";

В этой строке есть все: объявление строковой переменной, сравнение значения текущего часа с числом 12 и присвоение переменной momentOfDay2 требуемого значения в зависимости от результата сравнения. Здесь мы используем упрощенную конструкцию if/else. Код между знаком вопроса (?) и двоеточием (:) соответствует ветви кода в блоке if, который мы использовали раньше. Все, что идет после двоеточия, соответствует ветви else. Как видите, этот вариант очень компактный, и, как только вы поймете синтаксис, вам не составит труда его прочитать.

Дополнительные рекомендации

Мы рассмотрели приемы, с помощью которых можно упростить сложные конструкции с if, и мне бы хотелось поделиться еще несколькими советами.

Не оставляйте пустых блоков else. Если вы вдруг написали подобную конструкцию, исправьте ее:

if (condition) {     ... doSomethingHere() ...} else {}

Поскольку в блоке else кода нет, нет смысла писать этот блок. Оставьте только if, и код будет проще понять:

if (condition) {    ... doSomethingHere() ...}

Используйте логический оператор И вместо нового блока if. Взгляните на этот код:

if (condition1) {     if (condition2) {      ... doSomethingHere() ...   }}

Единственное, что содержит первый блок if в этом примере, это второй блок if и больше ничего. Код, который должен выполняться в первом блоке if, это на самом деле код второго блока if (если, конечно, второе условие condition2 выполняется). Поэтому мы можем переписать код, используя только один блок if и логический оператор И (который в Java пишется как два амперсанда: &&).

if (condition1 && condition2) {    ... doSomethingHere() ... }

Не повторяйте одно и то же условие. Если у вас слишком много вложенных if, можно запросто заблудиться в массе условий, которые вы написали для каждой ветви. Например:

if (condition1) {     ... someCodeHere() ...     if (condition2) {          ... someOtherCodeHere() ...          if (condition3) {               ... moreCodeHere() ...               if (condition1) {                    ... evenMoreCodeHere() ...               }               if (!condition2) {                    ... aLotMoreCodeHere() ...               }          }     }}

В этом примере в первом блоке if проверяется истинность первого условия (condition1). Но в одной из вложенных ветвей это условие проверяется еще раз. В этом нет смысла, если код внутри первого if никак не влияет на результат проверки этого условия. То же самое относится ко второму условию (condition2). Если код второго блока if не влияет на результат проверки второго условия, блок if, где проверяется !condition2, выполняться не будет.

Заключение

Из этих примеров видно, что иногда нам просто не нужно использовать конструкцию с if. Но когда мы ее используем, уровней вложенности должно быть как можно меньше. Пишите максимально простой и чистый код. При необходимости его проще отлаживать, а если через несколько месяцев вам нужно будет разобраться, что этот код должен был делать, вы без труда справитесь с этой задачей.


- Узнать подробнее о курсе "Java QA Automation Engineer".


-
Записаться на открытый урок "Http. Postman, newman, fiddler (charles), curl, soap. Soapui".

Подробнее..

Перевод Функции XPath для динамических XPath в Selenium

16.12.2020 14:07:22 | Автор: admin

Будущих студентов курса "Java QA Automation Engineer" и всех интересующихся приглашаем посмотреть подарочное демо-занятие в формате открытого вебинара.

А также делимся переводом полезной статьи.


В данной статье рассматриваются примеры использования функций XPath для идентификации элементов.

Автоматизация взаимодействия с любым сайтом начинается с корректной идентификации объекта, над которым будет выполняться какая-либо операция. Как нам известно, легче всего идентифицировать элемент по таким атрибутам, как ID, Name, Link, Class, или любому другому уникальному атрибуту, доступному в теге, в котором находится элемент.

Но правильно идентифицировать объект можно только в том случае, если такие атрибуты присутствуют и(или) являются уникальными.

=>Руководство по Selenium для новичков см. здесь

Чему вы научитесь:[показать]

Обзор функций XPath

Обсудим сценарий, при котором атрибуты недоступны напрямую.

Постановка задачи

Как идентифицировать элемент, если такие локаторы, как ID, Name, Class и Link, недоступны в теге элемента?

Суть проблемы демонстрирует следующий пример:

Авторизация вTwitter

Как видно из скриншота выше, заголовок Log in to Twitter не имеет дополнительных атрибутов. Поэтому мы не можем использовать ни один из локаторов, таких как ID, Class, Link или Name, для идентификации этого элемента.

Плагин Firepath для Firefox сгенерировал следующий путь XPath:

//[@id=page-container]/div/div[1]/h1

Мы бы не рекомендовали использовать указанный выше путь XPath, поскольку структура страницы или id могут меняться динамически. Если все же использовать этот нестабильный XPath, вероятно, потребуется чаще его обновлять, что подразумевает лишнюю трату времени на поддержку. Это один из случаев, когда мы не можем использовать общее выражение XPath с такими локаторами, как ID, Class, Link или Name.

Решение

Идентификация элемента с помощью функций XPath по тексту

Поскольку у нас есть видимый текст Log in to Twitter, мы могли бы использовать следующие функции XPath для идентификации уникального элемента.

  1. contains() [по тексту]

  2. starts-with() [по тексту]

  3. text()

Функции XPath, такие как contains(), starts-with() и text(), при использовании с текстом Log in to Twitter помогут нам корректно идентифицировать элемент, после чего мы сможем произвести над ним дальнейшие операции.

1. Метод Contains()

Синтаксис.Чтобы найти на веб-странице элемент Log in to Twitter, воспользуйтесь одним из следующих выражений XPath на основе метода contains().

Поиск по тексту:

  • //h1[contains(text(), Login to)]

  • //h1[contains(text(), in to Twitter)]

Примечание. Наличие одного совпадающего узла свидетельствует об успешной идентификации веб-элемента.

Из примера выше видно, что методу contains() не требуется весь текст для правильной идентификации элемента. Достаточно даже части текста, но эта часть текста должна быть уникальной. Используя метод contains(), пользователь может легко идентифицировать элемент, даже после смены ориентации страницы.

Обратите внимание, что при указании всего текста Log in to Twitter с методом contains() элемент будет также идентифицирован корректно.

2. Метод starts-with()

Синтаксис.Чтобы найти на веб-странице элемент Log in to Twitter, используйте следующие выражения XPath на основе метода starts-with().

Поиск по тексту:

  • //h1[starts-with(text(),Log in)]

  • //h1[starts-with(text(),Log in to)]

Из приведенного выше примера видно, что XPath-функции starts-with() требуется по крайней мере первое слово (Log) видимого текста для однозначной идентификации элемента. Функция работает даже с неполным текстом, но он должен как минимум включать первое слово частично видимого текста.

Обратите внимание, что при использовании всего текста Log in to Twitter с методом starts-with() элемент будет также идентифицирован корректно.

Недействительный XPath для starts-with()://h1[starts-with(text(),in to Twitter)]

Примечание.Отсутствие совпадающих узлов свидетельствует о том, что элемент на веб-странице не был идентифицирован.

3. Метод text()

Синтаксис.Чтобы найти на веб-странице элемент Log in to Twitter, воспользуйтесь следующим выражением XPath на основе метода text().

В этом выражении мы указываем весь текст, содержащийся между открывающим тегом <h1> и закрывающим тегом </h1>. Если использовать функцию text() с частью текста, как в случае с методами contains() и starts-with(), то мы не сможем найти данный элемент.

Недействительное выражение Xpath для text():

Идентификация элемента с помощью функций XPath по атрибуту

Мы используем функции XPath (contains или starts-with) с атрибутом в тех случаях, когда в теге содержатся уникальные значения атрибутов. Доступ к атрибутам производится с помощью символа @.

Для лучшего понимания рассмотрим следующий пример:

Авторизация вGoogle

1. Метод Contains()

Синтаксис.Чтобы точно идентифицировать кнопку Im Feeling Lucky (Мне повезет) с помощью XPath-функции contains(), можно указать следующие атрибуты.

Вариант А поиск по значению атрибута Value

  • //input[contains(@value,Feeling)]

  • //input[contains(@value,Lucky)]

На скриншотах выше видно, что поиск по атрибуту Value слов Feeling или Lucky с помощью функции contains() позволяет однозначно идентифицировать данный элемент. Стоит отметить, что, даже если мы используем полное содержимое атрибута Value, мы сможем корректно идентифицировать элемент.

Вариант Б поиск по содержимому атрибута Name

//input[contains(@name=btnI)]

Неправильное использование функции XPath с атрибутом:

Нужно быть крайне внимательным при выборе атрибута для поиска с помощью методов contains() и starts-with(). Если значение атрибута не уникальное, мы не сможем однозначно идентифицировать элемент.

Если мы воспользуемся атрибутом type при идентификации кнопки I'm Feeling Lucky, то XPath не сработает.

Наличие двух совпадающих узлов свидетельствует о том, что нам не удалось корректно идентифицировать элемент. В данном случае значение атрибута type не является уникальным.

2. Метод starts-with()

Метод starts-with() в сочетании с атрибутом может пригодиться для поиска элементов, у которых начало атрибута постоянное, а окончание изменяется. Такой метод позволяет работать с объектами, динамически изменяющими значения своих атрибутов. Его также можно использовать для поиска однотипных элементов.

Перейдите на страницу авторизацииFacebook.

Изучите первое текстовое поле First Name (Имя) и второе текстовое поле Surname (Фамилия) формы авторизации.

Первое текстовое поле First Name идентифицировано.

Второе текстовое поле Surname идентифицировано.

В случае обоих текстовых полей, из которых состоит форма авторизации Facebook, первая часть атрибута id всегда остается неизменной.

First Name id="u02"

Surname id="u04"

Это тот случай, когда мы можем использовать атрибут вместе с функцией starts-with(), чтобы получить все элементы такого типа с атрибутом id. Обратите внимание, что мы рассматриваем эти два поля только для примера. На экране может быть больше полей с id, которые начинаются с u0.

Starts-with() [по атрибуту id]

//input[starts-with(@id,"u0")]

Важное примечание.Здесь мы использовали двойные кавычки вместо одинарных. Но одиночные кавычки тоже будут работать с методом starts-with.

11 найденных узлов указывают на то, что данное выражение XPath позволило идентифицировать все элементы, id которых начинается с u0. Вторая часть id (2 для имени, 4 для фамилии и т. д.) позволяет однозначно идентифицировать элемент.

Мы можем использовать атрибут с функцией starts-with там, где нам нужно собрать элементы похожего типа в список и динамически выбрать один из них, передавая аргумент в обобщенный метод, чтобы однозначно идентифицировать элемент.

На примере ниже показано использование функции starts-with.

Пример кода

/ <strong>Generic Method</strong> /

public void xpathLoc(String identifier){

//The below step identifies the element First Name uniquely when the argument is 2

WebElement E1=d1.findElement(By.xpath("//input[starts-with(@id,u0+identifier )]"));

E1.sendKeys(Test1); / This step enters the value of First Name as Test 1 /

}

/ <strong>Main Method</strong>*/

public static void main(String[] args) {

xpathLoc(2); --- This step calls the xpathLoc() method to identify the first name.

}

Примечание.Eclipse может не допускать использование двойных кавычек. Возможно, вам придется прибегнуть к другому коду, чтобы сформировать динамический XPath.

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

Заключение

В этой статье мы рассмотрели, как можно использовать функции XPath contains(), starts-with() и text() с атрибутом и текстом для однозначной идентификации элементов в структуре HTML DOM.

Ниже приведены некоторые замечания касательно функций XPath:

  1. Используйте метод contains() в XPath, если известна часть постоянно видимого текста или атрибута.

  2. Используйте метод starts-with() в XPath, если известна первая часть постоянно видимого текста или атрибута.

  3. Вы также можете использовать методы contains() и starts-with() со всем текстом или полным атрибутом.

  4. Используйте метод text() в XPath, если вам известен весь видимый текст.

  5. Нельзя использовать метод text() с частичным текстом.

  6. Нельзя использовать метод starts-with(), если начальный текст не используется в XPath или если начальный текст постоянно изменяется.

В следующем уроке мы узнаем, как использовать оси XPath с функциями XPath для более точного определения расположения элементов на веб-странице.


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

Об этом расскажем на открытом вебинаре. Регистрируйтесь

Узнать подробнее о курсе "Java QA Automation Engineer" можно
здесь.

ЗАБРАТЬ СКИДКУ

Подробнее..

Перспективы разработчика в автоматизации тестирования ПО

15.02.2021 10:17:19 | Автор: admin

Как и многим, нам в Veeam нужны разработчики автотестов. Обычно на эту позицию претендуют специалисты с опытом в ручном тестировании ПО, но зачастую у них может не быть навыков программирования. А программисты, как правило, нечасто стремятся в разработку автотестов. Между тем у нас в компании есть автоматизаторы, которые изначально были не тестировщиками, а программистами, и они очень хорошо себя проявили в автоматизации. В статье я расскажу почему такой вариант развития карьеры может быть интересен для разработчиков.

Лирическое отступление

Это только присказка, сказка впереди

В начале 2000-x я работал в IT-компании, которая выполняла несколько аутсорсинговых проектов для компании Integrated Genomics. Проекты были связаны с расшифровкой геномов простейших организмов. К примеру, одна из утилит искала фрагменты (праймеры) с определенными свойствами в геноме кишечной палочки. На входе утилиты была последовательность ДНК, загружаемая из публичной базы геномов ERGO и состоящая из азотистых оснований. На выходе таблица фрагментов и их позиция в цепочке ДНК. Далее эти фрагменты использовались биологами для синтеза геномов. Задача была сравнительно простой. Нужно было лишь позаботиться о том, чтобы программа не выжирала всю оперативную память довольно слабых машин, которые были у нас на тот момент. Сложность других проектов заключалась в том, что они находились на стыке трех дисциплин: биологии, математики и информатики. В тех случаях, когда алгоритм задачи был понятен, его реализация в программном коде не представляла трудности. Но когда сама задача была неопределенной, и не находилось никого кто мог бы ее формализовать, это был серьезный вызов.

Выяснилось, что для того, чтобы успешно решать такие задачи, нужны фундаментальные знания по биологии и высшей математике. Нас, молодых и горячих, это не остановило. Мы нашли англоязычную книгу по биоинформатике профессора Павла Певзнера и приступили к изучению. Поначалу повествование было легким и непринужденным. Во вступлении Павел рассказывал о том, как выживал в Москве в студенческие годы, и это было поистине приятное и расслабляющее чтение. Далее в первых главах речь шла про азотистые основания нуклеотидов ДНК аденин, гуанин, тимин и цитозин и про комплиментарность нуклеиновых кислот, а именно: как основания нуклеотидов способны формировать парные комплексы аденинтимин и гуанинцитозин при взаимодействии цепей нуклеиновых кислот. Было понятно, что такое свойство нуклеотидов играет ключевую роль в репликации ДНК. Я помню, что испытывал подъем и состояние потока, читая это объяснение, и мысленно представлял, как мы сейчас быстренько все это освоим (подумаешь, биология) и сможем брать более серьезные задачи. Мы даже думали о том, что сможем написать свой геномный ассемблер и взяться за расшифровку простейших геномов. Продолжаю читать книгу, и тут бах система уравнений на полстраницы без каких-либо объяснений. Из контекста подразумевалось, что эта система проще пареной репы, и любое объяснение будет оскорблением для читателя. Не было даже сноски для факультативного чтения. Я решил эту страницу пропустить, вернуться к ней позже и пока что продолжить читать дальше вдруг станет понятно. Перелистываю страницу а там еще одна система уравнений уже на всю страницу и скупое описание, часть слов из которого мы не нашли в словаре. Стало понятно, что эту область знаний на стыке геномики и математики нахрапом не возьмешь. Также стало понятно, почему подавляющее большинство коллег, с которыми мы взаимодействовали, имели биологическое и/или математическое образование. В конечном итоге, шаг за шагом погружаясь в основы геномики, нам удалось создать несколько программных продуктов, и тогда я в первый раз всерьез задумался о том насколько результативной и интересной может быть деятельность на стыке нескольких дисциплин.

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

Перспективы разработчика в автоматизации тестирования ПО

Перспективы разработчика в этой области трудно переоценить. Как и биоинформатика, разработка автотестов находится на стыке двух дисциплин, и навыки программиста здесь все чаще выступают на первый план.

Исторически сложилось так, что в автоматизацию приходят специалисты с опытом в ручном тестировании ПО. В какой-то момент они осознают, что рутинные операции можно и нужно автоматизировать, либо просто желают попробовать себя в новом качестве. Опыт, привносимый ручными тестировщиками в проект автоматизации, бывает архиполезным. Никто лучше них не знает возможности и слабые места продукта, наиболее трудоемкие сценарии для тестирования, окружение, в котором работает продукт, а также пожелания пользователей и планы на следующие релизы. Как правило, хороший ручной тестировщик четко понимает, что именно он хочет автоматизировать, но с написанием автотестов порой возникают сложности. Почему? Потому что автотесты это такой же программный продукт, как и продукт, который они призваны тестировать. Нужно продумать архитектуру системы автотестов, механизмы их запуска (Continuous Integration, CI) и самое главное нужно писать хороший код. По факту, для ручного тестировщика это зачастую оказывается непросто. Ведь здесь требуется создать полноценный проект, который можно интегрировать в CI, изменять, расширять и переиспользовать. Для этого нужны способности к программированию, накопленный опыт и набитые шишки.

Ручной тестировщик с глубоким пониманием продукта и методик тестирования и разработчик с опытом программирования отлично дополняют друг друга. Первый силен в постановке задачи (use case -> test case), второй в ее реализации. Встает вопрос: почему среди автоматизаторов встречается много ручных тестировщиков? На это есть несколько причин:

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

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

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

Опасение потерять в карьерном росте и зарплате.

Разберем эти моменты.

Автотесты это такой же продукт разработки, как и тот продукт, для которого эти автотесты создаются. Если разработчик идет на позицию автоматизатора в выделенную группу разработки автотестов, он остается на стезе написания программного кода. Да, ему надо иметь представление о тестировании ПО, но базовые знания можно сравнительно быстро получить, пролистав какое-либо руководство по тестированию ПО (например, вот эту книгу). Безусловно, автоматизатору нужно понимать продукт, для которого он пишет автотесты. Но освоить продукт на приемлемом уровне зачастую оказывается легче, чем научиться писать хороший код. Знания о продукте и методиках тестирования будут расширяться по мере ознакомления с багами, написанными на продукт, и общения с ручными тестировщиками. Разработчик не перестанет быть разработчиком, не превратится в ручного тестировщика. Это не его путь. Путь разработчика писать хорошие автотесты.

Задачи у автоматизатора интересные и зачастую сложные. В первую очередь стоит вспомнить про знаменитую пирамиду Фаулера. Модульные, интеграционные, end-to-end тесты подразумевают вдумчивый подход к структуре тестов и выбору инструментов в соответствии с функциональностью продуктов, для которых пишем автотесты. Если говорить о продуктах, разрабатываемых в Veeam, то автоматизатору понадобится работать с REST, WebDriver, Microsoft SQL Server, Amazon Web Services, Microsoft Azure, VMware vCenter, Hyper-V список не исчерпан. У каждого из облаков и гипервизоров свой API и свои скелеты в шкафу. Порой приходится писать код на различных языках программирования, использовать заглушки, семафоры, создавать свои обертки и т.п.

Одну и ту же задачу можно решить по-разному, и автоматизатор ищет наиболее эффективное решение. Вот лишь один из примеров сценарий, реализованный для продукта Veeam ONE. Один из компонентов продукта Business View, который позволяет группировать элементы виртуальной инфраструктуры по различным критериям. Критериев и вариантов их комбинирования очень много, поэтому проверка этой функциональности вручную занимает много времени. Написание автотестов в лоб с имитацией действий ручных тестировщиков было бы неэффективным: тесты для графического интерфейса десктопных приложений, как правило, сложны и трудоемки в разработке, являются хрупкими, их тяжело модифицировать, и выполняются они долго. Мы нашли другое решение: поскольку действия пользователя в UI интерполируются в SQL-запросы к базе данных, мы используем SQL-запросы для создания категорий и групп. Это позволило нам в разумные сроки покрыть автотестами все свойства и операторы, задействованные в Business View.

Как сделать так, чтобы тесты, запускаемые параллельно ради уменьшения общего времени запуска, не мешали друг другу? Как измерить покрытие продукта автотестами? Анализ покрытия кода? Анализ покрытия пользовательских сценариев? Как отслеживать падения сервисов? Как интегрировать автотесты с репортинговыми фреймворками? Как интегрировать автотесты со сборкой билдов в CI? Как прогонять тесты по pull-реквестам? И многое-многое другое.

Нужно обратить внимание на качество самих автотестов. Кто сторожит сторожей? Какому автотесту можно доверять? Автотест должен быть эффективным по критерию количество затраченных на него усилий / полученный результат, автономным, стабильным (нехрупким), быстрым, надежным (никаких false positive и false negative). В автотесте должен быть понятный, хороший код с точки зрения возможностей языка программирования, чтобы этот код можно было легко расширять и изменять.

Начинающий автоматизатор приступает к написанию простых тестов в соответствии с согласованными планами/подходами/инструментами. Решает самые простые задачи, учится справляться с трудностями, находить решения, обрастает знаниями и навыками. Задачи становятся все более серьезными, кредит доверия автоматизатору растет. Если автоматизатор не останавливается в своем росте, то спустя какое-то время он дорастает до разработки комплексных технических решений и более глубокого участия в обсуждении стратегии и приоритетов в разработке автотестов. В один прекрасный день он принимает под свое крыло начинающих автоматизаторов, которые хотят приносить пользу и расти.

Что в Veeam?

В компании Veeam мы будем рады новым боевым товарищам с опытом программирования на C# (например, web-интерфейсы, десктопные приложения, консольные утилиты и т.п.). У нас в Veeam есть много продуктов. Технологии в них могут различаться. В автотестах для нескольких продуктов мы опираемся на REST и WebDriver. Если у вас нет опыта с этими технологиями, но вы уверенно себя чувствуете в написании кода на C# и питаете интерес к автоматизации тестирования, то, возможно, мы также найдем точки соприкосновения.

Мы будем рады вашему резюме и паре абзацев о том, что вас привлекает в автоматизации, о ваших сильных сторонах и профессиональных планах. Пишите нам на ящик qa@veeam.com внимательно прочитаем. Если укажете в теме письма [Хабр] (например, [Хабр] Позиция автоматизатора), будет плюс в карму =)

Да пребудет с Вами Сила.

Подробнее..

Автоматизация тестирования мобильных приложений. Часть 2 предусловия, верификация элементов и независимость шагов

16.03.2021 18:09:12 | Автор: admin

Меня зовут Дмитрий Макаренко, я Mobile QA Engineer в Badoo и Bumble: занимаюсь тестированием новой функциональности в наших приложениях вручную и покрытием её автотестами.

За последние два года подход к автоматизации тестирования в нашей компании сильно изменился. Количество людей, активно вовлечённых в разработку тестов, увеличилось с десяти до 40 человек. А любая новая функциональность в приложениях теперь обязательно должна быть покрыта тестами до релиза.

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

В подготовке текста мне помогал мой коллега Виктор Короневич: с этой темой мы вместе выступали на конференции Heisenbug.

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

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

Спойлер

В конце статьи будет ссылка на тестовый проект со всеми практиками.

Практика 4. Верификация изменения состояния элементов

Пожалуй, эта практика одна из самых важных в мобильной автоматизации в принципе, потому что в приложениях очень редко встречаются статические элементы, которые сразу же отображаются на экране и всегда на нём доступны. Чаще мы сталкиваемся с тем, что загрузка элементов занимает какое-то время.

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

Таким образом, прежде чем начать проверять элементы, нам необходимо дождаться их появления на экране. Естественно, эта проблема не нова и существуют стандартные решения. Например, в Selenium это различные типы методов wait, а в Calabash метод wait_for.

Зачем нам свой велосипед, или Почему мы отказались от стандартного решения

Когда мы начинали строить наш фреймворк автоматизации, мы использовали стандартный метод wait_for для ожидания появления или изменения состояния элементов. Но в какой-то момент столкнулись с проблемой. Иногда, раз в две-три недели, у нас зависали все тесты. Мы не могли понять, как и почему это происходит и что мы делаем не так.

После того как мы добавили логи и проанализировали их, оказалось, что эти зависания были связаны с реализацией метода wait_for, входящего в состав фреймворка Calabash. wait_for использует метод timeout модуля Ruby Timeout, который реализован на глобальном потоке. А тесты зависали, когда этот метод timeout использовался вложено в других методах: наших и фреймворка Calabash.

Например, рассмотрим прокрутку страницы профиля до кнопки блокировки пользователя.

def scroll_to_block_button  wait_for(timeout: 30) do    ui.scroll_down    ui.wait_until_no_animation    ui.element_displayed?(BLOCK_BUTTON)  endend

Мы видим, что используется метод wait_for. Происходит прокрутка экрана вниз, потом ожидание окончания анимации и проверка отображения кнопки блокировки.

Рассмотрим реализацию метода wait_until_no_animation.

def wait_until_no_animation  wait_for(timeout: 10) do    !ui.any_element_animating?  endend

Метод wait_until_no_animation реализован так же с wait_for. Он ждёт, когда на экране закончится анимация. Получается, что wait_for, вызванный внутри wait_for, вызывает другие методы. Представьте себе, что вызовы wait_for также есть внутри методов Calabash. С увеличением цепочки wait_for внутри wait_for внутри wait_for риск зависания увеличивается. Поэтому мы решили отказаться от использования этого метода и придумать своё решение.рый бы повторял проверку до тех пор, пока не выполнится заданное условие либо пока не истечёт отведённое время. Если проверка не проходит успешно за отведённое время, наш метод должен выбрасывать ошибку.

Сначала мы создали модуль Poll с одним методом for, который повторял стандартный метод wait_for. Со временем собственная реализация позволила нам расширять функциональность модуля по мере того, как у нас появлялась такая необходимость. Мы добавили методы, ожидающие конкретные значения заданных условий. Например, Poll.for_true и Poll.for_false явно ожидают, что исполняемый код вернёт true либо false. В примерах ниже я покажу использование разных методов из модуля Poll.

Также мы добавили разные параметры методов. Рассмотрим подробнее параметр return_on_timeout. Его суть в том, что при использовании этого параметра наш метод Poll.for перестаёт выбрасывать ошибку, даже если заданное условие не выполняется, а просто возвращает результат выполнения проверки.

Предвижу вопросы Как это работает? и Зачем это нужно?. Начнём с первого. Если в методе Poll.for мы будем ждать, пока 2 станет больше, чем 3, то мы всегда будем получать ошибку по тайм-ауту.

Poll.for { 2 > 3 }> WaitError

Но если мы добавим наш параметр return_on_timeout и всё так же будем ждать, пока 2 станет больше, чем 3, то после окончания тайм-аута, 2 всё ещё не станет больше, чем 3, но наш тест не упадёт, а метод Poll.for вернёт результат этой проверки.

Poll.for(return_on_timeout: true) { 2 > 3 }> false

Зачем это нужно? Мы используем параметр return_on_timeout для верификации изменения состояния наших элементов. Но очень важно делать это аккуратно, так как он может скрывать реальные падения тестов. При некорректном использовании тесты будут продолжать выполняться, когда заданные условия не выполняются, в тех местах, где должны были бы выбросить ошибку.

Варианты изменения состояния элементов

А теперь перейдём к самому интересному поговорим о том, как проверять различные изменения состояния и какие изменения состояния вообще существуют. Познакомьтесь с нашим объектом тестирования чёрным квадратом:

Он умеет всего две вещи: появляться на экране и пропадать с экрана.

Первый вариант изменения состояния называется Должен появиться. Он происходит в том случае, когда состояние 1 на экране нет нашего объекта тестирования, а состояние 2 он должен появиться.

Должен появитьсяДолжен появиться

Если он появляется, то проверка проходит успешно.

Второй вариант изменения состояния называется Должен пропасть. Происходит он тогда, когда в состоянии 1 отображается наш объект тестирования, а в состоянии 2 его быть не должно.

 Должен пропасть Должен пропасть

Третий вариант не такой очевидный, как первые два, потому что в нём, по сути, мы проверяем неизменность состояния. Называется он Не должен появиться. Это происходит, когда в состоянии 1 наш объект тестирования не отображается на экране и спустя какое-то время в состоянии 2 он всё ещё не должен появиться.

 Не должен появиться Не должен появиться

Вы, наверное, догадались, какой вариант четвёртый. Он называется Не должен пропасть. Происходит это, когда в состоянии 1 объект отображается на экране, и спустя какое-то время в состоянии 2 он всё ещё находится там.

Не должен пропастьНе должен пропасть

Реализация проверок разных вариантов

Мы зафиксировали все возможные варианты изменения состояния элементов. Как же их проверить? Разобьём реализацию на проверки первых двух вариантов и проверки третьего и четвёртого.

В случае с первыми двумя вариантами всё довольно просто. Для проверки первого нам просто нужно подождать, пока элемент появится, используя наш метод Poll:

# вариант "Должен появиться"Poll.for_true { ui.elements_displayed?(locator) }

Для проверки второго подождать, пока элемент пропадёт:

# вариант "Должен пропасть"Poll.for_false { ui.elements_displayed?(locator) }

Но в случае с третьим и четвёртым вариантами всё не так просто.

Рассмотрим вариант Не должен появиться:

# вариант "Не должен появиться"ui.wait_for_elements_not_displayed(locator)actual_state = Poll.for(return_on_timeout: true) { ui.elements_displayed?(locator) }Assertions.assert_false(actual_state, "Element #{locator} should not appear")

Здесь мы, во-первых, фиксируем состояние отсутствия элемента на экране.

Далее, используя Poll.for с параметром return_on_timeout, мы ждём появления элемента. При этом метод Poll.for не выбросит ошибку, а вернёт false, если элемент не появится. Значение, полученное из Poll.for, сохраняется в переменной actual_state.

После этого происходит проверка неизменности состояния элемента с использованием метода assert.

Для проверки варианта Не должен пропасть мы используем похожую логику, ожидая пропажи элемента с экрана вместо его появления:

# вариант "Не должен пропасть"ui.wait_for_elements_displayed(locator)actual_state = Poll.for(return_on_timeout: true) { !ui.elements_displayed?(locator) }Assertions.assert_false(actual_state, "Element #{locator} should not disappear")

Проверки этих четырёх вариантов изменения состояния актуальны для многих элементов мобильных приложений. А поскольку разработкой тестов у нас занимается много людей, всегда есть вероятность того, что кто-то забудет про некоторые варианты при создании новых проверок. Поэтому мы вынесли реализацию проверок всех вариантов изменения состояния в один метод:

def verify_dynamic_state(state:, timeout: 10, error_message:)  options = {    return_on_timeout: true,    timeout:           timeout,  }  case state    when 'should appear'      actual_state = Poll.for(options) { yield }      Assertions.assert_true(actual_state, error_message)    when 'should disappear'      actual_state = Poll.for(options) { !yield }      Assertions.assert_true(actual_state, error_message)    when 'should not appear'      actual_state = Poll.for(options) { yield }      Assertions.assert_false(actual_state, error_message)    when 'should not disappear'      actual_state = Poll.for(options) { !yield }      Assertions.assert_false(actual_state, error_message)    else      raise("Undefined state: #{state}")  endend

yield это код блока, переданного в данный метод. На примерах выше это был метод elements_displayed?. Но это может быть любой другой метод, результат выполнения которого отражает состояние необходимого нам элемента. Документация Ruby.

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

Выводы:

  • важно не забывать про все четыре варианта изменения состояния при проверках UI-элементов;

  • полезно вынести эти проверки в общий метод.

Мы рекомендуем использовать полную систему проверок всех вариантов изменения состояния. Что мы имеем в виду? Представьте, что когда элемент есть это состояние true, а когда его нет false.

Состояние 1

Состояние 2

Должен появиться

FALSE

TRUE

Должен пропасть

TRUE

FALSE

Не должен появиться

FALSE

FALSE

Не должен пропасть

TRUE

TRUE

Мы строим матрицу всех комбинаций. При появлении нового состояния таблицу можно расширить и получить новые комбинации.

Практика 5. Надёжная настройка предусловий тестов

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

Рассмотрим два примера. Первый отключение сервиса локации на iOS в настройках. Второй создание истории чата.

В первом примере реализация метода отключения сервиса локации на iOS выглядит следующим образом:

def switch_off_location_service  ui.wait_for_elements_displayed(SWITCH)  if ui.element_value(SWITCH) == ON    ui.tap_element(SWITCH)    ui.tap_element(TURN_OFF)  endend

Мы ждём, пока переключатель (элемент switch) появится на экране. Потом проверяем его состояние. Если оно не соответствует ожидаемому, мы его изменяем.

После этого мы закрываем настройки и запускаем приложение. И иногда внезапно сталкиваемся с проблемой: почему-то сервис локации остаётся включённым. Как это получается? Мы же сделали всё, чтобы его отключить. Кажется, что это проблема работы системных настроек в iOS системе. При быстром выходе из настроек (а тест делает это моментально после нажатия на переключатель) их новое состояние не сохраняется. Но проблемы могут возникнуть и при настройке предусловий в нашем приложении.

Давайте обратимся ко второму примеру созданию истории чата перед началом выполнения теста. Реализация метода выглядит следующим образом:

def send_message(from:, to:, message:, count:)  count.times do    QaApi.chat_send_message(user_id: from, contact_user_id: to, message: message)  endend

Мы используем QAAPI для отправки сообщений по user_id. В цикле мы отправляем необходимое количество сообщений.

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

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

Как же решить эту проблему? Мы можем добавить гарантию выполнения действия в методы установки предусловий.

Тогда наш метод отключения сервиса локации будет выглядеть следующим образом:

def ensure_location_services_switch_in_state_off  ui.wait_for_elements_displayed(SWITCH)  if ui.element_value(SWITCH) == ON    ui.tap_element(SWITCH)    ui.tap_element(TURN_OFF)    Poll.for(timeout_message: 'Location Services should be disabled') do      ui.element_value(SWITCH) == OFF    end  endend

Используя метод Poll.for, мы убеждаемся, что состояние переключателя изменилось, прежде чем переходить к следующим действиям теста. Это позволяет избежать проблем, вызванных тем, что сервис локации время от времени был включён.

Во втором примере нам снова помогут наши методы QAAPI.

def send_message(from:, to:, message:, count:)  actual_messages_count = QaApi.received_messages_count(to, from)  expected_messages_count = actual_messages_count + count  count.times do    QaApi.chat_send_message(user_id: from, contact_user_id: to, message: message)  end  QaApi.wait_for_user_received_messages(from, to, expected_messages_count)end

Перед отправкой сообщений мы получаем текущее количество сообщений в чате, а после убеждаемся, что необходимое количество сообщений было отправлено. Только после этой проверки тест продолжает своё выполнение. Таким образом, когда мы открываем чат в приложении, мы видим все необходимые сообщения и можем выполнять нужные проверки.

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

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

Более подробно о проблемах, описанных в этом разделе, можно прочитать в статье Мартина Фаулера.

Практика 6. Простые и сложные действия, или Независимость шагов в тестах

Простые действия

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

Начнём с теста поиска и отправки GIF-сообщений.

Сначала нам нужно открыть чат с пользователем, которому мы хотим отправить сообщение:

When  primary_user opens Chat with chat_user

Потом открыть поле ввода GIF-сообщений:

And   primary_user switches to GIF input source

Далее нам нужно ввести поисковый запрос для GIF-изображений, убедиться, что список обновился, отправить понравившееся изображение из списка и убедиться, что оно было отправлено.

And   primary_user searches for "bee" GIFsAnd   primary_user sends 7th GIF in the listThen  primary_user verifies that the selected GIF has been sent

Целиком сценарий выглядит так:

Scenario: Searching and sending GIF in Chat  Given users with following parameters    | role         | name |    | primary_user | Dima |    | chat_user    | Lera |  And   primary_user logs in  When  primary_user opens Chat with chat_user  And   primary_user switches to GIF input source  And   primary_user searches for "bee" GIFs  And   primary_user sends 7th GIF in the list  Then  primary_user verifies that the selected GIF has been sent

Обратим внимание на шаг, который отвечает за поиск гифки:

And(/^primary_user searches for "(.+)" GIFs$/) do |keyword|  chat_page = Pages::ChatPage.new.await  TestData.gif_list = chat_page.gif_list  chat_page.search_for_gifs(keyword)  Poll.for_true(timeout_message: 'Gif list is not updated') do    (TestData.gif_list & chat_page.gif_list).empty?  endend

Здесь, как и почти во всех остальных шагах, мы делаем следующее:

  1. сначала ожидаем открытия нужной страницы (ChatPage);

  2. потом сохраняем список всех доступных GIF-изображений;

  3. далее вводим ключевое слово для поиска;

  4. затем ждём изменения состояния обновления списка (ведь мы говорили о том, что полезно добавлять в тесты проверку выполнения действий).

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

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

Как же нам этого избежать? Как вы, возможно, заметили, наш шаг поиска GIF-изображений на самом деле включал в себя три действия:

  1. сохранение текущего списка;

  2. поиск;

  3. проверку обновления списка.

Решением проблемы переиспользования будет разделение этого шага на три простых и независимых.

Первый шаг сохраняет текущий список изображений:

And(/^primary_user stores the current list of GIFs$/) do  TestData.gif_list = Pages::ChatPage.new.await.gif_listend

Второй шаг поиск гифки позволяет напечатать ключевое слово для поиска:

And(/^primary_user searches for "(.+)" GIFs$/) do |keyword|  Pages::ChatPage.new.await.search_for_gifs(keyword)end

На третьем шаге мы ждём обновления списка:

And(/^primary_user verifies that list of GIFs is updated$/) do  chat_page = Pages::ChatPage.new.await  Poll.for_true(timeout_message: 'Gif list is not updated') do    (TestData.gif_list & chat_page.gif_list).empty?  endend

В итоге наш первоначальный сценарий выглядит следующим образом:

Scenario: Searching and sending GIF in Chat  Given users with following parameters    | role         | name |    | primary_user | Dima |    | chat_user    | Lera |  And   primary_user logs in  When  primary_user opens Chat with chat_user  And   primary_user switches to GIF input source  And   primary_user stores the current list of GIFs  And   primary_user searches for "bee" GIFs  Then  primary_user verifies that list of GIFs is updated  When  primary_user sends 7th GIF in the list  Then  primary_user verifies that the selected GIF has been sent

Таким образом, мы можем использовать первые два шага даже в том случае, если в тесте список не обновляется. Это помогает нам экономить время на разработку, так как мы можем переиспользовать простые шаги в разных тестах.

Но и здесь есть нюансы. Шаги не всегда можно сделать простыми и независимыми. В таких случаях мы будем называть их сложными.

Сложные действия

Под сложными шагами мы подразумеваем те, которые включают в себя переходы между разными экранами приложения либо работу с изменением состояния какого-либо экрана. Рассмотрим такую ситуацию на примере голосования в мини-игре.

Мини-игра это экран, на котором пользователю предлагаются профили других людей, которые ему написали. Можно либо отвечать на сообщения, либо пропускать этих пользователей. Действие пропуска назовём Голосовать нет.

Тестовый пользовательТестовый пользователь

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

Проголосовать нет простое действие. Но, если мы сделаем для него простой шаг, то для того чтобы проголосовать N раз, нам нужно будет использовать этот шаг столько же раз на уровне сценария. Читать такой сценарий неудобно. Поэтому есть смысл создать более сложный шаг с параметром Количество голосов, который сможет проголосовать необходимое нам количество раз.

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

When(/^primary_user votes No in Messenger mini game (\d+) times$/) do |count|  page = Pages::MessengerMiniGamePage.new.await  count.to_i.times do    page.vote_no  endend

Но тут мы сталкиваемся с проблемой: иногда тест голосует слишком быстро. То есть он нажимает на кнопку до того, как обрабатывается предыдущее нажатие. В этом случае приложение не отправит новый голос на сервер. Наш шаг при этом выполнится успешно. А на следующем шаге, когда мы захотим убедиться, что пользователь находится на правильной позиции в игре, тест упадёт, потому что предыдущий шаг не выполнил свою задачу и сделал меньше голосов, чем было нужно.

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

When(/^primary_user votes No in Messenger mini game (\d+) times$/) do |count|  page = Pages::MessengerMiniGamePage.new.await  count.to_i.times do    progress_before = page.progress    page.vote_no    Poll.for_true do      page.progress > progress_before       end  endend

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

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

Обобщая эти рекомендации, советуем выделять независимые методы для простых действий в тестах и добавлять проверку предустановок в сложные действия.

Практика 7. Верификация необязательных элементов

Под необязательными элементами мы понимаем такие элементы, которые могут либо отображаться, либо не отображаться на одном и том же экране в зависимости от каких-либо условий. Здесь мы рассмотрим пример диалогов о подтверждении действий пользователя, или алёртов (alerts).

Примеры диалоговых оконПримеры диалоговых окон

Наверняка вы сталкивались с подобными диалогами в различных мобильных приложениях. В наших двух приложениях, например, их больше 70. Они появляются в разных местах в ответ на разные действия пользователей. Что же представляют собой необязательные элементы на них?

Проанализируем скриншоты выше.

  • Скриншот 1: заголовок, описание и две кнопки.

  • Скриншот 2: заголовок, описание и одна кнопка.

  • Скриншот 3: описание и две кнопки.

Таким образом, необязательными элементами в этих диалогах являются заголовок и вторая кнопка. Локаторы у элементов на алёртах одинаковые вне зависимости от того, в каком месте приложения или после какого действия пользователя они появляются. Поэтому мы хотим реализовать проверки для всех типов диалогов один раз на их базовой странице, а потом наследовать от неё каждый конкретный алёрт, чтобы не пришлось повторять этот код. Создание подобных проверок мы и рассмотрим в этом разделе.

Начнём с того, как выглядит вызов метода для верификации каждого из диалогов:

class ClearAccountAlert < AppAlertAndroid  def verify_alert_lexemes    verify_alert(title:        ClearAccount::TITLE,                 description:  ClearAccount::MESSAGE,                 first_button: ClearAccount::OK_BUTTON,                 last_button:  ClearAccount::CANCEL_BUTTON)  endend
class WaitForReplyAlert < AppAlertAndroid  def verify_alert_lexemes    verify_alert(title:        WaitForReply::TITLE,                 description:  WaitForReply::MESSAGE,                 first_button: WaitForReply::CLOSE_BUTTON)  endend
class SpecialOffersAlert < AppAlertAndroid  def verify_alert_lexemes    verify_alert(description:  SpecialOffers::MESSAGE,                 first_button: SpecialOffers::SURE_BUTTON,                 last_button:  SpecialOffers::NO_THANKS_BUTTON)  endend

Во всех примерах мы вызываем метод verify_alert, передавая ему лексемы для проверки необходимых элементов. При этом, как вы можете заметить, WaitForReplyAlert мы не передаём лексему для второй кнопки, так как её не должно быть, а SpecialOffersAlert лексему для заголовка.

Рассмотрим реализацию метода verify_alert:

def verify_alert(title: nil, description:, first_button:, last_button: nil)  ui.wait_for_elements_displayed([MESSAGE, FIRST_ALERT_BUTTON])  ui.wait_for_element_text(expected_lexeme: title, locator: ALERT_TITLE) if title  ui.wait_for_element_text(expected_lexeme: description, locator: MESSAGE)  ui.wait_for_element_text(expected_lexeme: first_button, locator: FIRST_ALERT_BUTTON) ui.wait_for_element_text(expected_lexeme: last_button, locator: LAST_ALERT_BUTTON) if last_buttonend

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

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

Пользователь не понимает, что выбрать: обе кнопки как будто бы закрывают диалог. Это похоже на критический баг. Даже если приложение не падает, это нужно исправить. А тесты нам нужно изменить так, чтобы они выявляли подобные проблемы.

Для этого в тестах мы меняем проверку

ui.wait_for_element_text(expected_lexeme: title, locator: ALERT_TITLE) if title

на

if title.nil?  Assertions.assert_false(ui.elements_displayed?(ALERT_TITLE), "Alert title should not be displayed")else  ui.wait_for_element_text(expected_lexeme: title, locator: ALERT_TITLE)end

Мы изменили условие if и добавили проверку второго состояния. Если мы не передаём лексему для необязательного элемента, значит, этого элемента не должно быть на экране, что мы и проверяем. Если же в title есть какой-то текст, мы понимаем, что элемент с этим текстом должен быть, и проверяем его. Мы решили выделить эту логику в общий метод, который назвали wait_for_optional_element_text. Этот метод мы можем применять не только для диалогов из этого примера, но и для любых других экранов приложения, на которых есть необязательные элементы. Видим, что if-условие из примера выше полностью находится внутри нового метода:

def wait_for_optional_element_text(expected_lexeme:, locator:)  GuardChecks.not_nil(locator, 'Locator should be specified')  if expected_lexeme.nil?    Assertions.assert_false(elements_displayed?(locator), "Element with locator #{locator} should not be displayed")  else    wait_for_element_text(expected_lexeme: expected_lexeme, locator: locator)  endend

Реализация метода verify_alert тоже изменилась:

def verify_alert(title: nil, description:, first_button:, last_button: nil)  ui.wait_for_elements_displayed([MESSAGE, FIRST_ALERT_BUTTON])  ui.wait_for_optional_element_text(expected_lexeme: title, locator: ALERT_TITLE)  ui.wait_for_element_text(expected_lexeme: description, locator: MESSAGE)  ui.wait_for_element_text(expected_lexeme: first_button, locator: FIRST_ALERT_BUTTON)  ui.wait_for_optional_element_text(expected_lexeme: last_button, locator: LAST_ALERT_BUTTON)end

Мы всё так же ожидаем появления обязательных элементов на экране. После этого проверяем, что их текст совпадает с переданными в метод лексемами. В случае же с необязательными элементами теперь мы используем метод wait_for_optional_element_text, который помогает убедиться, что необязательный элемент не отображается тогда, когда не должен отображаться.

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

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

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

Общие рекомендации

Выделим основные рекомендации по автоматизации тестирования мобильных приложений из семи практик, которые мы описали:

  • так как проверки это то, ради чего мы пишем тесты, всегда используйте полную систему проверок;

  • не забывайте добавлять проверку предустановки для асинхронных действий;

  • выделяйте общие методы для переиспользования однотипного кода как в шагах, так и в методах на страницах;

  • делайте объект тестирования простым;

  • выделяйте независимые методы для простых действий в тестах.

Возможно, эти советы кому-то покажутся очевидными. Но мы хотим обратить ваше внимание на то, что применять их можно (и нужно) в разных ситуациях. Если вы хотите дополнить список другими полезными рекомендациями, добро пожаловать в комментарии!

Бонус

Мы подготовили тестовый проект, в котором отразили все практики, описанные в статье. Переходите по ссылке, изучайте и применяйте:

Mobile Automation Sample Project

Подробнее..

Перевод Что нам стоит автоматизацию построить три паттерна для повышения эффективности процессов

20.05.2021 20:12:40 | Автор: admin

Меня зовут Владислав Романенко, я старший iOS QA Engineer в Badoo и Bumble. Несколько лет назад мы начали активнее использовать автотесты в разработке, но столкнулись с некоторыми трудностями.

Логичный вопрос: почему мы занялись автоматизацией тестирования? Ответ кроется в еженедельном выпуске новых версий наших мобильных приложений. QA-отделу приходится тестировать новые фичи, их интеграцию в сервисы и заниматься регрессионным тестированием. За неделю комплексное регрессионное тестирование нужно проводить как минимум один раз, что довольно утомительно. Автоматизация помогла нам выпускать новые версии быстрее и без потери качества. Подробнее об автоматизации мобильного тестирования в докладе моих коллег.

В разработке проблемы часто решаются с помощью паттернов обобщённых решений для часто возникающих проблем в заданном контексте. То же и с автоматизацией тестирования, есть даже хорошее wiki-описание. В этой статье мы поговорим о паттернах процессов (Process Patterns). Они помогают организовать и улучшить процесс автоматизации тестирования.

Без дальнейших предисловий перейдём к трудностям, с которыми мы боролись, и рекомендациям по их преодолению с помощью тех самых паттернов. Замечу, что это не единственные проблемы, которые у нас были и есть. Но в рамках данной статьи я решил остановиться именно на них.

Паттерн 1. Просьба о помощи (Ask for Help)

Как это часто бывает, мы все имели разный опыт, знания и навыки. На тот момент не все в команде сталкивались с автоматизацией тестирования некоторым пришлось учиться на ходу. Чтобы помочь коллегам из команды QA справиться со сложностями, мы предложили им обратиться за помощью к команде автоматизации. Идея в том, что если ты застреваешь на какой-то проблеме, то не нужно самостоятельно бороться с ней слишком долго, ведь другие уже могли столкнуться с подобным и найти решение. А чтобы автоматизаторов не завалили вопросами, мы решили формализовать этот подход. Прекрасным решением оказался паттерн ASK FOR HELP:

Просите о помощи, а не теряйте время, пытаясь всё сделать самостоятельно.

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

Но оказалось, что простое следование паттерну может привести к новым проблемам. Команде автоматизации тестирования стали задавать вопросы ещё чаще, чем обычно (например, о настройке окружения). Со временем мы стали замечать, что разные члены команд задают одни и те же вопросы. У автоматизаторов уходило много времени на то, чтобы всем ответить, поскольку даже простой и хорошо сформулированный вопрос требует переключения контекста.

Внутрениий StackOverflow

Стоит отметить, что в нашей вики есть документация и описания решений для большинства распространённых проблем. Возможно, документировать все возможные ошибки и объяснения в вики не лучший подход, но мы считали, что сохранить их в одном месте будет полезно. Так у нас возникла идея создания локального Stack Overflow. Для этого мы воспользовались open source-решением Scoold. Оно предназначено для использования на первой линии обработки запросов и работает как обычная Stack Overflow, только для сотрудников компании. Когда у кого-нибудь возникает проблема, достаточно зайти в наш локальный Stack Overflow, чтобы найти решение или написать вопрос, на который ответит кто-то из специалистов.

Так выглядит наш локальный StackOverflowТак выглядит наш локальный StackOverflow

Преимущества

  • Процесс геймифицируется, поэтому в поиск решений вовлекается больше людей. Мы не стали придумывать разные лычки или звания количество голосов или звёзд ничего не значит и вам не советуем этим увлекаться. Геймификация лишь дополнительный стимул для участников, а не основной фактор.

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

  • Стало меньше сообщений и веток в чатах, где их трудно отслеживать и искать. Мессенджеры не лучшее место для хранения документации и обращения к ней как к источнику истины. Stack Overflow гораздо удобнее.

Недостатки

  • Нет ответственного за запуск реализации. Нужно, чтобы кто-то стимулировал создание даже простейшего решения. Нам повезло в нашем случаем таким человеком стал dr0pt4ble, который взял инициативу в свои руки. Не менее важно, чтобы руководство понимало и поддерживало такую инициативу.

  • Низкая популярность решения: платформа не сразу стала настолько востребованной, насколько мы ожидали. Любой новый инструмент должен сражаться за любовь пользователей, и это понятно, ведь не всем нравятся перемены. Возможно, мы не проявляли на сайте сверхактивность с первого дня, но постепенно смогли перенаправить туда почти все соответствующие вопросы, а также привлечь к решению проблем людей, не входящих в команду автоматизации тестирования.

  • На вопросы могут отвечать недостаточно быстро. На этот случай стоит реализовать уведомления в мессенджерах о появлении новых вопросов. Однако сегодня многие работают из дома и мессенджеры переполнены уведомлениями, что повышает соблазн отключить звук. Каждая команда решает эту проблему по-своему, но для нас появление напоминания в чате автоматизаторов в Slack работает хорошо.

Паттерн 2. Введение стандартов (Set Standards)

По мере возможности мы стараемся переиспользовать код в разных приложениях и на разных платформах, чтобы упростить сопровождение и поддержку. Но, как и в любой другой системе, которую несколько лет разрабатывало много людей, разные части кода могут быть написаны в разных стилях. Наш основной язык для сквозных автотестов это Ruby (об истории автоматизации мобильного тестирования в нашей компании рассказывал agile_seph здесь), потому что он как раз позволяет писать код в разных стилях (автор языка Юкихиро Мацумото хотел, чтобы разработчики использовали его язык так, как им удобно). Однако работать с чужим кодом бывает сложно.

Для решения этой проблемы мы выбрали паттерн SET STANDARDS:

Введите и соблюдайте стандарты для артефактов автоматизации.

Без этого может получиться, что над одним проектом работают несколько человек, и у каждого свой стиль и методы (как если бы все говорили на разных языках). Решать проблему мы начали со сбора мнений тех, кто непосредственно занимается автоматизацией. Мы стремились выработать такие соглашения о стиле, которые будут полезны всем пользователям. И чтобы каждый, кто участвовал в принятии решения, в будущем старался его придерживаться. Так мы создали руководства, которые принимают и соблюдают все участники процесса, что в буквальном смысле помогло нам найти общий язык.

Что мы сделали

  • Создали локальную вики с актуальными руководствами и документами (например, как настроить окружение, как запускать и поддерживать тесты), которая помогла сократить циклы ревизии кода. Всю документацию мы храним в Confluence, который разделили на несколько частей. Команды регулярно проверяют актуальность своей документации.

  • Добавили в вики межкомандные ретроспективы и соглашения. Ежемесячно мы проводим встречи и по результатам каждой из них делаем какие-то практические выводы.

  • Что касается самого кода тестов и ограничений на уровне кода для основных этапов и методов верификации, мы внедрили стандартные инструменты, включая RuboCop (статический анализатор и инструмент форматирования кода для Ruby), защитные проверки (Guard Cheсks) и pre-commit-хуки.

  • Вычистили устаревший код и выделили универсальные параметризированные этапы и методы. Рекомендовали не создавать собственные методы, если можно воспользоваться уже существующими. Образец проекта с некоторыми из наших лучших практик можно посмотреть здесь.

  • Из участников команды автоматизации тестирования сформировали комитет качества кода. У него есть список вопросов и задач, поучаствовать в решении которых может любой желающий. Мы также приглашаем на встречи комитета всех, у кого есть вопросы, чтобы помочь в их решении.

Преимущества

  • На сопровождение тестов и исправление ошибок тратится меньше времени и сил. Руководство по написанию кода очень облегчает понимание того, что и как писать.

  • На ревью кода уходит меньше времени. С одной стороны, код становится аккуратнее, с другой всегда есть источник истины для рецензента и рецензируемого.

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

Недостатки

  • После создания документации её никто не сопровождает. Это общая проблема для любой документации, особенно для той, что не используется постоянно. Поэтому мы назначили ответственных за сопровождение документов и настроили отправку им на почту уведомлений из Confluence, чтобы ничьи комментарии не оставались без внимания.

  • Трудно организовывать ретроспективные встречи и привлекать людей. Непросто найти время, когда все заинтересованные сотрудники не заняты решением своих задач. Поэтому такие встречи приходится планировать сильно заранее.

Паттерн 3. Делимся информацией (Share Information)

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

Этот паттерн называется SHARE INFORMATION:

Просите и давайте информацию начальству, разработчикам и другим тестировщикам.

Для нас это способ улучшить автоматизацию тестирования за счёт более широкого набора знаний. Для реализации этого паттерна мы запустили еженедельные короткие презентации QA Lightning Talks. Любой человек может предложить тему и в течение 10-15 минут выступить с ней. Поскольку у встреч чёткий тайминг, это хорошая возможность узнать что-то новое, не потратив на это много времени. Для тех, кто не смог присутствовать, мы сохраняем видеозаписи встреч во внутренней библиотеке.

Доклады могут быть посвящены не только тестированию и его автоматизации. Например, однажды bayandin рассказывал о своём опыте поддержки проекта Homebrew. А я как-то рассказывал о том, что я диванный картограф в Humanitarian OpenStreetMap. Я создаю карты районов, которые плохо картографированы, и потом ими пользуются в работе сотрудники разных организаций вроде Красного Креста или Врачей без границ. Сокращённая версия этого доклада позднее была представлена на сессии Soapbox конференции EuroSTAR 2020.

Преимущества

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

  • Общий объём знаний растёт. А согласно исследованию, обмен знаниями улучшает взаимодействие между департаментами и внутри них.

  • Дополнительная мотивация к саморазвитию и росту. Когда видишь, что делают другие и как ты можешь им помочь, это может вдохновить научиться чему-то новому или углубить знания.

Недостатки

  • Не все хотят делать доклады. Мало того, что нужно сделать презентацию, так ещё и необходимо набраться смелости и приложить усилия, чтобы выступить перед аудиторией. Мы сгладили этот момент, ограничив длительность выступлений и предлагая помощь и поддержку всем, кто хочет выступить. Многие уже сделали свои первые шаги на этом поприще.

  • Трудно удерживать интерес аудитории и увеличивать ее. Мы каждый раз меняем тему и сосредотачиваемся на актуальных проблемах. Например, один из проектов заключался в том, что мы собирали результаты всех тестовых прогонов тикета в таблицу и интегрировали её в Jira. Об этом на ближайшей встрече и рассказал один из участников команды автоматизации тестирования.

Бонус: паттерн для обучения разделение на пары (Pair Up)

Расскажу ещё о том, к чему привели вышеописанные шаги. Например, QA-инженеры стали готовы брать (и постепенно взяли) ответственность за написание, сопровождение и анализ нестабильных автоматизированных тестов. Поэтому мы решили пойти дальше сделать так, чтобы они взяли на себя ещё и фреймворк автоматизации тестирования и некоторые связанные с инструментарием задачи. Такой шаг был бы невозможен, если бы тестировщики не смогли, помимо своих повседневных задач, ещё и изучать фреймворк и плотно работать с кодовой базой. И хотя регулярные задачи решались качественно, казалось, что выйти за их пределы без помощи будет непросто.

Чтобы достичь цели, мы воспользовались паттерном PAIR UP:

Более опытный сотрудник получает себе в пару менее опытного.

Для QA-инженеров мы запустили внутреннюю программу менторства. Что это такое? QA-инженеры присоединяются к автоматизаторам на короткий промежуток времени (обычно на две недели) и работают над задачами из бэклога этой команды, а не из своего. Помогают им в этом менторы из числа старших инженеров-автоматизаторов. Цель внутреннего менторства заключается в обучении. Мы разработали такой манифест:

  • Сосредоточенность на обучении, а не на строгом соблюдении сроков.

  • Интерактивные обсуждения, а не работа в бункере.

  • Ранняя и регулярная обратная связь, а не ретрообзор в конце менторства.

Хотя утверждения справа ценны, утверждения слева для нас ценнее.

Подробнее о менторстве, о том, почему и как мы его практикуем, я рассказывал в недавнем выступлении.

Итоги

Применение этих паттернов помогло нам решить несколько сложных вопросов и в коммуникации, и в работе с кодом. Мы вышли на новый уровень автоматизации при этом процессы налажены, а люди в команде не перегружены работой.

Какие проблемы мы смогли решить

  • Нежелание обращаться за помощью к коллегам (решили с помощью паттерна Ask for help). Как отметили в своей книге A Journey through Test Automation Patterns: One teams adventures with the Test Automation Серетта Гамба и Дороти Грэхем, не бойтесь просить о помощи: большинству людей на самом деле нравится помогать.

  • Высокая стоимость сопровождения кодовой базы тестирования (решили с помощью паттерна Set Standards). Если вы давно работаете над автоматизацией, то стандарты просто необходимы.

  • Информационный бункер (решили с помощью паттерна Share Information). Общайтесь с другими людьми: в процессе обсуждения часто рождаются новые идеи.

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

Расскажите в комментариях, с какими сложностями при автоматизации тестирования столкнулись вы и как их решали.

Подробнее..

Перевод Неблокирующие ошибки метода assert в Pytest-check

17.12.2020 18:19:07 | Автор: admin

В преддверии старта курса "Python QA Engineer" для будущих студентов и всех интересующихся темой тестирования подготовили перевод полезного материала.

Также приглашаем посмотреть подарочное демо-занятие на тему "Карьера QA".


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

Какой бы подход вы ни выбрали, я думаю, можно с уверенностью сказать, что тесты должны оставаться ясными, краткими, читаемыми и, конечно же, их должно быть просто поддерживать. Лично у меня не возникает проблем с несколькими assert-ами в одном тесте, поскольку я фокусируюсь на одной функциональной области.

Например, возьмем форму регистрации:

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

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

Проблема

Проблема встроенного метода assert в Python заключается в том, что он останавливает выполнение теста при первой ошибке. В некоторых сценариях в этом и вправду есть смысл. То есть нет смысла продолжать тест, если часть приложения не работает. Однако бывает и такое, что есть смысл провести все проверки независимо от того, выпадет ли ошибка на одной из них или нет, особенно есть все ваши assert-ы относятся к одной странице в конце теста.

Такой подход мы используем при тестировании веб-сервисов. При проверке тела ответа мы, скорее всего, захотим проверить каждое возвращаемое значение, а затем вывести, какие из них были не такими, как ожидалось, вместо того чтобы останавливать тест при ошибке в первом assert.

Решение: плагин Pytest-check

Pytest-check (от Брайана Оккена) плагин Pytest, который позволяет вам обернуть все assert-ы теста в один результат pass/fail. Например, если у вас есть 3 assert-а и первый из них выдал fail, то Pytest-check продолжит выполнять оставшиеся 2. Затем он сообщит о том, что тест провалился, если одна или несколько проверок придут с результатом fail.

Python OpenSDK TestProject также поддерживает Pytest, поэтому если у вас уже есть тесты pytest на Selenium, их очень легко преобразовать в тесты TestProject. Вы можете узнать больше об этом в моей статье в блоге HowQA, а также ознакомиться с пошаговым руководством по началу работы здесь.

Давайте рассмотрим, как работает плагин Pytest-check.

Тесты Selenium

Для начала нам понадобится тест. Я воспользуюсь страницей регистрации этого приложения: https://docket-test.herokuapp.com/register

import selenium.webdriver as webdriverfrom selenium.webdriver.common.by import Bydef test_register_user():    # Arrange    url = "https://docket-test.herokuapp.com/register"    # set the driver instance    driver = webdriver.Chrome()    # browse to the endpoint    driver.get(url)    # maximise the window    driver.maximize_window()    # Act    # Complete registration form    # enter username value    driver.find_element(By.ID, "username").send_keys("Ryan")    # enter email value    driver.find_element(By.ID, "email").send_keys("Test@email.com")    # enter password value    driver.find_element(By.ID, "password").send_keys("12345")    # enter repeat password value    driver.find_element(By.ID, "password2").send_keys("12345")    # click register button    driver.find_element(By.ID, "submit").click()

У нас есть тест, но нужно написать несколько проверок. Давайте сделаем это с помощью метода assert:

# Assert# confirm registration has been successful# check if congratulations message contains the correct textmessage = driver.find_element(By.XPATH, "/html[1]/body[1]/div[1]/div[1]/div[1]/div[1]/form[1]/div[1]").textassert message == "Congratulations, you are now registered"# check user is routed to login pagecurrent_url = driver.current_urlassert current_url == "https://docket-test.herokuapp.com/login"

Если мы их запустим, то все пройдет успешно:

Пока все хорошо, но что случится, если оба assert-а вернутся с результатом fail? Давайте изменим код, чтобы посмотреть, что будет:

# Assert# confirm registration has been successful# check if congratulations message contains the correct textmessage = driver.find_element(By.XPATH, "/html[1]/body[1]/div[1]/div[1]/div[1]/div[1]/form[1]/div[1]").textassert message == "Well done, You've Registered"# check user is routed to login pagecurrent_url = driver.current_urlassert current_url == "https://docket-test.herokuapp.com/register"driver.quit()

Итак, мы поменяли сообщение, которое ожидаем увидеть и изменили ожидаемый URL, поэтому, когда мы снова выполним этот тест, он вернется с результатом fail:

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

Однако из сообщения об ошибке мы видим, что тест упал из-за формулировки сообщения, а URL он даже не проверил. Тест перестает выполняться, как только появляется первый fail, поэтому у нас нет возможности увидеть всю картину. В таком случае нам нужно будет исправить текст ожидаемого сообщения и повторить тест еще раз. Давайте так и сделаем.

Теперь мы поменяли ожидаемое сообщение обратно на Congratulations, you are now registered, и снова можем запустить тест:

Ага! Тест снова упал, на этот раз из-за неправильного URL.

Знаю, что вы думаете о том, как было бы круто, если бы мы поймали обе этих ошибки за один прогон теста. Что ж, вам повезло, в игру вступает Pytest-check.

Pytest-Check

Установка

Мы можем установить pytest-check через pip install pytest-check. После того как мы установили pytest-check, его можно импортировать в наш тест.

import pytest_check as check

Теперь, когда все готово, можно немного подправить наши assert-ы. Отныне мы не будем использовать оператор assert, вместо этого мы будем использовать синтаксис pytest-check следующим образом.

Чтобы проверить сообщение воспользуемся функцией check.equal и добавим туда ожидаемый и фактический текст, например:

check.equal(message, "Congratulations, you are now registered1")

Мы можем сделать то же самое и с проверкой URL-адреса, но сделаем это с помощью другого метода, а именно check.is_in.

check.is_in("login", current_url)

Полный тест выглядит следующим образом:

import selenium.webdriver as webdriverfrom selenium.webdriver.common.by import Byimport pytest_check as checkdef test_register_user():    # Arrange    url = "https://docket-test.herokuapp.com/register"    # set the driver instance    driver = webdriver.Chrome()    # browse to the endpoint    driver.get(url)    # maximise the window    driver.maximize_window()    # Act    # Complete registration form    # enter username value    driver.find_element(By.ID, "username").send_keys("Ryan8")    # enter email value    driver.find_element(By.ID, "email").send_keys("Test@email8.com")    # enter password value    driver.find_element(By.ID, "password").send_keys("12345")    # enter repeat password value    driver.find_element(By.ID, "password2").send_keys("12345")    # click register button    driver.find_element(By.ID, "submit").click()    # Assert    # confirm registration has been successful    # check if congratulations message contains the correct text    message = driver.find_element(By.XPATH, "/html[1]/body[1]/div[1]/div[1]/div[1]/div[1]/form[1]/div[1]").text    check.equal(message, "Congratulations, you are now registered")    # check user is routed to login page    current_url = driver.current_url    check.is_in("login", current_url)    driver.quit()

На этом этапе мы можем вернуть все ожидаемые значения в исходное состояние, чтобы тест завершился успешно. Так что давайте так и поступим.

Отлично! Теперь давайте посмотрим, что произойдет, если обе проверки вернут fail. Для начала нужно будет поменять код, чтобы они провалились:

# check if congratulations message contains the correct textmessage = driver.find_element(By.XPATH, "/html[1]/body[1]/div[1]/div[1]/div[1]/div[1]/form[1]/div[1]").textcheck.equal(message, "Congratulations, you are now registered!")# check user is routed to login pagecurrent_url = driver.current_urlcheck.is_in("1", current_url)

Запускаем.

Как и в прошлый раз, тест завершился неудачей, как мы и ожидали, но теперь мы видим, что fail вернулся в двух проверках: сначала вывод о том, что содержание сообщения не соответствует ожидаемому, а потом результат проверки URL. Если подойти к вопросу с точки зрения pytest, то можно рассматривать это как один провалившийся тест, но теперь-то мы знаем, что на самом деле несколько проверок вернули fail.

Только если все проверки в тесте будут пройдены успешно, он будет помечен как pass.

Вот такой крутой Pytest-check. Узнать о нем больше вы можете в документации.


- Узнать подробнее о курсе "Python QA Engineer".

- Посмотреть демо-занятие на тему "Карьера QA".

ЗАБРАТЬ СКИДКУ

Подробнее..

Перевод Автоматизация тестирования на Python Шесть способов тестировать эффективно

03.06.2021 18:21:18 | Автор: admin

Мы уже говорили об автоматизации тестирования, теперь пришло время познакомиться с шестью лучшими инструментами автоматизации тестирования на Python.

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

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

Итак, начнем.

PyUnit и Nose2

PyUnit это фреймворк юнит-тестирования на Python. Его добавили в стандартную библиотеку Python еще в версии 2.1, он совместим со всеми последующими версиями языка. PyUnit это реализация JUnit на Python, стандартного фреймворка юнит-тестирования Java. Именно поэтому разработчики, которые переходят с Java на Python найдут его очень простым в использовании. Оба фреймворка обязаны своим существованием фреймворку для тестирования на Smalltalk от Кента Бека.

PyUnit содержит все необходимые инструменты для создания автоматизированных тестов.

  • Фикстуры, с помощью которых вы можете создавать и удалять объекты, необходимые для теста.

  • Методы для выполнения тестов.

  • Наборы для группировки классов тестов в логические юниты.

  • Раннеры для выполнения тестов.

Вот пример базового юнит-теста:

import unittest     class SimpleWidgetTestCase(unittest.TestCase):        def setUp(self):            self.widget = Widget("The widget")     class DefaultWidgetSizeTestCase(SimpleWidgetTestCase):        def runTest(self):            assert self.widget.size() == (50,50), 'incorrect default size'

SimpleWidgetTestCase использует фикстуру setUp, чтобы создать Widget для тестирования. DefaultWidgetSizeTestCase это класс-наследник SimpleWidgetTestCase, который проверяет размер Widget.

PyUnit отличная вещь для начала настройки автоматизации тестирования на Python, но это лишь базовый набор инструментов. Вам еще понадобятся инструменты для автоматизации выполнения тестов и сбора результатов. Здесь в игру вступает Nose.

Nose2 это следующий шаг после PyUnit. Он добавляет поддержку автоматического обнаружения тестов и плагины для выполнения тестов и создания документации. Система плагинов Nose2 добавляет дополнительный функционал в виде декораторов, параметризированных тестов и поиска тестов. Например, AllModules находит все тесты и собирает из них выходные данные.

Nose2 также содержит Such DSL для написания функциональных тестов.

Если вы поместите код в файл с именем test_widgets.py, тест-раннер Nose2 найдет тест и запустит его. Все, что вам нужно сделать это добавить в ваши файлы префикс tests_.

PyTest

PyTest (https://pytest.org/en/latest/) нативная библиотека тестирования на Python, она содержит расширенный набор функций PyUnit. По сравнению с моделированием архитектуры JUnit, она определенно написана в стиле Python. Она активно использует декораторы и ассерты Python.

PyTest также поддерживает параметризированное тестирование (без плагинов по типу Nose), что упрощает переиспользование кода и его покрытие тестами.

Если вы перепишете под Pytest тот тест, который мы написали выше, он будет выглядеть более декларативным.

@pytest.fixturedef widget():    return Widget("The widget") def test_widget_size(widget):    assert widget.size() == (50,50), 'incorrect default size'

PyTest использует тестовые фикстуры для передачи Widget методу тестирования.

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

PyTest упрощает создание отчетов в виде обычного текста, XML или HTML. Также вы можете добавить информацию о покрытии кода в отчеты PyTest.

Несмотря на то, что PyTest можно использовать самостоятельно, вы можете интегрировать его с другими фреймворками тестирования и тест-раннерами, такими как PyUnit и Nose2. Благодаря такой совместимости PyTest станет отличным выбором для растущих проектов, которым нужно хорошее покрытие тестами. Для PyTest нужен Python 3.6 или более поздние версии.

Behave

PyUnit и PyTest мощные традиционные фреймворки для юнит-тестирования, но что, если вам нужны behavior-driven тесты?

Behave это behavior-driven (BDD) фреймворк для тестирования. Он критически отличается от PyUnit и PyTest. В нем вы пишете тесты на Gherkin вместо Python. Несмотря на то, что здесь не оригинальный Gherkin от Cucumber, в Behave есть полная поддержка Gherkin, поэтому он является одним из самых популярных BDD-фреймворков для Python.

Behave настолько распространен, что даже у Jetbrains есть для него плагин в PyCharm Professional Edition. Также существует множество онлайн-руководств и документации для работы с Behave.

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

Если вы интересуетесь или даже уже используете behavior-driven разработку (BDD), Behave один из лучший вариантов для этого. Он поставляется с интеграциями как для Django, так и для Flask, так что вы можете использовать его в full-stack проектах.

Тест из предыдущих примеров можно реализовать на Behave, как представлено ниже.

Вот грамматика естественного языка:

Feature: widget size  Scenario: verify a widget's size     Given we have a widget     When the widget is valid     Then the size is correct

А вот код на Python. У Given, When и Then есть соответствующие аннотации.

from behave import *@given('we have a widget')def step_given_a_widget(context):    context.widget = Widget('The widget')   @when('the widget is valid')def step_widget_is_valid(context)    assert context.widget is not None@then('the size is correct')def step_impl(context):    assert context.widget.size() == (50,50)

Lettuce

Lettuce это behavior-driven инструмент автоматизации для Selenium и Python. Подобно Behave, он использует синтаксис Gherkin для описания тестовых сценариев, но у него не такая совместимость, как у Behave. Lettuce не так распространен, как Behave, однако он хорошо работает с небольшими проектами.

Его также легко интегрировать с другими фреймворками, такими как Selenium и Nose.

Тесты на Lettuce чем-то напоминают тесты на Behave. Вот как это выглядит на естественном языке:

Feature: widget size Scenario: verify a widget's size     Given we have a widget     When the widget is valid     Then the size is correct

А вот код. Вместо отдельной аннотации для каждого шага теста, Lettuce аннотирует сам step.

from lettuce import stepfrom lettuce import world @step('we have a widget') def step_given_a_widget(step):    world.widget = Widget('The widget') @step('the widget is valid')def step_widget_is_valid(step)     assert world.widget is not None @step('the size is corrrect') def step_impl(context):     assert world.widget.size() == (50,50)

Когда вы интегрируете Lettuce с Selenium, у вас получается надежный фреймворк для тестирования приложений на Django. Так что, если вам не нравится синтаксис Jasmineс JavaScript, этот вариант может оказаться наилучшим.

Однако Lettuce не обновлялся с 2016 года. Вы все еще можете скачать его и использовать в коде, но больше он не поддерживается.

Jasmine для автоматизации тестирования на Python

BDD не просто популярная парадигма разработки на Python, также она широко распространена в веб-разработке. Jasmine популярный фреймворк для тестирования веб-приложений в стиле BDD. Скорее всего вы думаете о Jasmine, как об инструменте тестирования приложений на JavaScript, но вы вполне можете использовать его для автоматизации тестирования на Python.

Благодаря Jasmine-Py вы можете добавить Jasmine в свои проекты на Django. Так вы сможете запускать Jasmine из вашей среды Python и с вашего сервера CI/CD.

Тестирование веб-приложений на основе поведения, а не DOM, делает ваши тесты более устойчивыми к изменениям. Это становится огромным преимуществом в тот момент, когда вы тестируете как код на Django создает страницы. Вместо Gherkin вы будете писать тесты в грамматике Jasmine.

Результаты можно применить как к своему веб-сайту, так и к коду на Django.

Фреймворк Robot

Фреймворк Robot это открытый фреймфорк автоматизации тестирования. Организации используют его для автоматизации приемочного тестирования. Вы пишете тесты в DSL фреймворка Robot, синтаксисе, который используется для создания приемочных тестов.

Вместо того, чтобы ориентироваться на поведение, как в Jasmine, Robot ориентируется на ключевые слова.

Ключевое слово это любая функция или метод, которые вы можете вызвать в тесте. Ключевые слова определяются либо в Robot, либо в основной системе, либо в пользовательских библиотеках для тестирования. Также вы можете определять новые ключевые слова в терминах уже существующих ключевых слов.

Можно расширить возможности Robot с помощью библиотек для тестирования, написанных на Python или Java. Таким образом, в дополнение к использованию этого фреймворка для тестирования кода на Python, вы можете расширить Robot с помощью Python. Также у вас есть доступ к обширной библиотеке плагинов для Robot.

DSL фреймворка позволяет легко создавать сценарии для автоматизации тестирования. С помощью правильного набора плагинов вы можете автоматизировать почти любой аспект приемочного тестирования. Еще вы можете создавать новые ключевые слова более высокого уровня, используя уже существующие.

Вам определенно нужна автоматизация тестирования на Python

В последние десять лет популярность Python неуклонно росла. Ее рост вы можете увидеть в индексе TIOBE. Велика вероятность, что вы уже пишете на Python или планируете добавить его в свой инструментарий в ближайшее время.

Расширение области применения Python привело к распространению фреймворков, инструментов тестирования и других утилит. Вне зависимости от того, создаете ли вы REST-сервис на бэкенде или любое другое приложение, для вас найдется подходящий фреймворк для автоматизированного тестирования.

Какой из них будет лучше отвечать вашим потребностям? У Testim есть руководство, которое поможет вам принять взвешенное решение. Обратитесь к нему и начните тестировать на Python уже сегодня.


Перевод подготовлен в рамках курса "Python QA Engineer". Приглашаем всех желающих на день открытых дверей онлайн: на этой встрече узнаете больше о программе курса и формате обучения, познакомитесь с преподавателем. Регистрация здесь.

Подробнее..

Test Suite удобный инструмент автоматического тестирования

03.12.2020 18:17:56 | Автор: admin
image

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

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

Чтобы сделать выбор в сторону автоматизации, следует разобраться в ее плюсах и минусах.

Преимущества, которые дает тестировщику автоматизация:
  • Человеческий фактор сведен практически нулю скрипт не может ничего перепутать или запустить нерелевантный тест.
  • Скорость тестирования автоматизация позволяет прогнать за сравнимое время в несколько раз больше тестов.
  • Оптимизация усилий по повторению тестовых сценариев на поддержку существующих скриптов при автоматизации тратится гораздо меньше времени, чем на постоянное выполнение одного и того же сценария вручную.
  • Гибкое время исполнения тестировать можно в любое удобное время, а не только в рабочее.


Вместе с тем автоматизация имеет место и ряд недостатков, таких как:
  1. Если тестируемое ПО часто модифицируется, это будет требовать постоянных затрат на поддержку скриптов в актуальном состоянии.
  2. Изначально большие затраты на разработку автоматизированных тестов, делающие нецелесообразной автоматизацию разовых задач.
  3. Стоимость программных платформ автоматизации может быть достаточно высокой, а у бесплатных инструментов, обычно, более скромный функционал, меньше возможностей из коробки и меньшее удобство работы.


Что нужно учитывать перед автоматизацией тестирования?
  1. Насколько хорошо инструмент для автоматизации распознает элементы управления в приложении, с которым придется работать, это особенно актуально для, например, мобильных приложений или толстых клиентов, особенно написанных на старых платформах, таких как Delphi. Если элементы не распознаются, то необходимо найти плагин или соответствующий модуль. Если вы не можете надежно работать с тем приложением, тестирование которого надо автоматизировать, инструмент вам не подходит.
  2. Оценить время, которое требуется на поддержку скриптов, написанных с помощью выбранного инструмента, основные проблемы и сложности. Часто усилия требуются только на первых этапах работы, при освоении нового приложения, но иногда выбранный инструмент настолько неудобен, что лекарство становится хуже болезни
  3. Понять насколько удобно пользоваться инструментом для написания новых скриптов. Сколько требуется на это времени, насколько можно структурировать код, насколько он читаем, есть ли поддержка библиотек, работа с репозиториями кода и т.д.


Автоматизация тестирования с помощью RPA



Роботизация бизнес-процессов (RPA) интенсивно развивается и, в силу сходства бизнес задач и подходов, может быть полезной в автоматизации тестирования и разработки. При том, что по миру сейчас процент покрытия автоматизированным тестированием в среднем не больше 30%, применение гибких и простых инструментов, таких как RPA, может помочь его поднять до приемлемых величин (считается, что хорошим процентом покрытия для автоматизации тестирования является 60-70%).

Частые изменения экосистемы приложения
Мы уже упоминали среди системных минусов автоматического тестирования частые изменения тех продуктов, с которыми надо работать. К сожалению, пока эта проблема не решена вендорами и если ваша среда тестирования постоянно изменяется это будет серьезно ограничивать возможности для его автоматизации.

Современные решения, такие как UiPath RPA позволяют частично эту проблему снять за счет применения умного захвата элементов UI, понимающего, что внешний вид приложения или структура может, в определенных пределах, меняться; и репозитория объектов, который позволяет централизованно управлять таксономией UI элементов.

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

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

Отсутствие тестовых данных и сред
Это большая проблема: для того чтобы сделать хороший тест нужно иметь реальные данные. В свою очередь для этого нужно работать с живой системой, в которой ничего нельзя менять. Нельзя в действующем электронном магазине купить товаров на 100 тысяч, так как собьется вся статистика. Теоретически у тестировщика для работы должен быть тестовый магазин-двойник с теми же самыми данными, но, к сожалению, реализовать подобное очень сложно и, зачастую, непосильно дорого. Для банковских систем эта проблема еще более актуальна и в этой сфере еще меньше реальных тестовых данных.

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

Наличие user friendly инструментов автоматизации
Инструмент для автоматизации тестирования должен быть гибким и легким в освоении, это снижает порог вхождения и позволяет большему количеству сотрудников создавать тесты. Платформа UiPath дружественна к пользователю и наличие онлайн академии, форума, telegram-сообщества в России и т.д. позволяет быстро обучаться. Освоение инструментария UiPath до уровня, необходимого для создания хороших кейсов, намного проще, чем обучение хардкорным вещам типа Selenium. При этом для тех, кто уже уверенно владеет подобными инструментами, изучение UiPath не составит никакой сложности.

На рынке сегодня существует потребность в инструменте, который облегчил бы тестировщикам и инженерам по автоматизации работу с вышеупомянутыми пробелами. Решение Test Suite призвано сделать тестирование и его автоматизацию интуитивно понятными и простыми в обслуживании, так, чтобы у компаний не было больших расходов.

Преимущества Test Suite



Один инструмент для RPA и автоматизации тестирования

Платформа UiPath многофункциональна, совмещает в себе возможности для роботизации и автоматизации тестирования. Это позволяет обмениваться артефактами автоматизации, созданными во всей организации. Кроме того, в рамках тестового пространства платформа способна интегрироваться и работать более чем со 190 технологиями, что значительно облегчает ее внедрение.

Замена устаревших систем на современные
В любой большой экосистеме предприятия или организации функционирует большое количество разных приложений. Совершенно обычна ситуация, когда рядом работают приложения, выпущенные в 90-м году и в 2020, веб-сайты на разных движках и мобильные приложения на разных технологиях. Проблема зоопарка систем при тестировании заключается в том, что определенный инструмент подходит для тестирования одного-трех приложений, но не всех сразу. Есть приложения, которые хорошо тестируют веб-сайты и совсем не умеют работать с толстым клиентом. Test Suite позволяет создавать единую экосистему и эффективно тестировать ПО различных категорий и версий. В Test Suite можно одновременно тестировать мобильное приложение и веб-ресурсы и не переключаться между большим количеством разных окон.

image

Минимальные знания программирования
Тестировщик должен заниматься тестированием системы, а не беспокоиться о кодировании. С помощью Test Suite он получает универсальный инструмент тестирования все задачи, которые у него есть он может решать в одном инструменте с единой методологией и единым подходом, не вдаваясь в детали реализации.

image

Оркестровка корпоративного уровня
С помощью UiPath можно тестировать и живое, находящееся в эксплуатации, ПО, не обязательно в тестовом контуре. Для этого используются те же технологии, что и для роботизации реальных бизнес-процессов.

image

Test Suite хорошо интегрируется с CI/CD, в нем есть готовые коннекторы к большинству основных платформ issue tracking, плагины к Jira и SAP Solution Manager.

Простота создания и обслуживания
Решение для тестирования UiPath демонстрирует не только простоту использования, но и снижает затраты на техническое обслуживание. Некоторые из клиентов UiPath уже сообщили о двукратном увеличении тестового покрытия с помощью Test Suite.

При всех преимуществах автоматизации тестирования с помощью Test Suite, надо понимать, что инструмент не может заменить полностью человека, но он, безусловно, поможет сделать работу тестировщика более легкой и полезной.
Подробнее..

Перевод Тестирование в Puppeteer vs Selenium vs Playwright сравнение производительности

29.01.2021 10:18:54 | Автор: admin

Ранее мы уже писали о том, когда бывает нужна автоматизация тестирования и какие проверки при этом используют. Сегодня предлагаем обсудить использование инструментов на практике и оценить их производительность. С разрешения Giovanni Rago автора серии полезных материалов о тестировании мы перевели его статью Puppeteer vs Selenium vs Playwright: сравнение скорости (Puppeteer vs Selenium vs Playwright, a speed comparison). Статья будет интересна тем, кто задумывается о выборе подходящего инструмента автоматизации в своих проектах.

От автора:

Для разработки системы мониторинга и тестирования Checkly мы решили использовать Puppeteer. Это инструмент автоматизации тестирования с возможностью включения headless браузера и открытым исходным кодом, позже мы также внедрили Playwright. Checkly помогает узнать, работает ли тестируемый сайт так, как ожидается в определенный момент времени. В нашем случае основной интерес вызывала скорость работы инструмента.

Задача определения наиболее быстрого инструмента автоматизации не так проста. Поэтому мы решили провести свой бенчмарк тест производительности, чтобы сравнить новичков Puppeteer и Playwright с ветераном WebDriverIO (в связке с Selenium и протоколом автоматизации DevTools).

В результате проведения тестов мы сделали неожиданные открытия: например, Puppeteer работает быстрее на небольших скриптах, а WebDriverIO показывает больший разброс на длинных сценариях. Далее подробнее расскажем о наших результатах и о том, как мы их получили.

Почему мы сравниваем эти инструменты автоматизации?

Рассматривать Puppeteer/Playwright и Selenium это всё равно что сравнивать яблоки с апельсинами: инструменты имеют существенно разные возможности и применяются в разных областях автоматизации, и тот, кто их оценивает, должен учитывать это, прежде чем анализировать скорость.

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

Важно заметить, что WebDriverIO это высокоуровневый фреймворк с множеством полезных функций, который может запускать автоматизацию в нескольких браузерах, используя различные встроенные инструменты.

Опыт показывает, что большинство пользователей Selenium, которые работают с JavaScript, используют WebDriverIO для запуска автоматизированных скриптов. Именно поэтому мы выбрали его. Также нам было интересно протестировать новый DevTools режим.

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

Методология, или как мы запускали тесты

Можете смело пропустить этот раздел, если хотите сразу перейти к результатам. Но мы рекомендуем всё же ознакомиться с ним. Это поможет лучше понять результаты тестов.

Общие рекомендации

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

  1. Равенство ресурсов: каждый тест запускался последовательно на одной и той же машине в состоянии покоя, то есть ни одна ресурсозатратная рабочая нагрузка не выполнялась в фоновом режиме во время бенчмарка, так как это могло бы исказить измерения.

  2. Простое выполнение: скрипты запускались так, как это было показано в документации к каждому инструменту и с минимальными конфигурациями. Например, для Playwright node script.js

  3. Сопоставимые скрипты: мы стремились минимизировать разницу в скриптах, которые использовали для бенчмарка. Некоторые инструкции всё ещё нуждаются в добавлении, удалении, настройке, чтобы добиться стабильного выполнения.

  4. Самые свежие версии: для тестирования всех инструментов мы использовали их последние версии.

  5. Одинаковый браузер: все скрипты выполнялись в headless Chromium.

В следующем разделе представлена дополнительная информация по всем пунктам.

Техническая настройка

Для каждого теста на производительность мы собрали данные из 1000 успешных последовательных выполнений одного скрипта.

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

Все тесты мы проводили на MacBook Pro 16 последнего поколения под управлением macOS Catalina 10.15.7 (19H2) со следующими характеристиками:

Модель: MacBookPro16,1

Процессор: 6-Core Intel Core i7

Скорость процессора: 2,6 ГГц

Количество процессоров: 1

Количество ядер: 6

Кэш-память L2 (на ядро): 256 Кб

Кэш-память L3: 12Мб

Технология Hyper-Threading: включена

Память: 16 Гб

Мы использовали следующие зависимости:

bench-wdio@1.0.0 /Users/ragog/repositories/benchmarks/scripts/wdio-selenium

@wdio/cli@6.9.1

@wdio/local-runner@6.9.1

@wdio/mocha-framework@6.8.0

@wdio/spec-reporter@6.8.1

@wdio/sync@6.10.0

chromedriver@87.0.0

selenium-standalone@6.22.1

scripts@1.0.0 /Users/ragog/repositories/benchmarks/scripts

playwright@1.6.2

puppeteer@5.5.0

Скрипты, которые мы использовали вместе с результатами их выполнения, вы можете найти в GitHub-репозитории.

Измерения

Мы получили следующие показатели, все рассчитано на основе 1000 прогонов:

* Среднее время выполнения (в секундах).

* Стандартное отклонение (в секундах): показатель разброса времени выполнения.

* Коэффициент вариации (CV): безразмерный коэффициент, который показывает отклонение результатов от среднего.

* P95 (изменение 95-го процентиля): наибольшее значение, оставшееся после отбрасывания верхних 5% отсортированного списка полученных данных. Интересно было бы узнать, как выглядит не экстремальное, но все еще высокое значение.

Что мы не измерили (пока)

* Надежность: ненадежные сценарии быстро становятся бесполезными, независимо от того, насколько быстро они выполняются.

* Эффективность распараллеливания: параллельный запуск очень важен в контексте инструментов автоматизации. Однако в этом случае мы в первую очередь хотели понять, с какой скоростью может выполняться один скрипт.

* Скорость в нелокальных средах: также, как и распараллеливание, облачное выполнение является обширной темой, которая выходит за рамки этой статьи.

* Использование ресурсов: необходимые объемы памяти и вычислительной мощности помогут определить, где и как вы можете запускать свои сценарии.

Результаты

Ниже вы видите совокупные результаты тестирования производительности. Полные наборы данных вы можете найти в репозитории GitHub.

Запуск на демосайте

Первый бенчмарк запускался на нашем демонстрационном сайте. Эта веб-страница, размещенная на Heroku, создана на основес использованием Vue.js и использует бэкенд на Express. В большинстве случаев данные с бэкенда фактически не извлекаются. Вместо этого фронтенд хранит данные на стороне клиента.

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

Общие результаты выглядят следующим образом:

Результаты бенчмарка для сценария быстрого входа на нашем демосайтеРезультаты бенчмарка для сценария быстрого входа на нашем демосайте

Первое, что обращает на себя внимание это большая разница между средним временем выполнения для Playwright и Puppeteer, причем последний почти на 30% быстрее и демонстрирует меньший разброс в его производительности. Мы задумались, не связано ли это с более длительным запуском со стороны Playwright. Мы не стали рассматривать этот и аналогичный вопросы, во избежание увеличения объема работ для первого бенчмарка.

Playwright vs PuppeteerPlaywright vs Puppeteer

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

WebDriverIO with WebDriver vs WebDriverIO with DevToolsWebDriverIO with WebDriver vs WebDriverIO with DevTools

Возможно, менее удивительным является то, что запуск Puppeteer без добавления какого-либо высокоуровневого фреймворка помогает нам значительно сократить время выполнения этого очень короткого скрипта.

Puppeteer vs WebDriverIO with DevToolsPuppeteer vs WebDriverIO with DevTools

Запуск на реальном веб-приложении

Предыдущий опыт научил нас, что разницу между демонстрационной средой и реальным миром почти всегда недооценивают. Поэтому мы очень хотели, чтобы тесты производительности выполнялись на рабочем приложении. В этом случае мы выбрали Checkly для запуска интерфейса Vue.js и бэкэнда, который в значительной степени использует AWS.

Запущенный нами скрипт очень похож на классический тест E2E: вход в Checkly, настроенная проверка API, сохранение и тут же удаление. Мы с нетерпением ждали этого сценария, но у каждого из нас были разные ожидания относительно того, как будут выглядеть цифры.

Результаты бенчмарка для нашего проверочного сценария ChecklyРезультаты бенчмарка для нашего проверочного сценария Checkly

В этом случае разница во времени выполнения между Playwright и Puppeteer почти исчезла. Playwright теперь выходит на первое место и демонстрирует немного меньший разброс.

Playwright vs PuppeteerPlaywright vs Puppeteer

Разница между новыми инструментами и обеими разновидностями WebDriverIO тоже соответственно меньше. Стоит отметить, что последние два теперь дают больший разброс в результатах по сравнению с предыдущим сценарием, в то время как Puppeteer и Playwright показывают меньшие отклонения.

Playwright vs WebDriverIO with SeleniumPlaywright vs WebDriverIO with Selenium

Интересно, что наш первоначальный тест для этого сценария включал внедрение файлов cookie в совершенно новый сеанс, чтобы иметь возможность полностью пропустить процедуру входа в систему. Позднее от этого подхода отказались, поскольку мы столкнулись с проблемами на стороне Selenium, когда сеанс переставал отвечать после загрузки определенного количества файлов cookie. WebDriverIO хорошо справился с этим, но этап внедрения файлов cookie резко увеличил разброс времени выполнения. Иногда казалось, что на этом этапе происходит зависание более 5 секунд.

Теперь мы отступим назад и сравним время выполнения в разных сценариях:

Среднее время выполнения тестовых сценариевСреднее время выполнения тестовых сценариев

Сомневаетесь в результатах? Запустите свой собственный бенчмарк! Вы можете использовать наши сценарии тестирования, представленные выше. Не уверены в настройке? Не стесняйтесь сообщать об этом, чтобы сделать сравнение лучше.

Заключение

Прежде всего, давайте рассмотрим инструменты от самых быстрых к самым медленным для обоих сценариев тестирования:

Рейтинг производительностиРейтинг производительности

Наше исследование производительности позволило сделать несколько интересных выводов:

  • Хотя Puppeteer и Playwright используют сходные API, похоже, что Puppeteer имеет значительное преимущество в скорости на более коротких скриптах (по нашим наблюдениям, выигрыш составляет около 30%).

  • Скрипты Puppeteer и Playwright показывают более быстрое время выполнения (около 20% в сценариях E2E) по сравнению с вариантами Selenium и DevTools WebDriverIO.

  • Протоколы автоматизации WebDriverIO, WebDriver и DevTools показали сопоставимое время выполнения.

Выводы

  • При запуске большого количества более быстрых сценариев, если нет необходимости в кросс-браузерности, возможно, стоит запустить Puppeteer, чтобы сэкономить время. В более длинных сценариях E2E разница сводится к минимуму.

  • Имеет смысл подумать, необходим ли вам запуск установки большего количества barebone-систем. Возможно, что удобство дополнительных инструментов WebDriverIO стоит того, чтобы потратить немного больше времени на ожидание результатов.

  • Колебания времени выполнения могут не иметь большого значения в бенчмарках, но в реальном мире они могут накапливаться и замедлять сборку. Помните об этом при выборе средства автоматизации.

  • Глядя на прогресс с обеих сторон, мы задаемся вопросом, выйдет ли в будущем DevTools на передний план или WebDriver будет продолжать играть центральную роль в автоматизации браузеров. Мы предлагаем обратить внимание на обе технологии.

К переводу этой статьи нас подтолкнуло то, что мы, как и автор, при автоматизации тестирования наших проектов чаще всего использовали возможности WebDriver. Оценивая результаты данного исследования, мы планируем опробовать новые инструменты, такие как Puppeteer и Playwright.

Спасибо за внимание! Надеемся, что перевод был вам полезен.

Подробнее..

Перевод Автоматизированное тестирование баз данных в Java с помощью JdbcTemplate

15.03.2021 14:07:31 | Автор: admin

В преддверии старта курса "Java QA Automation Engineer" подготовили перевод полезного материала.

Также приглашаем поучаствовать в открытом вебинаре на тему HTTP. Postman, Newman, Fiddler (Charles), curl, SOAP. SoapUI. На этом занятии участники вместе с экспертом разберут, какие бываю API и каким способом можно проверить, что backend отдает ожидаемые данные, а также познакомятся с основными инструментами для тестирования.


Бывает, и достаточно часто, что во время автоматизированного тестирования наши тесты должны взаимодействовать с базами данных. Иногда нам нужно установить какие-либо тестовые данные. В других случаях нам нужно совершать запросы в базу данных, чтобы получить те самые тестовые данные. И давайте не будем забывать об очистке данных, которые мы использовали и которые больше нам не нужны. В этой статье я покажу, как вы можете использовать класс Spring JdbcTemplate для упрощения работы с базой данных MySQL из ваших автоматизированных тестов на Java.

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

Требования

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

<dependency>        <groupId>mysql</groupId>        <artifactId>mysql-connector-java</artifactId>        <version>8.0.23</version></dependency>

Первая зависимость, которую мы здесь видим, это зависимость из пакета Spring. Здесь мы можем найти класс JdbcTemplate, который мы будем использовать для коммуникации с базой данных. Этот класс содержит полезные методы для обновления или получения данных из базы данных. Вторая зависимость требуется для связи с инстансом MySQL.

Примечание: эти зависимости имеют последнюю доступную на момент написания статьи версию (какую вы можете увидеть в репозитории Maven). Версия mysql-connector-java должна быть синхронизирована с версией инстанса MySQL, на котором работает ваша база данных. В моем случае, мой сервер MySQL имеет версию > 8, поэтому версия моего mysql-connector-java также выше чем 8.

Подключение к базе данных

После того как мы разобрались с зависимостями, мы можем установить связь с нашей базой данных. Мы могли бы написать код, необходимый для этой операции, в нашем тесте. Однако нам обязательно понадобится этот код и в других тестовых классах. Следовательно, этот код может быть написан либо в специальном классе для работы с базой данных, либо в базовом классе, расширяемом вашими тестами. Независимо от того, какой вариант вы выберете, соединение может быть установлено с помощью подобного метода:

public DataSource mysqlDataSource() {    DriverManagerDataSource dataSource = new DriverManagerDataSource();    dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");    dataSource.setUrl("jdbc:mysql://dbURL:portNumber/nameOfDB?useSSL=false");    dataSource.setUsername("username");    dataSource.setPassword("password");    return dataSource;}

Первое, на что нам нужно обратить внимание, это то, что этот метод возвращает DataSource. Это необходимо для инициализации класса JdbcTemplate, который мы будем использовать в наших тестах, поскольку он хранит соединение с базой данных.

Затем в качестве имени класса драйвера в этом примере я использовал значение com.mysql.cj.jdbc.Driver. Опять же это требуется для установки соединения, и в некоторых случаях в более старых версиях зависимостей коннектора MySQL вместо него следует использовать com.mysql.jdbc.Driver. Если вы используете неправильное имя, вы получите соответствующее предупреждение при попытке подключения к базе данных.

Вам нужно будет указать расположение базы данных в методе setUrl. Он состоит из URL-адреса, порта и имени базы данных. И, конечно же, вам необходимо указать имя пользователя и пароль для подключения к базе данных, с помощью методов setUsername и setPassword.

Теперь, когда соединение установлено, нам нужно инициализировать класс JdbcTemplate. Мы можем объявить переменную этого типа в нашем тестовом классе:

private JdbcTemplate jdbcTemplate;

Затем в методе @BeforeAll мы можем инициализировать эту переменную, предоставив соединение, которое мы установили с базой данных:

jdbcTemplate = new JdbcTemplate(nameOfClass.mysqlDataSource());

На этом настройка завершена, соединение установлено, и мы можем начать обновление (updating) или запрашивание (querying) базы данных.

Update

В классе JdbcTemplate мы можем найти много полезных методов. Один из них, update, может быть использован для создания и обновления таблиц, добавления в них данных или даже удаления данных. Существует несколько вариантов этого метода (с разными сигнатурами), но тот, который я приведу здесь в качестве примера, принимает один параметр: SQL-запрос в виде String.

Пример

Создадим две новые таблицы: одну с именем meal (блюдо) и ingredient (ингредиент). В таблице meal мы хотим хранить название блюда, присвоенную ему категорию (представляющую, будь то завтрак, обед или ужин) и автоматически сгенерированный id в качестве первичного ключа (primary key). Для создания таблицы напишем в тестовом методе следующий код:

jdbcTemplate.update("create table meal(\n" +                 " meal_id bigint auto_increment primary key,\n" +                 " name varchar(50) not null unique,\n" +                 " category varchar(50) not null\n" + ");");

Когда мы запустим тест, таблица будет создана. Для создания таблицы больше ничего не требуется. Допустим, мы также хотим добавить к этому столу два блюда: фахитас из курицы и энчилада. Сделать это легко просто передадим требуемый SQL-запрос в метод update следующим образом:

jdbcTemplate.update("insert into meal (name, category) values ('Chicken Fajita', 'lunch');");jdbcTemplate.update("insert into meal (name, category) values ('Enchilada', 'lunch');");

Как видите, у нас по одному вызову метода update на одну SQL-операцию.

Теперь давайте создадим таблицу под названием ingredient. У нее не будет автоматически сгенерированного первичного ключа. Однако у нее будет внешний ключ (foreign key), соответствующий значению meal_id из таблицы meal. Каждая запись в этой таблице представляет собой ингредиент, соответствующий блюду из таблицы meal. Этот внешний ключ свяжет ингредиент с блюдом. Кроме того, в таблице ingredient есть столбцы для хранения названия ингредиента (name), количества (quantity) и единицы измерения (uom - unit of measure) для количества ингредиента.

Для того чтобы создать эту таблицу, а затем добавить к ней внешний ключ, мы снова будем использовать метод update, которому мы передадим соответствующий SQL-запрос:

jdbcTemplate.update("create table ingredient(\n" +         " meal_id bigint not null,\n" +         " name varchar(50) not null,\n" +         " quantity bigint not null,\n" +         " uom varchar(50) not null\n" + ");");jdbcTemplate.update("alter table ingredient add foreign key (meal_id)" +         " references meal(meal_id);\n");

Чтобы иметь больше данных для наших следующих примеров, я также добавлю некоторые данные в таблицу ingredient:

jdbcTemplate.update("insert into ingredient (meal_id, name, quantity,"                + " uom) values ((select meal_id from meal where name = 'Chicken Fajita'), 'chicken', 1, 'kg');\n");        jdbcTemplate.update("insert into ingredient (meal_id, name, quantity, uom) " +                "values ((select meal_id from meal where name = 'Chicken Fajita'), 'red pepper', 1, 'piece');\n");        jdbcTemplate.update("insert into ingredient (meal_id, name, quantity, uom) " +                "values ((select meal_id from meal where name = 'Chicken Fajita'), 'green pepper', 1, 'piece');\n");        jdbcTemplate.update("insert into ingredient (meal_id, name, quantity, uom) " +                "values ((select meal_id from meal where name = 'Chicken Fajita'), 'yellow pepper', 1, 'piece');");        jdbcTemplate.update("insert into ingredient (meal_id, name, quantity," + " uom) " +                "values ((select meal_id from meal where name = " + "'Enchilada'), 'chicken', 1, 'kg');\n");        jdbcTemplate.update("insert into ingredient (meal_id, name, quantity," + " uom) " +                "values ((select meal_id from meal where name = " + "'Enchilada'), 'cheese', 100, 'grams');\n");        jdbcTemplate.update("insert into ingredient (meal_id, name, quantity," + " uom) " +                "values ((select meal_id from meal where name = " + "'Enchilada'), 'tomato', 1, 'piece');\n");

Отлично, у нас есть 2 таблицы с данными, которые мы можем запрашивать. Теперь же мы будем использовать разные методы из класса JdbcTemplate для получения результатов разных типов.

queryForObject получить одно значение

Если нам нужно запросить из базы данных одно значение, мы можем использовать метод queryForObject. У этого метода также есть несколько вариантов использования, но здесь мы рассмотрим наиболее простой:

jdbcTemplate.queryForObject(String sqlStatement, Class returnType);

При вызове этого метода нам нужно указать, какой тип возвращаемого значения должен иметь запрос (Class). Мы могли бы, например, получить значение String (указав String.class) или целое число (указав Integer.class).

Пример

Нам нужно запросить базу данных, чтобы получить значение meal_id из таблицы meal для блюда Chicken Fajita. Нам нужно сохранить этот результат в переменной с типом int:

int id = jdbcTemplate.queryForObject("select meal_id from meal where name='Chicken Fajita';", Integer.class);

Здесь вы можете видеть, что тип возвращаемого значения запроса указан как Integer.class, поэтому результат сохраняется в переменной с типом int. Допустим, в тесте мы также хотим вывести в консоль результат этого запроса:

System.out.println("Meal id for Chicken Fajita = " + id);

Результатом этого вывода будет:

Meal id for Chicken Fajita = 1

queryForMap получить строку

Теперь предположим, что вы хотите получить целую строку из таблицы. Или части строки. Вы можете сделать это с помощью метода queryForMap, которому вы передаете необходимый SQL-запрос:

jdbcTemplate.queryForMap(String sqlStatement);

Результат этого запроса можно сохранить в переменной типа Map. Ключи map будут соответствовать имени каждого столбца, которому принадлежит элемент строки. Значение будет соответствовать фактическому значению из строки, соответствующей этому столбцу.

Пример

Мы хотим извлечь все данные о блюде с id 1 из таблицы meal, сохранить их в переменной и вывести результат в консоль. Это легко можно сделать следующим образом:

Map<String, Object> entireRowAsMap = jdbcTemplate.queryForMap("select * from meal where meal_id = 1");System.out.println("All details of meal with id 1 = " + entireRowAsMap);

Как видите, переменная entireRowAsMap представляет Map, ключи которой String, а значения Object. Это происходит потому, что некоторые значения являются целыми числами, некоторые строками и, конечно же, все эти типы являются объектами в Java. Вывод в консоль для приведенного выше кода:

All details of meal with id 1 = {meal_id=1, name=Chicken Fajita, category=lunch}

queryForList получить столбец

Когда вам нужно получить либо все значения, либо часть значений из конкретного столбца, вы можете использовать метод queryForList. В этом варианте использования я покажу на примере, что для результирующих элементов требуется SQL-запрос и тип возвращаемого значения. Речь идет о типе элементов, которые вы будете сохранять в список (List) Java. Например, если все элементы, которые вы извлекаете с помощью этого запроса, являются целыми числами, типом возврата будет Integer.class. Основной пример использования метода выглядит так:

jdbcTemplate.queryForList(String sqlStatement, Class returnType);

Пример

Мы хотим сохранить в список Java все названия ингредиентов, которые есть в таблице ingredient. Мы также хотим вывести эти значения в консоль. Этого можно добиться следующим образом:

List<String> queryForColumn = jdbcTemplate.queryForList("select " +         "distinct name from ingredient", String.class);System.out.println("All available ingredients = " + queryForColumn);

Поскольку все названия ингредиентов имеют тип String, тип возвращаемого значения для метода queryForList String.class. Вот что будет выведено на консоль:

All available ingredients = [chicken, red pepper, green pepper, yellow pepper, cheese, tomato]

queryForList получение списка строк

Другой вариант использования метода queryForList получение сразу нескольких строк. В этом случае единственный параметр, требуемый при вызове этого метода, это SQL-запрос, который собирает данные. Типом возврата будет список элементов типа map, где каждая map будет иметь ключ с типом String и соответствующее значение с типом Object. Этот метод выглядит так:

jdbcTemplate.queryForList(String sqlStatement);

Пример

Выберите все значения из таблицы meal, сохраните и выведите их в консоль.

List<Map<String, Object>> severalRowsAsListOfMaps = jdbcTemplate.queryForList("select * from meal;"); System.out.println("All available meals = " + severalRowsAsListOfMaps);

Вывод здесь представляет собой список элементов типа map:

All available meals = [{meal_id=1, name=Chicken Fajita, category=lunch}, {meal_id=2, name=Enchilada, category=lunch}]

Передача параметров запросам

В некоторых случаях SQL-запросы нуждаются в передаче параметра для замены захардкоженного значения из запроса. Например, вы можете захотеть выполнить тот же запрос для поиска строки в базе данных на основе ее id. Но вам может потребоваться передать id в тест через DataProvider. Следовательно, при каждом запуске метода для выполнения запроса у вас будет другое значение id.

В этом случае для любого запроса, который вы хотите выполнить, вместо указания явного значения вы проставляете символ ?. Это нужно сделать внутри SQL-запроса.

Пример

В тестовом методе нам нужно выяснить, сколько строк существует в таблице ingredient с именем, которое предоставляется в виде параметра. Результат этого запроса будет сохранен в переменной типа int и будет выведен в консоль. Этого можно добиться следующим образом:

Integer howManyUsages = jdbcTemplate.queryForObject("select count(*) "                 + "from ingredient where name=?", Integer.class, ingredientToLookFor);        System.out.println("How many time does the ingredient passed as "                 + "parameter appear in the DB " + " = " + howManyUsages);

Второй параметр, переданный методу queryForObject, это тип возвращаемого значения для запроса, а третий параметр это имя параметра, который будет отправлен в запрос из DataProvider. Например, если значение параметра ingredientToLookFor будет chicken, вывод в консоль будет следующим:

How many time does the ingredient passed as parameter appear in the DB = 2

Извлечение данных в объект Java

Помните мою статью об использовании объектов Java для моделирования данных, извлеченных из БД? Вы можете легко использовать JdbcTemplate для запроса базы данных и извлечения результата непосредственно в объект (Object). Все, что вам нужно для выполнения этой задачи, это объект Java для моделирования данных; класс преобразователя строк (row mapper), который сопоставляет столбец из базы данных со свойствами объекта; запрос, который извлекает данные в объект с помощью преобразователя строк.

Пример

Допустим, нам нужно смоделировать данные, соответствующие ингредиенту, название которого содержит текст yellow, в объект ингредиента (Ingredient Object). Это означает, что мы хотим, чтобы объект имел те же свойства, что и ингредиент в таблице. Мы хотим сопоставить каждый столбец со свойством. Поэтому мы создадим объект Java под названием Ingredient. Его свойства будут следующими:

public int meal_id; public String name; public int quantity; public String uom;

Рекомендуется синхронизировать имена свойств с именами столбцов базы данных. Таким образом, вы можете легко идентифицировать каждое свойство. Поскольку это объект, нам потребуется создать методы equals, hashCode и toString. Пока я пропущу эту часть.

Вместо этого я покажу кое-что еще, что вам понадобиться, а именно сеттеры для каждого свойства. Вы можете легко автоматически сгенерировать их в IntelliJ, используя в редакторе сочетание клавиш Alt+Insert. Они будут выглядеть следующим образом:

public void setMeal_id ( int meal_id){        this.meal_id = meal_id;}        public void setName (String name){          this.name = name;}        public void setQuantity ( int quantity){            this.quantity = quantity;        }        public void setUom (String uom){            this.uom = uom;        }

Вы будете использовать их для отображения данных БД в свойства объекта. И это произойдет внутри класса преобразователя строк, который мы создадим следующим. Тело этого класса выглядит следующим образом:

public class IngredientRowMapper implements RowMapper<Ingredient> {@Override        public Ingredient mapRow(ResultSet rs, int rowNum) throws SQLException {          Ingredient ingredient = new Ingredient();          ingredient.setMeal_id(rs.getInt("meal_id"));          ingredient.setName(rs.getString("name"));          ingredient.setQuantity(rs.getInt("quantity"));          ingredient.setUom(rs.getString("uom"));          return ingredient;        }}

Как видите, этот класс должен реализовать интерфейс под названием RowMapper. Из-за этого нам потребуется реализовать метод mapRow. И внутри этого метода вы будете сопоставлять каждое свойство объекта со столбцом базы данных, используя сеттеры. Так, например, для свойства quantity метод setQuantity установит значение, извлеченное из строки, имя соответствующего столбца которой тоже quantity.

Создав класс IntegerRowMapper, мы можем выполнить поставленную задачу, используя queryForObject для извлечения данных, соответствующих желтому (yellow) ингредиенту:

Ingredient ingredient = jdbcTemplate.queryForObject("select * from "         + "ingredient where name like '%yellow%'", new IngredientRowMapper());System.out.println("The ingredient object = " + ingredient);

Результат этого запроса будет отображаться как объект с соответствующими свойствами:

The ingredient object = Ingredient{meal_id=1, name='yellow pepper', quantity=1, uom='piece'}

Заключение

Мы рассмотрели несколько способов работы с базами данных из наших автоматизированных тестов: нужно ли нам обновлять их или просто извлекать данные в классе JdbcTemplate есть множество методов, которые могут нам помочь с этим. Вы можете выбрать, какой из них использовать, в зависимости от того, что должен возвращать ваш SQL-запрос.


Узнать подробнее о курсе "Java QA Automation Engineer".

Смотреть открытый вебинар на тему HTTP. Postman, Newman, Fiddler (Charles), curl, SOAP. SoapUI.

Подробнее..

Перевод Паттерны и Методологии Автоматизации UI Примеры из жизни

01.04.2021 16:06:37 | Автор: admin

Полезные паттерны для автоматизации тестирования UI

Паттерн проектирования - это не законченный фрагмент кода, который можно использовать в проектах. Напротив, это больше похоже на план решения.

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

Паттерн Декоратор (Decorator)

Мой фреймворк должен поддерживать различные варианты компонентов веб-сайта. Это необходимо, потому что наше веб-приложение постоянно меняется, и A/B-тесты выполняются на уровне компонентов.

Если у вас есть подобное требование, паттерн Декоратор может вам подойти! Он позволяет упаковать компоненты в конверты, которые перезаписывают или дополняют только определенные функции. Вам не нужно писать новый класс для каждой новой характеристики компонента: должны быть реализованы только изменения. Вы также можете использовать этот метод, если веб-компоненты меняются в зависимости от размера браузера или типа устройства.

Пример Декоратора

В этом примере у нас есть два компонента Login. У второго есть дополнительная кнопка отменить.

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

Давайте посмотрим на это с точки зрения декоратора!

LoginComponent - это интерфейс для каждого компонента Login. В нем говорится, что каждый компонент должен иметь определенный метод login.

package decorator;public interface LoginComponent {   void login(String user, String password);}

BasicLoginComponent имеет конкретную реализацию метода login. В этом примере он просто выводит Basic login в командную строку.

package decorator;public class BasicLoginComponent implements LoginComponent {   @Override   public void login(String user, String password) {       System.out.println("Basic login: " + user + ", " + password);   }}

Этот класс является сердцем паттерна. LoginDecorator может брать любой LoginComponent и оборачивать его нужными функциями. После этого результатом остается LoginComponent.

package decorator;public abstract class LoginDecorator implements LoginComponent {   private final LoginComponent loginComponent;   public LoginDecorator(LoginComponent loginComponent) {       this.loginComponent = loginComponent;   }   @Override   public void login(String user, String password) {       loginComponent.login(user, password);   }}

MobileLoginDecorator переопределяет функциональность login новым классом, специфичным для мобильных устройств. Опять же, он просто выводит Mobile login, чтобы этот пример был коротким.

package decorator;public class MobileLoginDecorator extends LoginDecorator {   public MobileLoginDecorator(LoginComponent loginComponent) {       super(loginComponent);   }   @Override   public void login(String user, String password) {       System.out.println("Mobile login: " + user + ", " + password);   }}

CancelButtonDecorator может добавить функцию cancel в любой компонент Login.

package decorator;public class CancelButtonDecorator extends LoginDecorator {   public CancelButtonDecorator(LoginComponent loginComponent) {       super(loginComponent);   }   public void cancel() {       System.out.println("Click the cancel button");   }}

Теперь мы можем проверить, как все это работает!

package decorator;public class Main {   public static void main(String[] args) {   System.out.println("DECORATOR PATTERN");   System.out.println("=================");   // This is the basic login component   LoginComponent loginComponent = new BasicLoginComponent();   loginComponent.login("User", "PW");   // Let's turn it into a mobile login component.   loginComponent = new MobileLoginDecorator(loginComponent);   loginComponent.login("User", "PW");   // Finally, we can add a cancel functionality.   loginComponent = new CancelButtonDecorator(loginComponent);   ((CancelButtonDecorator) loginComponent).cancel();   }}

Результат всего этого:

DECORATOR PATTERN=================Basic login: User, PWMobile login: User, PWClick the cancel button

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

Page Object и Page Component

Одним из первых паттернов, характерных для автоматизации UI, является паттерн Page Object. Это означает, что все функции конкретной страницы заключены в класс. Это хорошо для простых представлений без большого количества возможностей взаимодействия, поскольку объекты страницы ясны и управляемы.

Однако, если страница содержит много функций, классы объектов страницы могут стать огромными и превратиться в сложный и хаотичный код. Здесь и появляется расширение объектов страницы: Page Component. Идея состоит в том, чтобы обернуть функциональность компонента в класс, а не всю страницу.

Пример Page Object

Это очень простой интернет-магазин, который включает поиск и список результатов найденных продуктов. Если вы реализуете это с помощью Page Object, результат может выглядеть примерно так, как этот класс WebshopPage.

package pageobjects;public class WebshopPage {   public void search(final String queryString) {       System.out.println("Enter " + queryString);       System.out.println("Click search button");   }   public void checkResultHeadline() {       System.out.println("Check if the headline is correct.");   }   public void checkResults() {       System.out.println("Check if there are search results.");   }}

Все действия, которые можно выполнить на этой странице, включены сюда. Мы можем проверить это с помощью простого Main класса.

package pageobjects;public class Main {   public static void main(String[] args) {   System.out.println("PAGE OBJECTS");   System.out.println("============");   WebshopPage webshopPage = new WebshopPage();   webshopPage.search("T-Shirt");   webshopPage.checkResultHeadline();   webshopPage.checkResults();   }}

Как и ожидалось, это дает нам следующий результат:

PAGE OBJECTS============Enter T-ShirtClick search buttonCheck if the headline is correct.Check if there are search results.

На данный момент такой подход хорош. Но если на эту страницу добавить больше функций, тестовый код может быстро запутаться.

Пример Page Component

Здесь на помощь приходит Page Component. В нашем случае вы можете разделить страницу на два компонента: панель поиска и список результатов.

Класс SearchBar должен содержать только метод поиска.

package pagecomponents;public class SearchBar {   public void search(final String queryString) {       System.out.println("Enter " + queryString);       System.out.println("Click search button");   }}

Методы проверки заголовка результата и самих результатов относятся к ResultList:

package pagecomponents;public class ResultList {   public void checkResultHeadline() {       System.out.println("Check if the headline is correct.");   }   public void checkResults() {       System.out.println("Check if there are search results.");   }}

Есть еще WebshopPage, но в этой версии просто доступны два компонента.

package pagecomponents;public class WebshopPage {   public SearchBar searchBar() {       return new SearchBar();   }   public ResultList resultList() {       return new ResultList();   }}

Нам нужно внести лишь небольшие изменения в пример. Вместо доступа к функциям через страницу, теперь мы можем делать это через компоненты.

package pagecomponents;public class Main {   public static void main(String[] args) {   System.out.println("PAGE COMPONENTS");   System.out.println("===============");   WebshopPage webshopPage = new WebshopPage();   webshopPage.searchBar().search("T-Shirt");   webshopPage.resultList().checkResultHeadline();   webshopPage.resultList().checkResults();   }}

Результат все тот же:

PAGE COMPONENTS===============Enter T-ShirtClick search buttonCheck if the headline is correct.Check if there are search results.

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

Паттерн Фабрика (Factory)

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

Однако у этого подхода есть серьезный недостаток. Поскольку каждый класс компонента должен быть известен фреймворку и каждый компонент должен совместно использовать дублированный код инициализации при его создании. С помощью фабричного класса можно реализовать один источник истины, который может создавать экземпляры каждого необходимого класса.

Пример Фабрики

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

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

package factory;public class Component {   public void initialize() {       System.out.println("Initializing " + getClass().getName());   }}

Каждый из перечисленных выше компонентов может быть унаследован от этого класса Component:

public class ResultList extends Component {    ...}public class SearchBar extends Component {    ...}

Недостающая часть - это фабричный класс, который отвечает за производство компонентов. Для иллюстрации фабричный класс принимает имя компонента в качестве аргумента, создает экземпляр соответствующего класса, вызывает его метод initialize и возвращает его обратно.

package factory;public class ComponentFactory {   public static Component getComponent(final String componentName) throws Exception {   System.out.println("Creating " + componentName + "...");   // Create a component instance for the passed in component name.   Component component;   switch (componentName){       case "SearchBar":           component = new SearchBar();           break;       case "ResultList":           component = new ResultList();           break;       default:           throw new Exception(componentName + " unknown.");   }   System.out.println("Component created: " + component);   component.initialize();   return component;   }}

Код Main класса не выглядит иначе, потому что WebshopPage по-прежнему отвечает за управление его компонентами.

package factory;public class Main {   public static void main(String[] args) throws Exception {   System.out.println("FACTORY PATTERN");   System.out.println("===============");   WebshopPage webshopPage = new WebshopPage();   webshopPage.searchBar().search("Berlin");   }}

Результат измененного примера:

FACTORY PATTERN===============Creating SearchBar...Component created: factory.SearchBar@3d075dc0Initializing factory.SearchBarEnter BerlinClick search button

Компонент запрашивается, создается и инициализируется должным образом.

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

Внедрение зависимости (Dependency Injection)

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

Как правило, программное обеспечение, использующее внедрение зависимостей, полагается на какую-то специализированную среду, такую как Spring или Guice, которая обрабатывает создание и внедрение объектов. Чтобы прояснить концепцию, в следующем примере фреймворк не используется.

Пример Внедрения зависимости

Здесь мы хотим предоставить функции входа в систему для тестирования UI простого веб-сайта. Однако мы хотим хранить имя пользователя и пароль непосредственно внутри класса, поэтому нам не нужно передавать их всякий раз, когда мы хотим вызвать логин. Кроме того, наше требование состоит в том, что у нас может быть несколько наборов имени пользователя и пароля в зависимости от тест-кейса. Это новое требование не позволяет нам просто включить данные для логина на страницу Login, поскольку она должна оставаться гибкой. Вот почему вводить данные извне - хороший выбор.

Это интерфейс LoginData, который должны реализовывать все наши экземпляры данных для входа. Он просто возвращает имя пользователя и пароль.

package dependencyinjection;public interface LoginData {   String getUserName();   String getPassword();}

Давайте рассмотрим две реализации: одну для реальных, а другую для фейковых данных для логина.

package dependencyinjection;public class LoginDataReal implements LoginData {   @Override   public String getUserName() {       return "Real user";   }   @Override   public String getPassword() {       return "Real password";   }}package dependencyinjection;public class LoginDataFake implements LoginData {   @Override   public String getUserName() {       return "Fake user";   }   @Override   public String getPassword() {       return "Fake password";   }}

Конструктор LoginPage принимает экземпляр класса LoginData и использует его в своем методе login. Таким образом, фактическое имя пользователя и пароль для использования не управляются самой страницей LoginPage, а вместо этого выбираются и вводятся извне.

package dependencyinjection;public class LoginPage {   private final LoginData loginData;   public LoginPage(final LoginData loginData) {       this.loginData = loginData;   }   public void login(){       System.out.println("Logging in with " + loginData.getClass());       System.out.println("- user: " + loginData.getUserName());       System.out.println("- password: " + loginData.getPassword());   }}

Единственная отсутствующая часть - это тестовый класс, который использует оба набора данных для входа.

package dependencyinjection;public class Main {   public static void main(String[] args) {   System.out.println("DEPENDENCY INJECTION");   System.out.println("====================");   LoginPage loginPageReal = new LoginPage(new LoginDataReal());   loginPageReal.login();   LoginPage loginPageFake = new LoginPage(new LoginDataFake());   loginPageFake.login();   }}

Этот класс создает две отдельные страницы входа в систему, которые отличаются только переданными данными входа. Запуск класса выводит следующее:

DEPENDENCY INJECTION====================Logging in with class dependencyinjection.LoginDataRealuser: Real userpassword: Real passwordLogging in with class dependencyinjection.LoginDataFakeuser: Fake userpassword: Fake password

Опять же, для этого решения требуется больше классов, но в каждом из них гораздо меньше кода, что упрощает понимание и поддержку. Это также позволяет лучше повторно использовать каждый компонент в случае, если он появляется более чем на одной странице.

Вы можете видеть, что это делает наше ПО более гибким, поскольку его компоненты легко перепрограммировать без изменения какого-либо кода в большинстве классов. Кроме того, это приводит к более тестируемому коду, поскольку каждый класс можно тестировать как отдельную единицу.

И наконец: Две Методологии

Мы только что просмотрели много кода. Итак, давайте завершим эту статью чем-то совершенно другим: методологиями!

Всякий раз, когда вам нужно написать или расширить фреймворк тестирования UI, полезно придерживаться некоторых рекомендаций, которые помогут вам реализовать правильные вещи для ваших клиентов или товарищей по команде. Лично я всегда стараюсь придерживаться приведенных ниже примеров. Они оказались очень полезными для меня, не давая мне заблудиться в этом процессе.

Не усложняй (Keep It Simple)

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

Вам это не понадобится (You Arent Gonna Need It)

Лично для меня этот принцип, также известный как YAGNI, является одним из самых важных. Он связан с сохранением простоты в том смысле, что вы должны делать самое простое, что может работать. Это означает, что новые функции следует добавлять только тогда, когда вы уверены, что они необходимы, а не потому, что вы думаете, что они понадобятся в какой-то момент.

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

Переведено командой QApedia. Еще больше переведенных статей вы найдете на нашем телеграм-канале.

Подробнее..

Управление тестами в TestOps храните информацию, а не выводы

24.05.2021 12:21:25 | Автор: admin

Обеспечить представление данных из любой большой системы так, чтобы человек мог спокойно с этими данными работать задача нетривиальная, но давно решенная. В этой гонке уже давно победило "дерево". Папочные структуры в операционных системах знакомы всем и каждому и исторически простое дерево в UI/UX становится первым решением для упорядочивания и хранения данных. Сегодня поговорим о тестировании, так что в нашем случае объектами хранения будут выступать тест-кейсы. Как их хранят чаще всего? Верно, в папочках!

Привычно? Да. Но так ли это решение хорошо и универсально? Вопрос подкрепляется существованием исключений: почта, JIRA-тикеты и много чего другого хранится не в папках. Давайте разбираться, почему.

Единое дерево

Этот подход позволяет быстро запуститься на старте и смоделировать структуру для построения базы тестов. Например, организовать хранение тест-кейсов для веб-сервиса по страницам (или по фичам) каждой странице по папочке для тест-кейсов, даже если самих тестов еще нет. Это удобно сразу можно понять масштаб и работ, а по заполненности папочек можно примерно оценить покрытие.

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

Что с ним не так?

Представим, что у нас есть замечательный веб-сервис, тесты на него написаны, разложены по папкам (по одной на каждую страницу) и выполняются раз в неделю, все счастливы. Однако, в какой-то момент к руководителю QA приходит менеджер (а они всегда приходят!) и просит рассказать о покрытии тестами микросервисов. Это не то, чтобы большая проблема просто теперь в дополнение к существующей структуре нужно как-то собрать информацию о тестах из папочек и соотнести ее с микросервисами и сделать соответствующий (новый) отчет. Впрочем, чтобы в следующий раз не заниматься рутиной, старший тестировщик предлагает ввести фиксированные уровни у дерева: так у нас появится возможность иметь данные по командам в нашей структуре данных, а команды намного быстрее будут со своими репозиториями разбираться! Отличное решение, только в следующий раз менеджер просит статистику покрытия по фичам (еще один новый отчет)... Если тестов много, сотни или тысячи, то создание каждого нового среза потребует погружения внутрь всех вложенных ветвей и перераспределения тест-кейсов по новой структуре для анализа.

В результате общее дерево со временем разбивается на поддеревья: у каждой команды свое поддерево со своими правилами. При необходимости добавления дополнительного признака в тесты никто не пойдет править чужие поддеревья: неудобно, не надо, процессы уже завязаны на текущую структуру, некогда этим заниматься, "не трогайте наши тесты!" и еще по миллиону причин. В итоге получается картина, когда дерево папок общее, но в каждом поддереве свои уровни.

Минусы дерева папок отсутствие гибкости в доступе к данным и аналитике и сложность рефакторинга в условиях, когда требования к тестированию меняются.

При росте проекта и требований к прозрачности тестирования возникает второе ограничение папочной структуры: ее очень сложно обслуживать. Продукты редко становятся проще, поэтому со временем наступает момент, когда структура хранения тест-кейсов, созданная на старте, морально устаревает.

Со временем обнаружится, что папочки в изначальной структуре расплодились и превратились в неуправляемый ад. Мудрым решением пересобрать структуру по другому критерию, по фичам. Для этого надо спроектировать новую структуру, уже вместе с аналитиком (если такой есть) разобраться с фичами, и аккуратно переносить все тесты в новую структуру. Выглядит несложно, однако такой процесс в крупных проектах занимает недели и месяцы, и если те, кто с таким пока не столкнулся счастливые люди.

В итоге папочки станут снова управляемыми, уровни вложенности структурируются, но снова получится только один срез по фичам. Если попытаться создать структуру с двумя срезами, по фичам и компонентам сразу, ее сложность и запутанность создаст больше проблем, чем пользы. А если у вас 3000 тест-кейсов? А если 10000?

Впрочем, чтобы столкнуться с ограничениями папочной структуры хранения тестов, не обязательно ждать менеджера с каким-либо "хитрым" запросом. Если в недавно переработанной структуре по фичам понадобится найти самый "красный" компонент, сделать этого скорее всего не получится: вы найдете самую красную "папочку" с фичей, но какие компоненты в ней тестируются, сходу будет не понять. С любой метрикой качества тестирования или продукта придется идти разбираться головой и руками.

Еще один подводный камень хранения тестов в папках ждет тех, кто планирует хранить в них автоматизированные тесты, написанные на разных языках программирования. Тут все просто: в Java тесты хранятся по полному имени пакет вроде io.qameta.allure, а в JavaScript или Python просто по имени пакета. Это значит, что при автоматической генерации тест-кейсов из автотестов, каждый фреймворк будет городить свои подструктуры в дереве и вся нормализация и базовая структура будет нарушена. Конечно, можно написать свою интеграцию для каждого фреймворка, но мы же стараемся упростить себе жизнь, верно?

"Из коробки" роботы не всё делают одинаково. "Из коробки" роботы не всё делают одинаково.

Самое время задаться вопросом, при чем здесь принятие решений из заголовка статьи. Ответ прост: все перечисленные недостатки связаны с тем, что папочная организация хранения тест-кейсов заставляет команду тестирования с самого начала принимать решение о структуре тестов, а не просто хранить информацию о тестовых сценариях и их результатах. Мало будет просто "подумать наперед" придется рассчитывать, что будет с проектом и командой тестирования через год-два, а лучше через три. К сожалению, чаще всего прогнозы оказываются неверными: можно переоценить темпы роста и годами сидеть в структуре "на вырост" или, наоборот, недооценить их и оказаться в структуре, неудобной команде и коллегам.

Как эту задачу решали Ops'ы?

С ограничениями классического дерева разобрались, а как их обходить? На самом деле, с этой задачей уже сталкивались многие, и изобретать велосипед нет необходимости. Большинство систем, работающих с большими объемами данных или большим количеством объектов переходят на плоскую структуру с навигацией по признакам. Самый яркий из недавних примеров инструменты мониторинга типа Grafana и Graphite.

Давайте рассмотрим его повнимательнее и попробуем применить к тестированию. Админы и все те, кто работают в Ops к метрикам и данным относятся с большой щепетильностью и любовью. Просто потому, что о падении сервиса, сети или недоступности какой-то кластера вы захотите узнать раньше пользователя, так? Изначально весь мониторинг строился по классической иерархической структуре: дата-центр кластер машинка метрики (CPU, RAM, storage и прочее). Удобная и понятная система, которая работает, если не приходится в реальном времени отслеживать десяток метрик, которые не привязаны к физическим машинам. А метрики для Ops'ов очень важны.

Например, нужно получить срез загрузки всех машин, на которых крутится какой-то микросервис. Построить такой срез при единой иерархической структуре задача нетривиальная. Задачу решили понятным и элегантным способом: сложили все объекты в единый пул и начали каждый помечать необходимыми метками-признаками.

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

Набор меток хранится в плоской структуре и свободно используется любой критерий поиска, фильтрации или группировки становится меткой

Чтобы получить отчет или анализ данных, нужно просто указать, по каким критериям срезы нас интересуют: нагрузка на CPU по инстанстам, разделенным по географии.

Такой метод управления данными позволяет не только фильтровать и делать выборки внутри структуры. Он предоставляет еще один уровень свободы в виде вышеупомянутой группировки: теперь можно спокойно сделать запрос к хранилищу вида "покажи статистику доступности всех машин, на которых крутится такой-то микросервис с группировкой по кластерам". Парсер проходится по всем машинам, забирает статистику доступности с тех, на которых стоит метка микросервиса и группирует их по кластерам.

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

В любой момент по запросу пользователю доступна любая аналитика и любой срез данных. Зайдите в дэшборды любой Ops-команды крупного сервиса, здесь найдутся отчеты по десятку метрик (uptime, загрузка ресурсов, количество пользователей и т.д.), нарезанных по множеству критериев (география, балансировка между серверами и пр.).

Да это же просто автоматизация папочек! скажете вы.

И это прекрасно это классическое решение проблемы масштабирования. Конечно, у системы с метками есть несколько минусы:

  • Нельзя создать "скелет" из папок и оставить их пустыми. Классическая практика из начала статьи, которая позволяет на старте продумать архитектуру.

  • Неудобно для тех, кто последние годы работал с папочками. Создание тестов требует аккуратной разметки и осмысленного подхода к формулированию каждого тест-кейса. Если в папочках "неудачные" или "ненужные" тесты просто затеряются, здесь они будут мозолить глаза.

Тест-кейсы: дерево против меток

Хорошо, с админами все понятно. Они там сидят и целый день смотрят в метрики и крутят данные. А нам-то зачем?

Ответ прост: мир разработки, к сожалению или к счастью, уже давно ушел от жесткой структуры релизы ускоряются, сборки автоматизируются. На фоне этих изменений, мир тестирования в большинстве компаний выглядит немного архаично: на новые фичи готовится по пачке новых тест-кейсов для ручных тестов и автоматизации, они раскладываются по папкам, запускается в тестирование на неопределенный срок (от 1-2 дней до недели), собирается результат тестирования и отгружается обратно в разработку после полного завершения. Ничего не напоминает? Это же старая добрая каскадная разработка (waterfall, то бишь)! В 2021 году. Если послушать Баруха Садогурского в одном из подкастов, где он довольно убедительно рассказывает, что любой процесс это по сути agile, в котором "мы пытаемся отложить принятие решения максимально далеко, чтобы перед этим собрать побольше данных", станет понятно, почему весь мир разработки гонится за короткими итерациями и быстрыми релизами. Именно поэтому разработчики уже давно пишут софт итеративно, внедряя по одной фиче или паре фиксов на релиз, опсы отгружают эти релизы как можно скорее, которые перед этим тестируются. А как они тестируются?

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

Посмотрите на последние веяния в тестировании для DevOps заключаются в том, чтобы тестировать как можно меньше и как можно быстрее: это приводит к подходам вроде shift right и тестируйте как можно меньше. Практики, направленные на скорость, повторяемость и стабильность выполнения тестов, а не покрытие и оценку качества в широком смысле про это в свое время буду писать отдельные статьи. Поэтому, чтобы оставаться в мире, где тестирование отвечает за качество продукта, давайте вернемся к нашим папочкам и признакам. Создавая папочную структуру, мы изначально закладываем в хранилище тест-кейсов принятое решение о том, в каком виде хотим получать отчеты и видеть результаты. Проблема в том, что мы не знаем какие отчёты понадобятся в будущем.

Если вы не успеваете готовить отчеты для коллег и бизнеса, то действительно ли тестирование приносит максимальную пользу?

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

Пример представлений, которые можно получить в Allure TestOps в пару кликов из базы размеченных тестов.Пример представлений, которые можно получить в Allure TestOps в пару кликов из базы размеченных тестов.

Тестирование так же, как и мониторинг из примера, работает с метриками. Их может быть много или очень много, и придумать универсальную и вечную структуру просто невозможно. А собрать и сформулировать исчерпывающий набор метрик и критериев можно.

По сути, вместо одного статичного дерева, управление через признаки позволяет простым и понятным способом сделать генератор деревьев, основанный на нужных критериях, расширяемый и масштабируемый без лишних танцев с бубном. А как только мы получаем автоматически генерируемые выборки и группы, мы можем настроить автоматический live-репортинг и генерацию отчетов. Самый понятный пример JIRA, где можно по любому полю тикета построить график, тренд или дэшборд любого вида, собрать, отфильтровать или сгруппировать тикеты (могут понадобиться плагины!:)) в любую структуру прямо на митинге.

Согласитесь, звучит заманчиво: иметь гибкость JIRA-тикетов в управлении тест-кейсами. Останется только автоматизировать запуски на слияние веток и вот тестирование уже отвечает требованиям DevOps!

Подробнее..

К вопросу о сертификации ISTQB

16.06.2021 14:10:53 | Автор: admin
Добрый день, уважаемый Habr. Мне кажется, что у большинства членов комьюнити сложилось довольно скептическое отношение к сертификации вообще и к ISTQB в частности, поэтому не хотелось бы сводить разговор к холивару на эту тему. А хотелось бы обсудить, некоторые моменты, которые лично меня ставят в этом вопросе в тупик, думаю, что похожие проблемы испытывают и другие русскоязычные тестировщики, перед которыми, в силу различных причин, стоит задача получения сертификата.

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

Дабы не распылять внимание уважаемого сообщества, в этой заметке, я хотел бы остановиться на терминах, так или иначе связанных с тест-кейсами, поскольку считаю это понинятие одним из самых важных среди задач, стоящих перед тестировщиком. Здесь мы попытаемся сравнить официальные русско-язычный и англо-язычный глоссарии, являющиееся альфа и омегой для испытуемых. Итак, вод ряд терминов, так или иначе связанных между собой:

Англоязычный глоссарий:

test procedure: See test procedure specification

test procedure specification: A document specifying a sequence of actions for the execution
of a test. Also known as test script or manual test script. [After IEEE 829] See also test
specification

test specification: A document that consists of a test design specification, test case
specification and/or test procedure specification

test script: Commonly used to refer to a test procedure specification, especially an automated one

test case: A set of input values, execution preconditions, expected results and execution
postconditions, developed for a particular objective or test condition, such as to exercise a
particular program path or to verify compliance with a specific requirement. [After IEEE
610]

******** Те же термины из русско-язычного глоссария ********

процедура тестирования (test procedure): См. спецификация процедуры тестирования.

спецификация процедуры тестирования (test procedure specification): Документ, описывающий последовательность действий при выполнении теста. Также известен как ручной сценарий тестирования. [IEEE 829] См. также спецификация теста

спецификация теста (test specification): Документ, состоящий из спецификации проектирования теста, спецификации тестовых сценариев и/или спецификации процедуры тестирования.

автоматизированный сценарий тестирования (test script): Обычно используется как синоним спецификации процедуры тестирования, как правило, автоматизированной.

тестовый сценарий (test case): Набор входных значений, предусловий выполнения, ожидаемых результатов и постусловий выполнения, разработанный для определенной цели или тестового условия, таких как выполнения определенного пути программы или же для проверки
соответствия определенному требованию. [IEEE 610]

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

Ну что же, идем в англоязычный глоссарий, и видим, что несмотря на упоминание об автоматизации этот терми содержит и другой посыл Commonly used to refer to a test procedure specification. Идем по указанному посылу где смысл начинает плавно меняться: A document specifying a sequence of actions for the execution of a test. Also known as test script or manual test script.

Опа, и нас переносит из области автоматизации в прямо противоположную ей область: Also known as test script or manual test script. Таким образом, выходит, что тестовый скрипт это все же нечто иное, чем скрипт, написанный для автоматизации теста. Но тогда что же это? Буду весьма обязан, если кто-нибудь из специалистов возьмет на себя труд принять участие в обсуждении этой группы, связанных между собой терминов.

По всему выходит, что центральной фигурой здесь выступает test specification на которую, в конечном итоге замыкаются все ссылки англоязычного глоссария: test specification: A document that consists of a test design specification, test case specification and/or test procedure specification. Как видите он собирает в себя вообще все, что можно, и вместо поставленной задачи разобраться в различии между терминами test script и test case, мы вдобавок, получаем кучу новых понятий никак не проясняющих, а лишь усугубляющих наше (ну мое по крайней мере) недоумение.

Очень бы хотелось прильнуть к неиссякаемому источнику коллективного разума, чтобы найти объяснение всем вышеуказанным терминам и тому, как они соотносятся друг с другом. На экзамене и в учебнике, написанном авторами всей документации по ISTQB, идет постоянное жонглирование этими терминами и многие вопросы строятся на попытке подловить испытуемых на их толковании.
Подробнее..

Перевод Как найти все битые ссылки на странице с помощью Selenium

02.06.2021 14:09:29 | Автор: admin

Когда вам нужно проверить все ссылки в вашем проекте, вы можете сделать это с помощью Postman или любого другого инструмента тестирования API, но есть более простой способ. Когда вы используете инструменты тестирования API, вам нужно написать все соединения ссылок один за другим, а когда ссылки меняются, вам нужно снова редактировать все тесты один за другим.

Теперь с помощью этого java-кода вы можете проверить все ссылки. Эти ссылки могут быть ссылками pdf, изображения, видео или фотографии.

Шаг 1: В HTML мы связываем ссылки с помощью этого кода: <a href="Adress"></a> это означает, что мы должны собрать все ссылки на веб-странице на основе <a>. Для этого мы используем этот код:

List<WebElement> allLinks = driver.findElements(By.tagName(LINKS_TAG));

LINKS_TAG - это "a". В конце страницы я добавлю весь код.

Шаг 2: Определение и проверка URL-адреса

String urlLink = link.getAttribute(LINKS_ATTRIBUTE);

LINKS_ATTRIBUTE - это "href"

Шаг 3: Отправка HTTP-запроса и считывание кодов HTTP-ответов

Мы создаем HttpConnection с параметром URL. Я добавил также Connection Timeout.

URL url = new URL(urlLink);HttpURLConnection httpURLConnect=(HttpURLConnection)url.openConnection();httpURLConnect.setConnectTimeout(5000);httpURLConnect.connect();
  • Информационные коды ответов: 100-199

  • Коды успешного ответа: 200-299

  • Редирект коды: 300-399

  • Коды ошибок клиента: 400-499

  • Коды ошибок сервера: 500-599

В принципе, мы можем сказать, что если код ответа больше или равен 400, то в этом случае соединение прервано.

import org.openqa.selenium.By;import org.openqa.selenium.WebDriver;import org.openqa.selenium.WebElement;import org.openqa.selenium.chrome.ChromeDriver;import org.openqa.selenium.chrome.ChromeOptions;import org.testng.annotations.AfterClass;import org.testng.annotations.BeforeTest;import org.testng.annotations.Test;import java.net.HttpURLConnection;import java.net.URL;import java.util.List;public class FindAllBrokenLinks {    public final String DRIVER_PATH = "Drivers/chromedriver";    public final String DRIVER_TYPE = "webdriver.chrome.driver";    public WebDriver driver;    public final String BASE_URL = "https://www.bbc.com/";    public final String LINKS_ATTRIBUTE = "href";    public final String LINKS_TAG = "a";    @BeforeTest    public void beforeTest(){        ChromeOptions options = new ChromeOptions();        options.addArguments("--disable-notifications","--ignore-certificate-errors","--disable-extensions");        System.setProperty(DRIVER_TYPE,DRIVER_PATH);        driver = new ChromeDriver(options);        driver.manage().window().maximize();        driver.get(BASE_URL);    }    @Test    public void FindAllBrokenLinks() throws Exception{        List<WebElement> allLinks = driver.findElements(By.tagName(LINKS_TAG));        for(WebElement link:allLinks){            try {                String urlLink = link.getAttribute(LINKS_ATTRIBUTE);                URL url = new URL(urlLink);                HttpURLConnection httpURLConnect=(HttpURLConnection)url.openConnection();                httpURLConnect.setConnectTimeout(5000);                httpURLConnect.connect();                if(httpURLConnect.getResponseCode()>=400)                {                    System.out.println(urlLink+" - "+httpURLConnect.getResponseMessage()+"is a broken link");                }                else{                    System.out.println(urlLink+" - "+httpURLConnect.getResponseMessage());                }            }catch (Exception e) {            }        }    }    @AfterClass    public void CloseDriver(){        driver.close();    }}

Я использовал URL веб-страницы BBC в качестве базового URL, но запуск этого кода занял 1 минуту и 49 секунд. :) Возможно, вам стоит выбрать другой сайт.

Вот некоторые результаты тестов:

https://www.bbc.com/sport OK

https://www.bbc.com/reel OK

https://www.bbc.com/worklife OK

https://www.bbc.com/travel Временно приостановил работу

https://www.bbc.com/future OK

https://www.bbc.com/culture OK

https://www.bbc.com/culture/music OK

http://www.bbc.co.uk/worldserviceradio/ Не доступен

http://www.bbc.co.uk/programmes/p00wf2qw Не доступен

https://www.bbc.com/news/world-europe-57039362 OK


Перевод подготовлен в рамках набора учащихся на курс "Java QA Automation Engineer". Если вам интересно узнать о курсе подробнее, а также познакомиться с преподавателем, приглашаем на день открытых дверей онлайн.

Подробнее..

Перевод Кросс-браузерное тестирование в Selenium

11.06.2021 02:20:41 | Автор: admin

В этой статье мы рассмотрим кросс-браузерное тестирование. Это тип тестирования, который проверяет, работает ли приложение так, как ожидается, в нескольких браузерах, операционных системах и устройствах. Мы можем проводить кросс-браузерное тестирование с помощью автоматизации и без нее. Сценарии автоматизированного тестирования могут быть написаны или созданы с помощью таких программ, как TestProject и Selenium.

К концу этой статьи вы узнаете об определении кросс-браузерного тестирования, его преимуществах и работе с ним в Selenium и TestProject.

Примечание: Код из этой статьи находится на GitHub здесь.

Что такое кросс-браузерное тестирование?

Кросс-браузерное тестирование гарантирует, что наше тестируемое приложение (AUT) совместимо с каждым браузером, операционной системой и устройством. Цель заключается в сравнении ожидаемого поведения приложения в различных случаях. Бывают случаи, когда один и тот же тестовый сценарий проходит на одном или нескольких экземплярах, но не работает на другом экземпляре.

Возможно, сбой произошел из-за нашего тестового скрипта или приложения. Вы когда-нибудь пытались открыть веб-сайт с помощью Internet Explorer, но он не работал, а затем тот же сайт без проблем открывался в Chrome? Такие проблемы выявляются во время кросс-браузерного тестирования, поскольку данные из AUT отображаются по-разному в каждом браузере.

Преимущества кросс-браузерного тестирования

Мы проводим кросс-браузерное тестирование, устанавливая базовую линию. Базовая линия - это стандартный сценарий тестирования. Его цель - изучить, как работает наш AUT, используя один браузер, одну операционную систему и одно устройство. После этого мы можем использовать базовую линию в других комбинациях браузеров, операционных систем и устройств.

Я сосредоточусь на двух преимуществах кросс-браузерного тестирования:

  1. Время

  2. Тестовое покрытие

Время

Создание и выполнение индивидуального сценария тестирования (Test Script) для уникальных сценариев занимает много времени. Поэтому наши тестовые сценарии создаются с тестовыми данными для использования их комбинаций. Один и тот же сценарий тестирования может выполняться на Chrome и Windows для первой итерации, затем на Firefox и Mac для второй итерации, а затем на других сценариях для последующих итераций.

Это экономит время, поскольку мы создаем только один тестовый сценарий, а не несколько. Ниже приведены 2 фрагмента кода для загрузки и получения заголовка для страницы TestProject. Один пример - это кросс-браузерное тестирование, а другой пример содержит отдельные тестовые сценарии для трех браузеров (Chrome, Firefox и Edge).

package tutorials.testproject;import io.github.bonigarcia.wdm.WebDriverManager;import org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;import org.openqa.selenium.edge.EdgeDriver;import org.openqa.selenium.firefox.FirefoxDriver;import org.testng.annotations.Parameters;import org.testng.annotations.Test;public class CrossBrowserTesting {  WebDriver driver;  @Test  @Parameters ( {"BrowserType"} )  public void testExamplePageOnMultipleBrowsers (String browserType) {    if (browserType.equalsIgnoreCase("Chrome")) {      WebDriverManager.chromedriver().setup();      driver = new ChromeDriver();    }    else if (browserType.equalsIgnoreCase("Edge")) {      WebDriverManager.edgedriver().setup();      driver = new EdgeDriver();    }    else if (browserType.equalsIgnoreCase("Firefox")) {      WebDriverManager.firefoxdriver().setup();      driver = new FirefoxDriver();    }    driver.manage().window().maximize();    driver.get("https://example.testproject.io/web/index.html");    System.out.println(browserType + ": " + driver.getTitle());  }}
package tutorials.testproject;import io.github.bonigarcia.wdm.WebDriverManager;import org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;import org.openqa.selenium.edge.EdgeDriver;import org.openqa.selenium.firefox.FirefoxDriver;import org.testng.annotations.Test;public class IndividualBrowserTesting {  WebDriver driver;  @Test  public void testExamplePageOnMultipleBrowsersOnChrome () {    WebDriverManager.chromedriver().setup();    driver = new ChromeDriver();    driver.manage().window().maximize();    driver.get("https://example.testproject.io/web/index.html");    System.out.println("Chrome: " + driver.getTitle());  }  @Test  public void testExamplePageOnMultipleBrowsersOnFirefox () {    WebDriverManager.firefoxdriver().setup();    driver = new FirefoxDriver();    driver.manage().window().maximize();    driver.get("https://example.testproject.io/web/index.html");    System.out.println("Chrome: " + driver.getTitle());  }  @Test  public void testExamplePageOnMultipleBrowsersOnEdge () {    WebDriverManager.edgedriver().setup();    driver = new EdgeDriver();    driver.manage().window().maximize();    driver.get("https://example.testproject.io/web/index.html");    System.out.println("Chrome: " + driver.getTitle());  }}

Тестовое покрытие

Тестовое покрытие - это техника, которая определяет, что и в каком объеме покрывается в наших тестовых сценариях.

Мы определяем особенности и убеждаемся, что для этих особенностей есть адекватные тестовые сценарии. Тестовое покрытие - это преимущество, поскольку оно позволяет нам измерить качество наших тестовых циклов.

  • Что будет включено в наши сценарии тестирования, зависит от требований.

  • То, сколько охвачено в наших сценариях тестирования, зависит от браузеров и их различных версий.

Тестовое покрытие является эффективным мерилом для процесса тестирования. Однако 100% покрытие трудно обеспечить, и, скорее всего, функция ведет себя не так, как это обычно происходит в конкретной версии.

Как осуществить кросс-браузерное тестирование в Selenium?

Мы осуществляем кросс-браузерное тестирование в Selenium, используя его сетку (grid) или тестовые данные. Selenium Grid упрощает процесс, а тестовые данные используются в качестве исходных. С помощью Selenium Grid наши тестовые сценарии выполняются параллельно на нескольких удаленных устройствах. Команды отправляются клиентом удаленным экземплярам браузера.

Тестовые данные могут храниться в файле Excel, CSV, файле свойств, XML или базе данных. Мы также можем объединить TestNG с тестовыми данными для проведения тестирования на основе данных или кросс-браузерного тестирования. Для тестирования на основе данных аннотация DataProvider и атрибут dataProvider или атрибут dataProviderClass позволяют нашему тестовому сценарию получать неограниченное количество значений.

Когда речь идет о кросс-браузерном тестировании, мы можем использовать тег параметра и аннотацию Parameters для передачи различных имен браузеров. Ниже приведены фрагменты кода, отображающие XML-файл с тегом параметра и тестовый сценарий с аннотацией Parameters.

<suite name="Cross Browser Testing">    <test name = "Test On Chrome">        <parameter name = "BrowserType" value="Chrome"/>            <classes>                <class name = "tutorials.testproject.CrossBrowserTesting"/>            </classes>    </test>    <test name = "Test On Edge">        <parameter name = "BrowserType" value="Edge"/>        <classes>            <class name = "tutorials.testproject.CrossBrowserTesting"/>        </classes>    </test>    <test name = "Test On Firefox">        <parameter name = "BrowserType" value="Firefox"/>        <classes>            <class name = "tutorials.testproject.CrossBrowserTesting"/>        </classes>    </test></suite>
package tutorials.testproject;import io.github.bonigarcia.wdm.WebDriverManager;import org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;import org.openqa.selenium.edge.EdgeDriver;import org.openqa.selenium.firefox.FirefoxDriver;import org.testng.annotations.Parameters;import org.testng.annotations.Test;public class CrossBrowserTesting {  WebDriver driver;  @Test  @Parameters ( {"BrowserType"} )  public void testExamplePageOnMultipleBrowsers (String browserType) {    if (browserType.equalsIgnoreCase("Chrome")) {      WebDriverManager.chromedriver().setup();      driver = new ChromeDriver();    }    else if (browserType.equalsIgnoreCase("Edge")) {      WebDriverManager.edgedriver().setup();      driver = new EdgeDriver();    }    else if (browserType.equalsIgnoreCase("Firefox")) {      WebDriverManager.firefoxdriver().setup();      driver = new FirefoxDriver();    }    driver.manage().window().maximize();    driver.get("https://example.testproject.io/web/index.html");    System.out.println(browserType + ": " + driver.getTitle());  }}

В XML-файле тег параметра расположен на уровне теста. У нас есть возможность разместить тег на уровне тестового набора, на уровне теста или на обоих уровнях. Обратите внимание, что тег параметра имеет имя и значение с данными между двойными кавычками. Его имя, т.е. "BrowserType", передается тестовому сценарию через аннотацию @Parameters, а значение, т.е. "Chrome", передается в операторы if и else if.

Операторы if и else if устанавливают Chrome, Edge или Firefox. Каждый браузер получал команды от одного и того же тестового сценария после выполнения из XML-файла. Следующие результаты тестирования показывают, как успешно загружается страница TestProject, а консоль печатает уникальное имя браузера и заголовок страницы.

Кросс-браузерное тестирование в Selenium с помощью TestProject

OpenSDK / Закодированный тест

Существует 2 способа проведения кросс-браузерного тестирования с помощью TestProject. Мы можем использовать OpenSDK с открытым исходным кодом или AI-Powered Test Recorder. OpenSDK оборачивается в Selenium и поддерживает Java, C# или Python. Наши тестовые сценарии похожи на кросс-браузерное тестирование в Selenium с минимальными изменениями в коде и зависимостях. Мы должны включить зависимость TestProject для Maven или Gradle, импортировать драйверы браузера и передать токен.

<dependency>     <groupId>io.testproject</groupId>     <artifactId>java-sdk</artifactId>     <version>0.65.0-RELEASE</version> </dependency>
implementation 'io.testproject:java-sdk:0.65.0-RELEASE'
import io.testproject.sdk.drivers.web.ChromeDriver;import io.testproject.sdk.drivers.web.FirefoxDriver;import io.testproject.sdk.drivers.web.EdgeDriver;

AI-Powered Test Recorder

С помощью AI-Powered Test Recorder мы создаем новое веб-задание, затем выбираем несколько браузеров, таких как Chrome, Edge и Firefox. Тест в задании TestProject позволяет нам выбрать дополнительный источник данных CSV, если мы хотим выполнить тестирование на основе данных. Вот несколько скриншотов, показывающих шаги по выполнению кросс-браузерного тестирования и отчета.

Вот пошаговая демонстрация кросс-браузерного тестирования с помощью TestProject AI-Powered Test Recorder.

Выводы

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

Кросс-браузерное тестирование осуществляется с помощью Selenium и TestProject.

TestProject позволяет нам создавать собственные тестовые сценарии с использованием Java, C# или Python после добавления OpenSDK с открытым исходным кодом. OpenSDK является оберткой Selenium, поэтому он содержит команды Selenium плюс дополнительные команды из TestProject. Кроме того, мы можем использовать TestProject's AI-Powered Test Recorder для проведения кросс-браузерного тестирования. Это удобный процесс, который требует от нас только выбора браузера, который мы хотим использовать для кросс-браузерного тестирования.


Перевод статьи подготовлен в рамках курса "Java QA Engineer. Basic". Всех желающих приглашаем на двухдневный онлайн-интенсив Теория тестирования и практика в системах TestIT и Jira. На интенсиве мы узнаем, что такое тестирование и откуда оно появилось, кто такой тестировщик и что он делает. Изучим модели разработки ПО, жизненный цикл тестирования, чек листы и тест-кейсы, а также дефекты. На втором занятии познакомимся с одним из главных трекеров задач и дефектов Jira, а также попрактикуемся в TestIT отечественной разработке для решения задач по тестированию и обеспечению качества ПО.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru