(@ Matteo: della serie "Tu m'hai provocato...")
Appena uscito su CodePlex, TestApi 0.1 sembra costituire uno strumento veramente potente per il testing automatico dell'UI di applicazioni WPF, Windows Forms e Win32. La parte che più mi ha affascinato sin da subito è l'UI testing tramite Input Injection (simulazione dell'input utente). In merito a questo aspetto, questa libreria fornisce due tipologie di facilitazioni per le classiche tecniche di "input simulation":
- Invocazione programmatica degli eventi dell'UI utilizzando i metodi degli AutomationElement
- Interazione con i sistemi di input stream messi a disposizione dall'OS ( es. classi Microsoft.Test.Keyboard e Microsoft.Test.Mouse )
Vediamo queste API in azione!!!
Partiamo da una semplice Window WPF sviluppata secondo il pattern M-V-VM:
Anzitutto, sfruttando le AutomationProperties identifichiamo univocamente nello XAML dell'applicazione (via AutomationId) i controlli che saranno oggetto di testing automatico per la simulazione dell'input utente.
In questo caso abbiamo una ComboBox che permette di selezionare la modalità di filtraggio dei risultati, una TextBox per l'inserimento di una filter expression, un Button per invocare il filtraggio dei risultati ed una ListBox per la visualizzazione dei risultati.
( ATTENZIONE: Questa non è l'unica tecnica per individuare gli elementi della UI, ma questo è un aspetto volutamente tralasciato per brevità :D )
<Window x:Class="MVVMSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="300" Width="500">
<Grid>
...
<StackPanel Grid.Row="0" Orientation="Horizontal">
<Label Content="Product:"></Label>
<ComboBox ... AutomationProperties.AutomationId="lstFilterCriteria"></ComboBox>
<TextBox ... AutomationProperties.AutomationId="txtProductName"></TextBox>
<Button ... AutomationProperties.AutomationId="btnFilterProducts"></Button>
</StackPanel>
<ListView ... AutomationProperties.AutomationId="listViewProducts">
...
</ListView>
...
</Grid>
</Window>
Creiamo quindi un progetto di Test in cui referenziamo gli assembly delle TestApi: DotNetUtilities.dll e WpfUtilities.dll.
Let's simulate and test!!!
Nell'esempio di codice riportato qua sotto ho realizzato un test che simula in maniera automatica i seguenti Task:
- Lancio dell'applicazione
- Selezione della modalità di filtraggio (ovvero selezione di un item della ComboBox)
- Inserimento della filterExpression (typing nella TextBox)
- Click del Button di ricerca
- Valutazione dei risultati nella ListBox
using System.Diagnostics;
using System.Windows.Automation;
using Microsoft.Test;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Windows.Input;
[TestMethod]
public void TestFilterSingleProduct()
{
string productNameToMatch = "Chang";
AutomationElement rootElement;
// 1. Lancio dell'applicazione WPF e determinazione dell'AutomationElement associata alla MainWindow (rootElement)
Process appProcess = AutomationHelpers.StartProcess(new ProcessStartInfo(@"C:\...\MVVMSample.exe"), out rootElement);
AutomationElement txtProductName = AutomationUtilities.FindElementsById(rootElement, "txtProductName")[0];
AutomationElement lstFilterCriteria = AutomationUtilities.FindElementsById(rootElement, "lstFilterCriteria")[0];
AutomationElement btnFilterProducts = AutomationUtilities.FindElementsById(rootElement, "btnFilterProducts")[0];
AutomationElement listViewProducts = AutomationUtilities.FindElementsById(rootElement, "listViewProducts")[0];
// 2. Selezione del tipo di filtro nella ComboBox (l'ultimo della lista: "EqualsTo")
MoveToAndClick(lstFilterCriteria);
Microsoft.Test.Keyboard.Press(Key.Down);
Microsoft.Test.Keyboard.Press(Key.PageDown);
Microsoft.Test.Keyboard.Press(Key.Enter);
System.Threading.Thread.Sleep(1000);
// 2. Inserimento della stringa "Chang" nella TextBox
MoveToAndClick(txtProductName);
Microsoft.Test.Keyboard.Type(productNameToMatch);
System.Threading.Thread.Sleep(1000);
// 3. Click sul Button "Search"
MoveToAndClick(btnFilterProducts);
// IMPORTANTE: ATTENDERE I RISULTATI!!! (ASPETTO CRITICO)
System.Threading.Thread.Sleep(2000);
// 4. Valutazione dei risultati del test
object o;
listViewProducts.TryGetCurrentPattern(GridPatternIdentifiers.Pattern, out o);
GridPattern grid = (GridPattern)o;
int gridRowCount = grid.Current.RowCount; // Numero di righe
AutomationElement rowItem = grid.GetItem(0, 0);
string productName = rowItem.Current.Name; // ProductName della prima riga
try
{
Assert.AreEqual(gridRowCount, 1, "The filter string '{0}' resulted more than a single result. Expected: {1} Actual: {2}", productNameToMatch, gridRowCount, 1);
Assert.AreEqual(productName, productNameToMatch, "The filter result did not match. Expected: {1} Actual: {0}", productName, productNameToMatch);
}
finally
{
AutomationHelpers.CloseWindow(rootElement);
appProcess.WaitForExit();
}
}
private void MoveToAndClick(AutomationElement element)
{
System.Windows.Point winPoint = element.GetClickablePoint();
System.Drawing.Point drawingPoint = new System.Drawing.Point((int)winPoint.X, (int)winPoint.Y);
Microsoft.Test.Mouse.MoveTo(drawingPoint);
Microsoft.Test.Mouse.Click(MouseButton.Left);
}
Lanciando il test (e mettendo le mani dietro la testa), il risultato che otteniamo è il seguente:
N.B.: L'utilizzo di Thread.Sleep(...) è fondamentale per due motivi:
- rendere i vari passi della simulazione maggiormente percepibili visivamente dal tester umano
- prevenire le situazioni in cui la simulazione dell'interazione con l'UI avvenga prima che i controlli siano correttamente inizializzati/popolati. Quest'ultimo è un aspetto molto CRITICO.