Интеграция Ruby и TestComplete

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

Более того, со временем идеологии автоматизации тоже меняются и если раньше делали упор на таких скриптовых языках, как VBScript, с целью упростить написание тестов за счет простоты и популярности данных языков (по-крайней мере так маркетинг предполагает), то сейчас автоматизация UI-level тестирования все-таки больше тяготеет к интеграции в процесс разработки. А это влечет за собой тенденцию к разработке тестов с использованием языков программирования, которые более интенсивно используются при разработке самих приложений. Тот же Selenium в этом плане даже не является исключением, а скорее подтверждением этой тенденции.

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

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

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

Предыстория

На самом деле ситуация совсем не выдуманная, а вполне реальная. Жило-было себе приложение, которое имело несколько компонент, различных клиентов. И были как десктопные клиенты (Win32, .NET), так и Web-часть. Когда имеем дело с миксом типов UI, то тут в большинстве случаев приходится задействовать инструменты от вендоров. И в этом TestComplete имел ряд выигрышей: широкий спектр поддерживаемых технологий и цена. Эти составляющие и сыграли в пользу выбора данного инструмента.

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

Но однажды появляется необходимость подключения еще одной платформы. Например, какую-то часть десктопных приложений решили портировать на Mac. А TestComplete там уже не так силен, как в Windows.

Но и на этом все не заканчивается. Есть еще тесты на уровне API, а также появились какие-то наброски UI-level тестов для Mac. И все это на Ruby, причем начала активно продвигаться идея Behavior Driven Development, соответственно, тут же нарисовался Cucumber. В общем, нарисовалась схема вида:

 

Ruby и TestComplete 

 

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

Надо что-то делать

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

Во-первых, когда у нас есть такой «зоопарк» технологических решений, нам приходится ряд дублирующих операций реализовывать для всех используемых систем. А это потери по времени. Для того, чтобы эти потери минимизировать (полностью устранить их так просто не получится), нужно получить некоторый общий для всех решений уровень, на котором будет реализовано большую часть функционала. Как минимум, набор общих операций нужно будет реализовывать только 1 раз. Еще одним преимуществом является возможность задействовать низкоуровневый функционал на уровне API, если надо оптимизировать какие-то начальные операции. Имея общий слой, это вполне выполнимо.

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

В-третьих, было бы неплохо иметь возможность привлечь и нетехнических специалистов к написанию тестов.

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

В идеале хотелось бы получить структуру вида:

 

Ruby и TestComplete 

 

То есть, в качестве общего слоя будет набор Ruby модулей, которые предоставляют интерфейс взаимодействия с системой. А на самом верхнем уровне тесты пишутся с использованием natural language инструкций, которые потом обрабатывает Cucumber.

Но в этой схеме мы потеряли TestComplete, что не очень хорошо, так как существующее решение должно для нас по-прежнему работать. Да и переписывать все тесты с нуля – тоже не самое эффективное решение.

Нужно как-то совместить и уже существующее решение и то, что мы хотим получить. В итоге нам нужно получить структуру вида:

 

Ruby и TestComplete 

 

Как видно, и есть общий уровень, и TestComplete по-прежнему на месте и по-прежнему взаимодействует с приложением под Windows. Осталось только выяснить, как же мы можем обеспечить взаимодействие между Ruby и TestComplete.

Решение

Итак, у нас есть 2 компонента: Ruby и TestComplete. Между ними нужно обеспечить связь. Причем код вызывается на Ruby, а фактически выполняется TestComplete. В этом случае можно было бы применить клиент-серверную архитектуру, где Ruby выступал бы в качестве клиента, а в роли сервера был бы TestComplete. И вся коммуникация идет через HTTP. Но тут же мы получаем одну неприятность: тесты для TestComplete разрабатывались на языке Jscript, который изначально был предназначен для написания клиентских скриптов и функционала, обеспечивающего какое-то серверное поведение, он не содержит. Но на Jscript можно писать клиентский код. В то же самое время на Ruby возлагается клиентская часть. То есть у нас есть 2 клиента и для того, чтобы обеспечить коммуникации между ними, нужен какой-то промежуточный компонент. Назовем его HTTP мост. Получается вот такая структура:

 

