Pull to refresh

Автоматизация тестирования Windows-приложений с использованием .Net

Reading time 11 min
Views 9.9K

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


Тестирование – деятельность, выполняемая для оценки и улучшения качества программного обеспечения. Эта деятельность, в общем случае, базируется на обнаружении дефектов и проблем в программных системах.
Тестирование программных систем состоит из динамической верификации поведения программ на конечном (ограниченном) наборе тестов, выбранных соответствующим образом из обычно выполняемых действий прикладной области и обеспечивающих проверку соответствия ожидаемому поведению системы.
Основным подходом при тестировании программного обеспечения является тестирование «черного ящика». При данном подходе тестировщику неизвестно внутреннее устройство программы. Тестировщик взаимодействует с программой: вводит данные, нажимает кнопки, манипулирует другими визуальными компонентами и оценивает результаты.

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

Механизмы платформы .Net


При разработке Windows-приложений на платформе .Net можно воспользоваться платформой для создания легковесного и гибкого инструмента, который будет запускать разработанные тесты. Не обязательно создавать полное решение, иногда имеет смысл быстрое создание набора автоматизированных тестов, которые будут заданы прямо в коде. Для быстрой модификации и добавления тестов необходимо создать и использовать сборку с различными методами, которые будут выполнять с пользовательским интерфейсом программы необходимые тестировщику действия.
При автоматизированном тестировании пользовательского интерфейса Windows-приложений возможны два решения. Первое из них основано на механизме отражения (Reflection).
Механизм отражения позволяет получать объекты, которые описывают сборки, модули и типы. Отражение можно использовать для динамического создания экземпляра типа, привязки типа к существующему объекту, а также получения типа из существующего объекта и вызова его методов или доступа к его полям и свойствам.
Второй подход основан на низкоуровневых функциях библиотек Win32 API: FindWindow, FindWindowEx, SendMessage, PostMessage и механизме P/Invoke (вызов неуправляемого кода).
Вызов неуправляемого кода — это служба, которая позволяет управляемому программному коду вызывать неуправляемые функции, реализованные в библиотеках динамической компоновки (DLL). Вызов неуправляемого кода обнаруживает и вызывает экспортируемую функцию.
Второй подход работает не только для .Net-приложений, но для любых Windows-приложений с пользовательским интерфейсом.

Тестируемое приложение


Тестируемое приложение складывает два целых числа и после нажатия на кнопку выводит результат. Если сложение не удалось по каким-либо причинам, вместо результата выводится слово “Error”.





Доступ к коду при тестировании «черного ящика» не требуется. Мы можем не использовать наш код и получить всю необходимую информацию из сборки и запущенного приложения. Но при доступе к коду некоторые процедуры упрощаются.
Приложение находится по адресу D:\visual studio 2010\Projects\WindowsFormsApplication1\WindowsFormsApplication1\bin\Debug\AUT.exe
Название AUT – общепринятое сокращение Application Under Testing.

Тестирование основанное на механизме отражения.


При использовании механизма отражения необходимо загрузить сборку, получить тип формы, создать экземпляр и запустить приложение с данной формой в новом потоке.
Если у тестировщика нет доступа к исходному коду, то можно использовать утилиту Reflector компании Red Gate’s. C ее помощью можно посмотреть названия формы, контролов и методов.



Таким образом можно узнать, что класс формы называется Form1. Текстовые поля имеют названия textBox1и textBox2, кнопка – button1, вывод результата производится в Label с именем label1. Метод обработки нажатия на кнопку называется button1_Click и принимает на вход параметры типа object и EventArgs.
Для создания автоматизированного теста данных знаний о внутреннем устройстве приложения достаточно.
Для повышения повторного использования кода создадим ряд классов и методов.
Запуск тестируемого приложения происходит с использованием метода StartApplication.

static Form StartApplication(string path, string formName)
{
Form result = null;
Assembly a = Assembly.LoadFrom(path);
Type t = a.GetType(formName);
result = (Form)a.CreateInstance(t.FullName);
ApplicationState aps = new ApplicationState(result);
ThreadStart ts = new ThreadStart(aps.RunApp);
Thread thread = new Thread(ts);
thread.Start();
return result;
}

