OpenQuality.ru

Качество программного обеспечения

Качество программного обеспечения: в главных ролях

Лента  Радар  Блог  Опыт  
Разум  Видео  Заметки  Эпизоды


Перемен требуют наши сердца

 

Перемены в контексте автоматизации тестирования бывают разные:
Прокрустово ложе

  • Добровольные и принудительные:
  • “Я подумал, посоветовался и решил” или “Партия сказала – комсомол ответил “Есть!””.

  • К лучшему или к худшему:
  • Более широкое покрытие кода и повышение надежности автотестов за счет расширения методик и инструментов или перемещение в прокрустово ложе одного “верного” подхода (скажем, “никаких e2e-тестов”).

  • Простые и сложные:
  • Рефакторинг класса сравнительно прост (если он выполняется грамотно и не в первый раз), а капитальный ремонт (“Мы наш, мы новый мир построим”) сулит непредсказуемые испытания.

Список критериев неполный, но даже здесь возможны примечательные комбинации: например, “принудительные, к лучшему, сложные” (”потом спасибо скажешь!”) или “добровольные, к худшему, простые” (”сам дурак”). Одно и то же изменение можно трактовать как “к лучшему“, так и “к худшему“, и если на эту тему бесконечно спорить, то далеко не уплыть.

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

Ага, подсматриваете?
В таком контексте мы не можем пенять на начальство, политику и погоду. Мы накопили опыт, нам доверяют, вперед и с песней. Но… большие перемены? А они точно нужны? Мы строили, строили… и перестраивать? В целом, сознание автоматизатора проходит следующие этапы (временами, но далеко не всегда):

  • Oтрицание (“да не надо ничего делать, ничего не поменялось, а если и поменялось, то не критично, и нам ничего менять не надо”)
  • Агрессия (“да дернул же кого-то черт эти изменения спровоцировать”)
  • Надежда (“ну ладно, может быть какие-нибудь костыли и подпорки спасут?”)
  • Апатия (“с подпорками не вышло; покатимся в тартарары либо надо что-то менять кардинально, но непонятно как подступиться”)
  • Восстать из пепла (“прорвемся”)

Проиллюстрирую эти этапы на конкретном примере. В одном проекте долгое время был задействован Selenesse – гибрид FitNesse и Selenium RC. Жили мы с этим гибридом дружно, зверя удалось приручить, многие неувязки были устранены, тесты были наглядными, все хорошо. Но в один прекрасный момент функциональности Selenesse стало не хватать. Сначала выяснилось, что в WebDriver есть более удобные возможности для работы с полями ввода и алертами. Потом захотелось добавить логику в e2e-тесты, а сделать это в рамках Selenesse было затруднительно. Поэтому были внедрены Groovy-скрипты, которые вызываются из сценариев FitNesse, позволяют обращаться к WebDriver и реализовать if/else и другие логические конструкции. И снова все стало хорошо, но…

Мы по-прежнему сильно зависили от Selenium RC, потому что много “старых” тестов были написаны на Selenesse. При этом поддержка Selenium RC становилась все более и более вялой. Багтрекер Selenium наполнялся все новыми и новыми багами (в частности, о проблемах с новыми версиями браузеров), но реакция на них была прохладной. Думалось: “Это временно. Рано или поздно их закидают багами, и они все исправят” (отрицание). Но недели шли за неделями, месяц за месяцем, и лучше не становилось. Мысли были примерно такого плана: “И зачем мы только связались с FitNesse и Selenesse? Ну почему Firefox обновляется раз в шесть недель, и его поддержка в Selenium RC быстро устаревает? Ну не могут же они совсем отказаться от поддержки Selenium RC? Столько народу на нем сидит! Куда они смотрят?” (агрессия).

Подобные мысли сдвинуть телегу не помогали, надо было что-то делать. Просматривались такие варианты:
Куда податься?

1. Сидеть на старом браузере: “Да ладно, зачем нам браузер обновлять, там ничего для нас важного не меняется”.

2. Взять исходный код Selenium RC и заняться поддержкой новых версий браузеров самостоятельно: “Представляю сколько на это времени уйдет, и не факт что получится.”

3. Разбираться с багами Selenium RC и Slim Selenium Driver “в лоб” и искать для них обходные пути.

4. Позаимствовать или сделать “свой” Selenesse, но уже не на Selenium RC, а на WebDriver. Иными словами, все Click/Open/WaitForTextElementPresent и т.п. реализовать в отдельном jar и подгружать в Fitnesse. Наша проблема не была уникальной, в Сети было найдено несколько попыток создать Selenesse 2, но все они были сырыми и заброшенными. Складывалось ощущение, что это никому не нужно.

5. Полностью отказаться от всех Selenesse-тестов и все переписать на Groovy/WebDriver. Тестов много, очень и очень трудоемко.