Ruby и TestComplete 

 

Как оно должно работать? Нужно обеспечить выполнение следующих правил:

  1. Ruby-клиент опрашивает мост на предмет его готовности обработать новую команду
  2. TestComplete клиент опрашивает мост на предмет наличия команды для выполнения
  3. TestComplete клиент выполняет команды, передает результаты и сообщает мост, что выполнение завершено и клиент готов принять новую команду
  4. Когда Ruby-клиент отправляет команду на выполнение, то следующие команды будут отклонены до тех пор, пока отправленная команда не будет выполнена со стороны TestComplete и не будут получены результаты

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

Http-мост:

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

TestComplete клиент:

  • Опрашивает на наличие команд
  • Выполняет команды
  • Отправляет результаты

Ruby клиент:

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

В работе этой связки можно выделить 3 состояния:

  • Ожидание команды
  • Выполнение
  • Завершение выполнения

Итак, на начальной стадии TestComplete просто опрашивает мост, есть ли что-то, что можно было бы выполнить. То есть Ruby клиент еще ничего не передал. Схематически это выглядит так:

 

Ruby и TestComplete 

 

Как только со стороны Ruby клиента поступила команда, TestComplete клиент меняет статус выполнения на «в процессе» и начинает непосредственно выполнять команду. В это время Ruby клиент начинает опрашивать Http мост и проверять статус. Пока статус не стал в значении «завершено», этот опрос продолжается. Схематически это выглядит так:

 

Ruby и TestComplete 

 

Как только TestComplete закончил выполнение команды, он отсылает результаты и ставит статус выполнения в «завершено». Увидев нужный статус, Ruby клиент первым делом спрашивает результаты выполнения. Они уже доступны на уровне Ruby кода, где их ждут всяческие проверки и прочая обработка, отражающая уже логику тестов. Схематически это выглядит так:

 

Ruby и TestComplete 

 

Вот таким вот образом должна работать наша система. После этого шага всё возвращается к первой стадии.

Формат передаваемых данных

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

По сути у нас есть 2 вида данных:

  1. Информация об исполняемой команде – направлена на TestComplete клиент
  2. Результаты выполнения тестов – направлены на Ruby клиент

Соответственно, формат данных адаптируется под соответствующие клиенты.

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

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

Реализация

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

Http мост

По сути это небольшой веб-сервис, у которого есть несколько ресурсов. Часть из них отправляются TestComplete клиентом, часть – Ruby клиентом. Примерная схема запросов такова:

  • Запросы, отправляемые TestComplete клиентом:
    • GET /exec – получает команду для выполнения (если есть)
    • POST /status?stat={pending|done} – устанавливает статус выполнения команды.
    • POST /resultsпосылает результаты выполнения  
  • Запросы, отправляемые Ruby клиентом:
    • POST /exec – посылает команду для выполнения
    • GET /status – получает текущий статус выполнения (либо pending, либо done)
    • GET /results – извлекает статус последней выполненной команды
    • GET /exitостанавливает работу моста  

Все, что осталось добавить, так это непосредственно код реализации. Вот он:

{syntaxhighlighter brush: java;fontsize: 100; first-line: 1;}package com.mycompany.httpbridge;

import java.io.;
import java.net.
;
import java.util.*;