* This source code was highlighted with Source Code Highlighter.


На вход передается путь к сборке и имя формы. Указанная сборка загружается, определяется тип формы, создается экземпляр формы и с его использованием запускается приложение в новом потоке: указывается делегат и метод для запуска в новом потоке.
Класс ApplicationState.

class ApplicationState
{
public readonly Form formToRun;

public ApplicationState(Form f)
{
this.formToRun = f;
}

public void RunApp()
{
Application.Run(formToRun);
}
}

* This source code was highlighted with Source Code Highlighter.


Для повторного использования необходимо определить ряд методов, полей и делегатов.

static BindingFlags flags = BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.Static |
BindingFlags.Instance;

delegate void SetControlPropertyValueHandler(Form f, string controlName, string propertyName, object newValue);

static void SetControlPropertyValue(Form f, string controlName, string propertyName, object newValue)
{
if (f.InvokeRequired)
{
f.Invoke(
new SetControlPropertyValueHandler(SetControlPropertyValue),
new object[] { f, controlName, propertyName, newValue }
);
}
else
{
Type t1 = f.GetType();
FieldInfo fi = t1.GetField(controlName, flags);
object ctrl = fi.GetValue(f);
Type t2 = ctrl.GetType();
PropertyInfo pi = t2.GetProperty(propertyName);
pi.SetValue(ctrl, newValue, null);
}
}
static AutoResetEvent are = new AutoResetEvent(false);
delegate void InvokeMethodHandler(Form f, string methodName, params object[] parms);
static void InvokeMethod(Form f, string methodName, params object[] parms)
{
if (f.InvokeRequired)
{
f.Invoke(
new InvokeMethodHandler(InvokeMethod),
new object[] { f, methodName, parms }
);
}
else
{
Type t = f.GetType();
MethodInfo mi = t.GetMethod(methodName, flags);
mi.Invoke(f, parms);
are.Set();
}
}
delegate object GetControlPropertyValueHandler(Form f, string controlName, string propertyName);
static object GetControlPropertyValue(Form f, string controlName, string propertyName)
{
if (f.InvokeRequired)
{
object iResult = f.Invoke(
new GetControlPropertyValueHandler(GetControlPropertyValue),
new object[] { f, controlName, propertyName }
);
return iResult;
}
else
{
Type t1 = f.GetType();
FieldInfo fi = t1.GetField(controlName, flags);
object ctrl = fi.GetValue(f);
Type t2 = ctrl.GetType();
PropertyInfo pi = t2.GetProperty(propertyName);
object gResult = pi.GetValue(ctrl, null);
return gResult;
}
}
delegate object GetControlHandler(Form f, string controlName);
static object GetControl(Form f, string controlName)
{
if (f.InvokeRequired)
{
object iCtrl = f.Invoke(
new GetControlHandler(GetControl),
new object[] { f, controlName }
);
return iCtrl;
}
else
{
Type t1 = f.GetType();
FieldInfo fi = t1.GetField(controlName, flags);
object gCtrl = fi.GetValue(f);
return gCtrl;
}
}
delegate object GetFormPropertyValueHandler(Form f, string propertyName);
static object GetFormPropertyValue(Form f, string propertyName)
{
if (f.InvokeRequired)
{
object iResult = f.Invoke(
new GetFormPropertyValueHandler(GetFormPropertyValue),
new object[] { f, propertyName }
);
return iResult;
}
else
{
Type t = f.GetType();
PropertyInfo pi = t.GetProperty(propertyName);
object gResult = pi.GetValue(f, null);
return gResult;
}
}
delegate void SetFormPropertyValueHandler(Form f, string propertyName, object newValue);
static void SetFormPropertyValue(Form f, string propertyName, object newValue)
{
if (f.InvokeRequired)
{
f.Invoke(
new SetFormPropertyValueHandler(SetFormPropertyValue),
new object[] { f, propertyName, newValue }
);
}
else
{
Type t = f.GetType();
PropertyInfo pi = t.GetProperty(propertyName);
pi.SetValue(f, newValue, null);
}
}

* This source code was highlighted with Source Code Highlighter.