Придерживаться п.1 было недальновидно и стыдно, п.2 и п.4 казались совсем туманными, п.5 вызывал уныние, поэтому первые усилия были направлены на п.3: “мы встретим баг лицом к лицу как в жизни следует бойцу” (надежда).

Одним из камней преткновения при переходе с Firefox 28 на Firefox 32 был неудачный старт Slim Selenium Driver:

|start            |Slim Selenium Driver|${seleniumHost}|${seleniumPort}|${seleniumBrowser}|${baseURL}|

Чем же так отличаются FF 28 и 32 c точки зрения Slim Selenium Driver? Никакой информации в Сети, но эксперименты с опциями Firefox в pref.js (сравнение опций в FF 28 и 32) показали, что все упирается вот в эту переменную: Browser.startup.homepage_override.mstone. В FF 28 оно было равно 16.0.2, а в Firefox 32 – 32.0.3. Если после установки FF 32 вернуть это значение в 16.0.2, то драйвер стартует. Ура? Не совсем. После открытия браузера под профайлом с этим значением оно перезаписывается, и следующий запуск Slim Selenium Driver будет неудачным. Но мы же автоматизаторы. Швейцарский нож весьма кстати:

sfk filter %FirefoxPrefs% -rep "/%OriginalString%/%NewString%/" -write -yes

Но это была агония. C Firefox 33.1.1, несмотря на все приложенные усилия, не удалось избавиться от страницы What’s New, открывающейся при каждом запуске браузера. А с FF 34 функциональность Selenesse не работала совсем (не стартовал Selenium Server).

На этом попытки схлестнуться с багами были прекращены, и мы снова стали ждать, когда баги с Selenium RC будут исправлены (апатия). Но в один прекрасный момент в багтрекере Selenium появилась такая запись:
Не может быть!

“Selenium RC is deprecated and the support of Selenium RC is discontinued. Selenium team is not going to fix remaining issues in Selenium RC and we don’t accept new issue reports on Selenium RC.”

Все, отступать стало некуда. Варианты 1 и 3 отпали ранее, а после такого пассажа бороться с ветряными мельницами в п.2 показалось совершенно бесперспективным. Остались два варианта: написать свой Selenesse на базе WebDriver либо полностью отказаться от Selenesse-тестов и все переписать на Groovy.

Первый вариант по-прежнему казался туманным: “Ну почему никто этого до сих пор не сделал? Почему попыток было всего несколько, и все эти проекты заброшены?”. В те дни казалось, что будет надежней все переписать на Groovy: “Да, это трудоемко, но по крайней мере прозрачно”. Решили забить пробный шар и перенести на Groovy несколько старых тестовых сьют. Процесс получился и простым, и сложным:

Простым, потому что 1) локаторы (xpath, id, css) уже были забиты в Selenesse-тестах, и их не нужно было заново определять; 2) перевести линейные тесты, представленные в виде Fitnesse-степов без логики if/else и т.п., на язык программирования - задача простая даже для начинающих.
 
Сложным, потому что 1) подтвердились опасения, что задача будет трудоемкой; 2) тесты стали менее наглядными: все локаторы и анализ результатов оказались спрятаны в скриптах, и отлаживать/изменять тесты стало менее удобно. С таким подходом, изменения ухудшали существующее положение дел несмотря на то, что позволяли в будущем полностью отказаться от Selenium RC и поддерживать новые версии браузеров.

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

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

2. Нам не надо поддерживать все возможности WebDriver. Нам нужны только методы, идентичные методам Selenesse, которые мы использовали в “старых” тестах.

Значит, в путь! Проект получил название FitWebDriver (FitNesse + WebDriver). Вот фрагмент исходного кода c несколькими базовыми методами (без претензий на хороший стиль программирования):

 
package FitWebDriver;
 
import java.util.List;
import java.util.concurrent.TimeUnit;
 
import org.openqa.selenium.*;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.remote.UnreachableBrowserException;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.support.ui.WebDriverWait;
 
public class FitWebDriver {
 
 
	public static WebDriver driver;
	public WebElement e;
	private static int TimeWait = 30;
 
 	public void open(String url) {
 
 		System.setProperty("webdriver.firefox.profile", "Custom");
        	driver = new FirefoxDriver();
        	driver.manage().timeouts().implicitlyWait(TimeWait, TimeUnit.SECONDS);
 		driver.get(url);
	}
 
	public void stop() {
 
		try {
			driver.close();
		}
		catch (NullPointerException|UnreachableBrowserException e) {
        		System.out.print("Driver instance does not exist");
        	}
 	}
 