public class Server extends Thread {

Socket connectedClient = null;
BufferedReader inFromClient = null;
DataOutputStream outToClient = null;
public static String prevQueryString = "";


public static String commandQuery = "";
public static boolean isPending = false;
public static String lastResult = "";

public Server(Socket client) {
	connectedClient = client;
}

public void run() {

	try {
	
		inFromClient = new BufferedReader(new InputStreamReader (connectedClient.getInputStream()));
		outToClient = new DataOutputStream(connectedClient.getOutputStream());
		
		String requestString = inFromClient.readLine();
		String headerLine = requestString;
		
		StringTokenizer tokenizer = new StringTokenizer(headerLine);
		String httpMethod = tokenizer.nextToken();
		String httpQueryString = tokenizer.nextToken();
		String httpRequestBody = "";
		
		StringBuffer responseBuffer = new StringBuffer();
		
		requestString = "";
		while( inFromClient.ready() ){
			int res = inFromClient.read();
			requestString = requestString + (char)res;
		}
		try {
			httpRequestBody = requestString.split("\r\n\r\n")[1];
		}
		catch( Throwable e ){
			;
		}
		
		if( Server.prevQueryString.equals( httpMethod + " " + httpQueryString ) ){
			System.out.print( "." );
		}
		else {
			System.out.println( "" + httpMethod + " " + httpQueryString );
			Server.prevQueryString = "" + httpMethod + " " + httpQueryString;
		}
		
		if (httpMethod.equals("GET")) {
			if (httpQueryString.startsWith("/exec")){
				if( Server.commandQuery.equals( "" ) ){
					if( Server.isPending ){
						responseBuffer.append( "Execution is pending" );
						sendResponse( 404, responseBuffer.toString() , false);							
					}
					else {
						if( !Server.prevQueryString.equals( httpMethod + " " + httpQueryString ) ){
							System.out.println( "TCClient: No operations in the stack" );
						}
						responseBuffer.append( "No function is in execution stack" );
						sendResponse(404, responseBuffer.toString(), false);
					}
				}
				else {
					responseBuffer.append( Server.commandQuery );
					Server.isPending = true;
					if( !Server.prevQueryString.equals( httpMethod + " " + httpQueryString ) )
						System.out.println( "TCClient: Execution is pending now..." );
					sendResponse(200, responseBuffer.toString(), false);
					Server.commandQuery = "";
				}
					
			}
			else if(httpQueryString.startsWith("/status")){
				if( Server.isPending ){
					if( !Server.prevQueryString.equals( httpMethod + " " + httpQueryString ) )
						System.out.println( "Ruby Client: command execution is still pending" );
					responseBuffer.append( "Ruby Client: command execution is still pending" );
					sendResponse(200, responseBuffer.toString(), false);
				}
				else {
					if( !Server.prevQueryString.equals( httpMethod + " " + httpQueryString ) )
						System.out.println( "Ruby Client: no commands to execute for now" );
					responseBuffer.append( "Ruby Client: no commands to execute for now" );
					sendResponse(204, responseBuffer.toString(), false);
				}
			}
			else if( httpQueryString.startsWith( "/results" ) ){
				System.out.println( "Send results:\r\n" + Server.lastResult + "\r\n\r\n");
				responseBuffer.append( Server.lastResult );
				sendResponse(200, responseBuffer.toString(), false);
			}
			else if(httpQueryString.startsWith("/exit")){
				responseBuffer.append( "TCClient: quitting" );
				sendResponse(200, responseBuffer.toString(), false);
				connectedClient.close();
				System.exit( 0 );
			}
		}
		else if( httpMethod.equals( "POST" )) {
			if (httpQueryString.startsWith("/exec")){
				
				responseBuffer.append( httpRequestBody );
				System.out.println( httpQueryString );
				Server.commandQuery = httpRequestBody;
				sendResponse(200, responseBuffer.toString(), false);
			}
			else if(httpQueryString.startsWith("/status")){
				if( !Server.prevQueryString.equals( httpMethod + " " + httpQueryString ) )
					System.out.println( "TCClient: operating with status" );

				httpQueryString = httpQueryString.split( "stat=" )[1];
				
				if( httpQueryString.equals( "done" ) ){
					if( Server.isPending ){
						if( !Server.prevQueryString.equals( httpMethod + " " + httpQueryString ) )
							System.out.println( "TCClient: command execution completion" );
						responseBuffer.append( "TCClient: setting execution status to done" );
						sendResponse(204, responseBuffer.toString(), false);
						Server.isPending = false;
						Server.commandQuery = "";
					}
					else {
						responseBuffer.append( "TCClient: command execution has already been stopped" );
						sendResponse(200, responseBuffer.toString(), false);
					}
				}
				else {
					Server.isPending = true; 
					responseBuffer.append( "TCClient: continue with pending status" );
					sendResponse(200, responseBuffer.toString(), false);
				}
			}
			else if( httpQueryString.startsWith( "/results" ) ){
				Server.lastResult = httpRequestBody;
				
				responseBuffer.append( "TCClient: results were posted" );
				sendResponse(204, responseBuffer.toString(), false);
			}
			else {
				responseBuffer.append( "Undefined resource: " + httpQueryString );
				sendResponse(404, responseBuffer.toString(), false);
			}
		}
		else{
			sendResponse(404, "The Requested resource not found ....", false);
		}
		
		responseBuffer.append( "Closing client" );
		this.connectedClient.close();
		
	} 
	catch (Exception e) {
		e.printStackTrace();
	}
}

public void sendResponse (int statusCode, String responseString, boolean isFile) throws Exception {

	String statusLine = null;
	String serverdetails = "Server: Java HTTPServer";
	String contentLengthLine = null;
	String contentTypeLine = "Content-Type: text/html" + "\r\n";
	
	switch(statusCode){
		case 200:
		case 201:
		case 204:
			statusLine = "HTTP/1.1 " + statusCode + " OK" + "\r\n";
			break;
		case 404:
			statusLine = "HTTP/1.1 " + statusCode + " Not Found" + "\r\n";
			break;
		default:
			statusLine = "HTTP/1.1 " + statusCode + " Undefined" + "\r\n";
			break;
	}
		
	contentLengthLine = "Content-Length: " + responseString.length();
	
	outToClient.writeBytes(statusLine);
	outToClient.writeBytes(serverdetails);
	outToClient.writeBytes(contentTypeLine);
	outToClient.writeBytes(contentLengthLine);
	outToClient.writeBytes("Cache-Control: no-cache\r\n");
	outToClient.writeBytes("Connection: close\r\n");
	outToClient.writeBytes("\r\n");
	
	outToClient.writeBytes(responseString);
	
	outToClient.close();
}

public static void main (String args[]) throws Exception {
	
	ServerSocket server = new ServerSocket (5000, 10, InetAddress.getByName("127.0.0.1"));
	System.out.println ("TCPServer Waiting for client on port 5000");
	
	while(true) {
	Socket connected = server.accept();
	    (new Server(connected)).start();
	}
}

}{/syntaxhighlighter}