Свойство InvokeRequired получает значение, показывающее, следует ли вызывающему оператору обращаться к методу invoke во время вызовов метода из элемента управления, так как вызывающий оператор находится не в том потоке, в котором был создан элемент управления [4].
InvokeMethod – вызывает метод, указанный в параметрах с указанными параметрами.
SetControlPropertyValue – устанавливает свойство указанного контрола.
GetControlPropertyValue – получает значение указанного свойства контрола.
SetFormPropertyValue – устанавливает указанное свойство формы.
GetFormPropertyValue – получает значение свойства формы.
GetControl – получает указанный контрол.
Используя данные методы можно создать тестирующее приложение.

static void Main(string[] args)
{
string path = @"D:\visual studio 2010\Projects\WindowsFormsApplication1\WindowsFormsApplication1\bin\Debug\aut.exe";
string nameForm = "AUT.Form1";
Form myForm = StartApplication(path, nameForm);
string f1 = "521";
string f2 = "367";
SetControlPropertyValue(myForm, "textBox1", "Text", f1);
SetControlPropertyValue(myForm, "textBox2", "Text", f2);
object ctrl = GetControl(myForm, "button1");
InvokeMethod(myForm, "button1_Click", ctrl, EventArgs.Empty);
string res = GetControlPropertyValue(myForm, "label1", "Text").ToString();
string resTest = "FAIL";
if (res == "888") resTest = "PASS";
Console.WriteLine("{0} + {1} = {2}. Test {3}", f1, f2, res, resTest);
}

* This source code was highlighted with Source Code Highlighter.


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

Тестирование основанное на вызове неуправляемого кода.


Для тестирования используется ряд функций из библиотеки user32.dll.
Для импорта функций используется атрибут DllImport.

[DllImport("user32.dll", EntryPoint = "FindWindow", CharSet = CharSet.Auto)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

[DllImport("user32.dll", EntryPoint = "FindWindowEx", CharSet = CharSet.Auto)]
static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);

[DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
static extern void SendMessage1(IntPtr hWnd, uint Msg, int wParam, int lParam);

[DllImport("user32.dll", EntryPoint = "PostMessage", CharSet = CharSet.Auto)]
static extern bool PostMessage1(IntPtr hWnd, uint Msg, int wParam, int lParam);

[DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
static extern int SendMessage3(IntPtr hWndControl, uint Msg, int wParam, byte[] lParam);

* This source code was highlighted with Source Code Highlighter.


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

static void ClickKey(IntPtr hControl, VKeys key)
{
PostMessage1(hControl, (int)WMessages.WM_KEYDOWN, (int)key, 0);
PostMessage1(hControl, (int)WMessages.WM_KEYUP, (int)key, 0);
}
static void ClickOn(IntPtr hControl)
{
PostMessage1(hControl, (int)WMessages.WM_LBUTTONDOWN, 0, 0); // button down
PostMessage1(hControl, (int)WMessages.WM_LBUTTONUP, 0, 0); // button up
}
static void SendChar(IntPtr hControl, char c)
{
uint WM_CHAR = 0x0102;
SendMessage1(hControl, WM_CHAR, c, 0);
}
static void SendChars(IntPtr hControl, string s)
{
foreach (char c in s)
{
SendChar(hControl, c);
}
}
static IntPtr FindWindowByIndex(IntPtr hwndParent, int index)
{
if (index == 0)
return hwndParent;
else
{
int ct = 0;
IntPtr result = IntPtr.Zero;
do
{
result = FindWindowEx(hwndParent, result, null, null);
if (result != IntPtr.Zero)
++ct;
} while (ct < index && result != IntPtr.Zero);
return result;
}
}
static List<IntPtr> GetAllControls(IntPtr hwndParent)
{
IntPtr ctrl = IntPtr.Zero;
List<IntPtr> res = new List<IntPtr>();
ctrl = FindWindowEx(hwndParent, ctrl, null, null);
while (ctrl != IntPtr.Zero)
{
res.Add(ctrl);
ctrl = FindWindowEx(hwndParent, ctrl, null, null);
}
return res;
}
static IntPtr FindMainWindowHandle(string caption)
{
IntPtr mwh = IntPtr.Zero;
bool formFound = false;
int attempts = 0;
do
{
mwh = FindWindow(null, caption);
if (mwh == IntPtr.Zero)
{
Thread.Sleep(100);
++attempts;
}
else
{
formFound = true;
}
} while (!formFound && attempts < 25);
if (mwh != IntPtr.Zero)
return mwh;
else
throw new Exception("Could not find Main Window");
}

* This source code was highlighted with Source Code Highlighter.


Основная задача – определение порядка контролов. Можно получить указатель на контрол, если известно его свойство name/title/caption. Если же это свойство неизвестно, или оно не уникально – необходимо использовать метод FindWindowByIndex. Нулевым контролом будет само окно. Далее контролы следуют в порядке добавления:
this.Controls.Add(this.label1);
this.Controls.Add(this.button1);
this.Controls.Add(this.textBox2);
this.Controls.Add(this.textBox1);

* This source code was highlighted with Source Code Highlighter.

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



Первым номером идет Label, вторым кнопка, третьим и четвертым текстовые поля.
На основе этой информации создается код для тестирования приложения.

static void Main(string[] args)
{
string path = @"D:\visual studio 2010\Projects\WindowsFormsApplication1\WindowsFormsApplication1\bin\Debug\aut.exe";
string nameForm = "AUT";
Process p = Process.Start(path);
//запускаем приложение
IntPtr mwh = FindMainWindowHandle(nameForm);
//получаем указатель на главное окно
IntPtr tb1 = FindWindowByIndex(mwh, 3);
IntPtr tb2 = FindWindowByIndex(mwh, 4);
//получаем указатели текстовых полей
string f1 = "521";
SendChars(tb1, f1);
string f2 ="367";
SendChars(tb2, f2);
//пишем туда
IntPtr btn = FindWindowByIndex(mwh, 2);
ClickOn(btn);
//получаем указатель на кнопку и нажимаем на нее
Thread.Sleep(150);
IntPtr lbl = FindWindowByIndex(mwh, 1);
uint WM_GETTEXT = 0x000D;
byte[] buffer = new byte[256];
string res = null;
int numFetched = SendMessage3(lbl, WM_GETTEXT, 256, buffer);
res = System.Text.Encoding.Unicode.GetString(buffer);
//получаем содержимое Label
string resTest = "FAIL";
if (res == "888") resTest = "PASS";
Console.WriteLine("{0} + {1} = {2}. Test {3}", f1, f2, res, resTest);
Thread.Sleep(3000);
p.CloseMainWindow();
p.Close();
}

* This source code was highlighted with Source Code Highlighter.

Заключение


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

Список источников


1. swebok.sorlik.ru/4_software_testing.html IEEE Guide to Software Engineering Body of Knowledge, SWEBOK, 2004. Основы Программной Инженерии. Перевод Сергей Орлик.
2. Сэм Канер, Джек Фолк, Енг Кек Нгуен. Тестирование программного обеспечения. Фундаментальные концепции менеджмента бизнес-приложений. ДиаСофт, 2001.
3. msdn.microsoft.com/ru-ru/library/ms173183.aspx — Отражение (C# и Visual Basic).
4. msdn.microsoft.com/ru-ru/library/system.windows.forms.control.invokerequired.aspx — Библиотека классов платформы .NET Framework. Свойство Control.InvokeRequired.
5. msdn.microsoft.com/ru-ru/library/26thfadc.aspx — Использование неуправляемых функций DLL.
6. James D. McCaffrey. Net test automation recipes: a problem-solution approach. books.google.ru/books?id=3vN9zsMLvxkC
7. www.automatedtestinginstitute.com/home/index.php?option=com_content&task=view&id=1312&option=com_content&Itemid=1000 UI Automation Beneath the Presentation Layer. Bj Rollison. Ссылка на журнал с полной версией статьи. www.automatedtestinginstitute.com/home/ASTMagazine/2010/AutomatedSoftwareTestingMagazine_June2010.pdf
8. msdn.microsoft.com/en-us/magazine/cc163864.aspx Lightweight UI Test Automation with .NET. James McCaffrey.
9. habrahabr.ru/blogs/net/58582 NET в unmanaged окружении: platform invoke или что такое LPTSTR.
10. PInvoke.net

Исходный код проекта
Tags:
Hubs:
+6
Comments 17
Comments Comments 17

Articles