	private By defineLocator(String strategy,String path){
 
		By locator = null;
 
		switch (strategy.toLowerCase()){
 
		case "id":
			locator = By.id(path);
		 	break;
		case "xpath":
			locator = By.xpath(path);
			break;
		case "name":
			locator = By.name(path);
			break;
		case "classname":
			locator = By.className(path);
			break;
		case "cssselector":
			locator = By.cssSelector(path);
			break;
		case "link":
			locator = By.linkText(path);
			break;
		case "tagname":
			locator = By.tagName(path);
			break;
		case "partiallinktext":
			locator = By.partialLinkText(path);
			break;
		default:
			locator = By.className(path);
			break;
		}
 
		return locator;
	}
 
	public boolean findElement(String locator) {
 
		String[] result = locator.split("=", 2);
		String strategy = result[0]; 
		String path = result[1];
 
		try {
			e = (new WebDriverWait(driver, TimeWait))
                    .until(ExpectedConditions.presenceOfElementLocated(defineLocator(strategy,path)));
			System.out.println("Element '" + path + "' found using '" + strategy +"'.");
			return true;
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
 
		return false;
	}
 
 
	public boolean click(String locator) {
 
		boolean elementFound = findElement(locator);
 
		try{
			if (elementFound) {
				e.click();
				System.out.println("Click on: " + locator);
			}
		} catch(Exception e) {
			System.out.println(e.getMessage());	
		}
 
		return elementFound;
	}
 
	public boolean type(String locator, String text) {
 
		boolean elementFound = findElement(locator);
 
		if (elementFound) {
			try {
				e.clear();
				e.sendKeys(text);
				System.out.println("Typing: " + text);
			}catch(InvalidElementStateException stateEx){
				System.out.println(stateEx.getMessage());
			}
		}
 
		return elementFound;
	}
 
 
	public boolean waitForTextPresent(String text, int specifiedTimeout) throws InterruptedException {
 
		System.out.println("Timeout is: " + specifiedTimeout);
		Boolean result = true;
		System.out.println("waitForTextPresent " + text);
		String [] cards = text.split("\\*");
 
		for (String card : cards) {
			for(int i=1; i<specifiedTimeout; i++) {
	     			Thread.sleep(1000);
				if (!driver.getPageSource().contains(card)) {
					result = false;
				} else {
					result = true;
					break;
				}
			}
 
			if (!result) {
				return false;
			}
		}
 
		return true;
	}
 
	public boolean waitForTextPresent(String text) throws InterruptedException {
		System.out.println("Default timeout");
	    return waitForTextPresent(text,TimeWait);
	}
 
}

А дальше – самая приятная часть. Поскольку сигнатуры методов FitWebDriver с небольшими исключениями повторяли сигнатуры методов Selenesse, перевод старых тестов на новые рельсы был прозрачным:

 

1. Если раньше в SetUp мы импортировали Selenesse…

!|import |
|selenesse |

… то теперь мы импортируем FitWebDriver:

!|import |
|FitWebDriver |

2. Сами тесты почти не изменились. К примеру, если раньше сценарий установки пароля аккаунта выглядел так…

!|scenario |Set password |accountName||accPwd|
|click |id=Details | |
|type; |id=AccountName |@accountName |
|type; |id=sPswd |@accPwd |
|type; |id=sConfPswd |@accPwd |
|click |id=SubmitChangesButton | |
|waitForTextPresent|Changes saved successfully for @accountName|

… то при переходе на FitWebDriver он таким и остался за счет совпадения сигнатур методов click, type и waitForTextPresent в Selenesse и FitWebDriver.

 
Извлеченные уроки (”да никаких уроков, все было известно заранее“):

  • Hевозможное возможно.
  • К большой цели – маленькими шагами.
  • Нужно с самого начала стремиться увидеть как можно больше вариантов решения задачи и тщательно их анализировать. В идеале – не тратить время на “проходные” варианты и вплотную подступать пусть к более сложным, но зато более дальновидным решениям.
  • Нужно иметь хорошую репутацию – тебе должны доверять, чтобы у тебя было время на подобные эксперименты.

 

Всего доброго.

Отправить в Twitter, Facebook, FriendFeed, ВКонтакте | Опубликовано 15.09.2015 в рубрике "Автоматизация"

Комментарии (2)

  1. Автор комментария : 333 | February 11, 2016

    “Перемен требуют наши сердца”

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

    [Ответить]


  2. Автор комментария : Капитан | February 11, 2016

    333, соглашусь. Название статьи чересчур лиричное (песня крутилась в голове) и не отражает сути процесса. Мозги ближе к делу.

    [Ответить]



Добавить комментарий

Пожалуйста, исправьте результат: дважды два равно



КРАТКОЕ СОДЕРЖАНИЕ

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


ПУТЕВОДИТЕЛЬ

Список всех статей с краткой аннотацией и разбивкой по рубрикам. Открыть карту.

ПОДПИСКА

Доступ к самым интересным материалам по электропочте и RSS. Подробности.

ИЩЕЙКА