TestComplete клиент

У TestComplete клиента есть несколько функциональных частей. В основе всего лежит функционал посылки запроса и получение ответа. В Гугле по запросу “Ajax example JScript” можно найти множество примеров, которые можно адаптировать. Для нашего случая был получен вот такой код: 

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1;}function createRequest(method, URL , body) { var ajaxRequest; // The variable that makes Ajax possible!

if( body == undefined || body == null ){
    body = null;
}

if( ajaxRequest == null ){
    try{
        // Opera 8.0+, Firefox, Safari
        ajaxRequest = new XMLHttpRequest();
    } catch (e){
        // Internet Explorer Browsers
        try
        {
            ajaxRequest = new ActiveXObject("Msxml2.XMLHTTP");
        }
        catch (e)
        {
            try{
                ajaxRequest = new ActiveXObject("Microsoft.XMLHTTP");
            } 
            catch (e)
            {
                // Something went wrong
                Log.Error("Your browser broke!");
                return false;
            }     
        }
    }
}

ajaxRequest.open(method, URL , false);
ajaxRequest.setRequestHeader("Pragma", "no-cache");
ajaxRequest.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");
ajaxRequest.setRequestHeader("Cache-Control", "no-cache");

return ajaxRequest;

}

function ajaxFunction( method, URL , body ){
var ajaxRequest = createRequest( method, URL , body );
var retries = 5;

while( retries > 0 ){
    try {
        ajaxRequest.send( body );
    }
    catch( e ){
        var message = e.description;

        if( message.indexOf( "System error:" ) >= 0 ){
            delete ajaxRequest;
            ajaxRequest = createRequest( method, URL , body );
            retries--;
        }
        else {
            retries = 0;
        }
    }
}

var result = new Array( ajaxRequest.status , ajaxRequest.responseText );
delete ajaxRequest;

return result;

} {/syntaxhighlighter}

Здесь 2 функции:

·         createRequest – инициализирует объект, отвечающий за посылку запроса (в зависимости от браузера могут использоваться разные объекты) и пробует отправить запрос

·         ajaxFunction – непосредственно отвечает за отправку запроса, причем не просто отправляет запрос, но и получает результат. При этом еще обрабатывается ситуация, когда запрос по каким-то причинам не отправился. В данном фрагменте у функции есть 5 попыток отправить запрос с полной реинициализацией COM-объектов, отвечающих за отправку запроса.

Это ядро. Уже на его основе надстраиваются дополнительные функции, которые:

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

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

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1;}var errors = new Array(); var data = new Array(); {/syntaxhighlighter}

Далее мы реализуем код, который будет ждать поступления команд и установит нужный статус. Добавляем функции pollForInstruction, setExecStatus. Реализация выглядит так:

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1;}function pollForInstruction( host , port ){ if( host == undefined || host == null ){ host = "127.0.0.1"; }

if( port == undefined || port == null ){
    port = 5000;
}

var result = ajaxFunction( "GET" , "http://" + host + ":" + port + "/exec" );
if( result[0] != 404 ){
    setExecStatus( false , host , port );
    if( result[1] == "exit();" ){
        setExecStatus( true , host , port );
        throw -1, "The end";
    }
    return result[1];
}
return null;

}

function setExecStatus( done , host , port ){
if( host == undefined || host == null ){
host = “127.0.0.1”;
}
if( port == undefined || port == null ){
port = 5000;
}

try {
    ajaxFunction( "POST" , "http://" + host + ":" + port + "/status?stat=" + ((done)?("done"):("pending")) , null );
}
catch( e ){
    //Log.Warning( e.description );
}

}{/syntaxhighlighter}

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

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1;}function postResults( retVal, errList , host, port )  {/syntaxhighlighter}{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1;} if( host == undefined || host == null ){

host = "127.0.0.1";

}

if( port == undefined || port == null ){

port = 5000;

}

requestBody = "ret_val: " + retVal + “\r\nerrors: \r\n”;

var i;

for( i = 0 ; i < errList.length ; i++ ){

requestBody = requestBody + "  - " + errList[i] + "\r\n";

}

errors = new Array();

ajaxFunction( “POST”, “http://” + host + “:” + port + “/results” , requestBody );

}{/syntaxhighlighter}

И теперь у нас есть все необходимое для написания функционала выполнения команд: 

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1;}function runFunction( host , port ){

if( host == undefined || host == null ){

host = "127.0.0.1";

}

if( port == undefined || port == null ){

port = 5000;

}

var result = pollForInstruction( host , port );

if( result == null ){

return;

}

var retVal = null;

try {

Log.Message( "Running: " + result );

retVal = eval( result );

Log.Message( "Completed" );

}

catch( e ){

Log.Error( "Unhandled error:" + e.description );

}

postResults( retVal , errors , host, port );

setExecStatus( true , host, port );

}
{/syntaxhighlighter}

Ключевой частью данного метода является вызов

 retVal = eval( result );

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

Чтобы они были полностью работоспособными нужно добавить один небольшой фрагмент кода. Когда мы отправляем результаты, мы передаем и список ошибок,которые возникли. Для этого мы использовали глобальную переменную errors. Но где мы будем заполнять этот массий значениями?

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

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1;}function GeneralEvents_OnLogError(Sender, LogParams) { errors.push( LogParams.Str ); } {/syntaxhighlighter}

Тут важно не забыть подключать нужные модули.

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

 function runner(){

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1;} var host = "127.0.0.1"; var port = 5000;

if( host == undefined || host == null ){
    host = "127.0.0.1";
}

if( port == undefined || port == null ){
    port = 5000;
}

try {
    setExecStatus( true , host , port );

    while( true ){
        runFunction( host , port );
        BuiltIn.delay( 1000 );
    }
}
catch( e ){
    Log.Message( e.description );
}

}{/syntaxhighlighter}

Всё, с TestComplete клиентом мы разобрались. Осталось
описать клиент для
Ruby.

Ruby клиент

Прежде чем начать работу с Ruby, нужно убедиться, что у нас подключены нужные модули. В нашем случае нам нужен модуль httpclient. Чтобы установить его надо в командной строке вызвать команду: 

{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1;}gem install httpclient{/syntaxhighlighter}

А вот теперь перейдем непосредственно к клиенту.

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

{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1;}require 'httpclient' require 'test/unit/assertions' require 'yaml' require 'uri'

Class responsible for interaction with Http bridge proxy.

All Ruby API calling TestComplete functionality via Http bridge

uses only one method and usage looks like:

@client.run “<some JScript code>”

class Client

include Test::Unit::Assertions

attr_reader :host
attr_reader :port

Initializes basic connection options like host and port.

These options should match to Http proxy listening the same

port and located at the specified host. By default client

sends requests to http://localhost:5000 URL

def initialize(host=“127.0.0.1”,port=5000)
@host = host
@port = port
@url = “http://#{@host}:#{@port}”
@client = HTTPClient.new
@time_out = 600
end
end {/syntaxhighlighter}

Что мы здесь храним? Мы храним информацию об адресе HTTP моста, экземпляр класса HTTPClient для непосредственной отправки запросов и время ожидания выполнения запроса.

Теперь рассмотрим ключевые методы, которые надо добавить ынутрь данного класса. Во-первых, нам нужен метод, который принудительно установит статус выполнения команды в «завершено». Дело в том, что в ряде случаев может оказаться так, что различные компоненты могут быть в различных состояниях и может так случиться, что HTTP мост хранит статус «в процессе», когда реально ничего не происходит. Из-за этого Ruby-клиент может нормально не сработать. Код метода выглядит так:

{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1;}def force_completion @client = HTTPClient.new @client.post( "#{@url}/status?stat=done" ) end{/syntaxhighlighter}

Следующий на очереди метод опрашивает мост насчет статуса выполнения команды и прекращает работу, если статус имеет значение «завершено» либо же закончилось время ожидания. Второй вариант позволяет учесть тот случай, если выполнение команды просто зависло (TestComplete этим периодически может «радовать»). Код имеет вид:

{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1;}def is_completed?( time_out = @time_out ) @client = HTTPClient.new res = @client.get( "#{@url}/status" ) time_out.times do sleep 1 if( res.status == 204 ) break end @client = HTTPClient.new res = @client.get( "#{@url}/status" ) end (res.status == 204) end{/syntaxhighlighter}

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

{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1;}def get_last_results data = nil errors = nil

5.times do |x|
@client = HTTPClient.new
res = @client.get( “#{@url}/results” )
content = res.content

data = YAML::load(content)

if data != false
  break
end
sleep( 1 )

end
errors = data[‘errors’] if data.has_key?( “errors” )
assert( false , “Errors during execution:” + errors.join( “\r\n” ) ) unless errors == nil
data[‘ret_val’]
end{/syntaxhighlighter}

Обратите внимание на то, что преобразование
ответа от сервиса (обычная строка) в структуру данных делается одной командой:

{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1;}data = YAML::load(content){/syntaxhighlighter}

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

И наконец основной метод клиентской части (и единственный, который должен использоваться за пределами класса), который отвечает за посылку команды и выдачу результатов. Он работает в такой последовательности:

  1. Ждем готовность принять команду
  2. Ставим статус «в процессе»
  3. Запускаем команду на выполнение
  4. Ждем готовность принять следующую команду

Код самого метода выглядит так:

{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1;}def run( code ) assert( is_completed? , "Operation timeout error: cannot run the command " + "because server is still busy during timeout of #{@time_out} seconds" )

@client = HTTPClient.new
@client.post( “#{@url}/status?stat=pending” )

@client = HTTPClient.new
@client.post( “#{@url}/exec” , code )

assert( is_completed? ,
"Operation timeout error: server was busy during timeout " +
“of #{@time_out} seconds” )

return get_last_results
end {/syntaxhighlighter}

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

{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1;}client = Client.new{/syntaxhighlighter}

А после этого можно делать вызовы вида:

{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1;}@client.run "RunUninstall();" {/syntaxhighlighter}

Также, этот код может быть обернут внутри какого-то класса/модуля. Главное передать объект клиента. А поскольку в run мы передаем фрагмент кода, то это необязательно будет одна команда. Это может быть набор команд. Просто возвращаемое значение будет содержать значение последнего вычисленного выражения.

Как запускать тесты

Запуск всей системы лучше всего делать в такой последовательности:

  • 1)      Запускаем HTTP-мост
  • 2)      Запускаем TestComplete-клиент и ждем, когда он начнет свою работу
  • 3)      Запускаем тесты на Ruby 

Результаты

Итак, что же мы получили в итоге?

1)      Мы добавили новый уровень кода, который уже хорошо совместим не только с GUI-тестами под Windows, но и другими тестами. То есть ряд функционала тех же низкоуровневых тестов может использоваться и в GUI тестах (один из удобных способов оптимизации работы тестов)

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

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

4)      Поскольку тесты, написанные до этого в TestComplete по-прежнему работают, то процесс миграции с одного решения на другое не означает простоя в работе тестов. То есть все это время наше решение продолжает работать.

Альтернатива

Описанное решение не является чем-то уникальным. Есть множество способов получить те же результаты. Например, одно из решений описано здесь: http://samsagiletesting.blogspot.com/2010/03/test-complete-and-ruby.html 

Тоже неплохо и имеет право на жизнь. Но у такого решения есть недостатки по сравнению с моим решением:

  1.  Сильная привязка к TestComplete – фактически в описанном решении тесты из себя представляют TestComplete тесты на Ruby. В принципе, и описываемое здесь решение имеет привязку к TestComplete, но на уровне кода никаких специфических конструкций нет, что дает возможность для маневра (напрмиер, если надо будет какой-то функционал TestComplete полностью заменить на Ruby)
  2.  Ограничения для распределенного тестирования – по-прежнему есть необходимость использовать NetworkSuite со всеми его трудностями, в то время как в описываемом в данной статье решении Ruby клиенту одинаково, кто фактически будет выполнять команды, результаты будут приходить клиенту напрямую. То есть, возможность распределенного тестирования изначально заложена в архитектуру
  3.  Сложный клиентский код – если посмотреть пример альтернативного решения, то можно увидеть, что блок инициализации достаточно большой и громоздкий. Плюс ко всему структура, по которой мы могли бы доступаться к функциям, по прежнему требует обращения к ряду вложенных объектов. В описываемом в статье решении всё уже абстрагировано таким образом, что Ruby-часть пишется исключительно на  Ruby, а TestComplete-часть пишется в TestComplete. То есть, адаптор, обеспечивающий взаимодействие между этими 2-ми частями, намного проще.

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