Ответ 1
В итоге я использовал ApplicationUnderTest.Launch(...) (MSDN), который автоматически создается при записи автоматического теста с помощью Microsoft Test Manager.
Проблема
VS2010 и TFS2010 поддерживают создание так называемых кодированных пользовательских интерфейсов. Все демо, которые я нашел, начинаются с приложения WPF, уже запущенного в фоновом режиме при запуске теста кодированного пользовательского интерфейса или запуска EXE с использованием абсолютного пути к нему.
Однако я хотел бы запустить тестируемое WPF-приложение из кода unit test. Таким образом, он также будет работать на сервере сборки и на моих рабочих торгах.
Как это сделать?
Мои открытия пока
a) В этом сообщении показано как запустить окно XAML. Но это не то, что я хочу. Я хочу запустить App.xaml, потому что он содержит ресурсы XAML, и в коде позади файла есть логика приложения.
b) Второй снимок экрана на этот пост показывает строку, начинающуюся с
ApplicationUnterTest calculatorWindow = ApplicationUnderTest.Launch(...);
что концептуально в значительной степени то, что я ищу, за исключением того, что снова этот пример использует абсолютный путь к исполняемому файлу.
c) A Поиск Google для "Программного запуска WPF" тоже не помог.
В итоге я использовал ApplicationUnderTest.Launch(...) (MSDN), который автоматически создается при записи автоматического теста с помощью Microsoft Test Manager.
MyProject.App myApp = new MyProject.App();
myApp.InitializeComponent();
myApp.Run();
Я делаю что-то подобное в VS2008 и вручную создаю тесты с использованием UI Spy, чтобы помочь мне идентифицировать элементы управления и некоторые вспомогательные методы, которые не показаны, чтобы запускать нажатия кнопок и проверять значения на экране. Я использую объект Process для запуска приложения, которое я тестирую в методе TestInitialize, и в методе TestCleanup я закрываю процесс. У меня есть несколько способов гарантировать, что процесс полностью закрыт в CleanUp. Что касается проблемы абсолютного пути, я просто программно просматриваю текущий путь и добавляю исполняемый файл приложения. Поскольку я не знаю, сколько времени требуется для запуска приложения, я помещаю AutomationId в свое главное окно и устанавливаю его в "UserApplicationWindow" и ожидаю, что это будет видно, конечно, у вас может быть что-то еще, что вы могли бы ожидать, Наконец, я использую MyTestClass как базовый класс и расширяю класс для разных тестов.
[TestClass]
public class MyTestClass
{
private Process _userAppProcess;
private AutomationElement _userApplicationElement ;
/// <summary>
/// Gets the current directory where the executables are located.
/// </summary>
/// <returns>The current directory of the executables.</returns>
private static String GetCurrentDirectory()
{
return Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().GetName().CodeBase).AbsolutePath).Replace("%20", " ");
}
[TestInitialize]
public void SetUp()
{
Thread appThread = new Thread(delegate()
{
_userAppProcess = new Process();
_userAppProcess.StartInfo.FileName =GetCurrentDirectory() + "\\UserApplication.exe";
_userAppProcess.StartInfo.WorkingDirectory = DirectoryUtils.GetCurrentDirectory();
_userAppProcess.StartInfo.UseShellExecute = false;
_userAppProcess.Start();
});
appThread.SetApartmentState(ApartmentState.STA);
appThread.Start();
WaitForApplication();
}
private void WaitForApplication()
{
AutomationElement aeDesktop = AutomationElement.RootElement;
if (aeDesktop == null)
{
throw new Exception("Unable to get Desktop");
}
_userApplicationElement = null;
do
{
_userApplicationElement = aeDesktop.FindFirst(TreeScope.Children,
new PropertyCondition(AutomationElement.AutomationIdProperty, "UserApplicationWindow"));
Thread.Sleep(200);
} while ( (_userApplicationElement == null || _userApplicationElement.Current.IsOffscreen) );
}
[TestCleanup]
public void CleanUp()
{
try
{
// Tell the application main window to close.
WindowPattern window = _userApplicationElement.GetCurrentPattern(WindowPattern.Pattern) as WindowPattern ;
window.Close();
if (!_userAppProcess.WaitForExit(3000))
{
// We waited 3 seconds for the User Application to close on its own.
// Send a close request again through the process class.
_userAppProcess.CloseMainWindow();
}
// All done trying to close the window, terminate the process
_userAppProcess.Close();
_userAppProcess = null;
}
catch (Exception ex)
{
// I know this is bad, but catching the world is better than letting it fail.
}
}
}
Вот то, что я только что взломал, что немного удалось выполнить в блочном тестировании калиберн микро:
[TestFixture]
public class when_running_bootstrapper
{
[Test]
public void it_should_request_its_view_model()
{
TestFactory.PerformRun(b =>
CollectionAssert.Contains(b.Requested, typeof(SampleViewModel).FullName));
}
[Test]
public void it_should_request_a_window_manager_on_dotnet()
{
TestFactory.PerformRun(b =>
CollectionAssert.Contains(b.Requested, typeof(IWindowManager).FullName));
}
[Test]
public void it_should_release_the_window_manager_once()
{
TestFactory.PerformRun(b =>
Assert.That(b.ReleasesFor<IWindowManager>(), Is.EqualTo(1)));
}
[Test]
public void it_should_release_the_root_view_model_once()
{
TestFactory.PerformRun(b =>
Assert.That(b.ReleasesFor<SampleViewModel>(), Is.EqualTo(1)));
}
}
static class TestFactory
{
public static void PerformRun(Action<TestBootStrapper> testLogic)
{
var stackTrace = new StackTrace();
var name = stackTrace.GetFrames().First(x => x.GetMethod().Name.StartsWith("it_should")).GetMethod().Name;
var tmpDomain = AppDomain.CreateDomain(name,
AppDomain.CurrentDomain.Evidence,
AppDomain.CurrentDomain.BaseDirectory,
AppDomain.CurrentDomain.RelativeSearchPath,
AppDomain.CurrentDomain.ShadowCopyFiles);
var proxy = (Wrapper)tmpDomain.CreateInstanceAndUnwrap(typeof (TestFactory).Assembly.FullName, typeof (Wrapper).FullName);
try
{
testLogic(proxy.Bootstrapper);
}
finally
{
AppDomain.Unload(tmpDomain);
}
}
}
[Serializable]
public class Wrapper
: MarshalByRefObject
{
TestBootStrapper _bootstrapper;
public Wrapper()
{
var t = new Thread(() =>
{
var app = new Application();
_bootstrapper = new TestBootStrapper(app);
app.Run();
});
t.SetApartmentState(ApartmentState.STA);
t.Start();
t.Join();
}
public TestBootStrapper Bootstrapper
{
get { return _bootstrapper; }
}
}
[Serializable]
public class TestBootStrapper
: Bootstrapper<SampleViewModel>
{
[NonSerialized]
readonly Application _application;
[NonSerialized]
readonly Dictionary<Type, object> _defaults = new Dictionary<Type, object>
{
{ typeof(IWindowManager), new WindowManager() }
};
readonly Dictionary<string, uint> _releases = new Dictionary<string, uint>();
readonly List<string> _requested = new List<string>();
public TestBootStrapper(Application application)
{
_application = application;
}
protected override object GetInstance(Type service, string key)
{
_requested.Add(service.FullName);
if (_defaults.ContainsKey(service))
return _defaults[service];
return new SampleViewModel();
}
protected override void ReleaseInstance(object instance)
{
var type = instance.GetType();
var t = (type.GetInterfaces().FirstOrDefault() ?? type).FullName;
if (!_releases.ContainsKey(t))
_releases[t] = 1;
else
_releases[t] = _releases[t] + 1;
}
protected override IEnumerable<object> GetAllInstances(Type service)
{
throw new NotSupportedException("Not in this test");
}
protected override void BuildUp(object instance)
{
throw new NotSupportedException("Not in this test");
}
protected override void Configure()
{
base.Configure();
}
protected override void OnExit(object sender, EventArgs e)
{
base.OnExit(sender, e);
}
protected override void OnStartup(object sender, System.Windows.StartupEventArgs e)
{
base.OnStartup(sender, e);
_application.Shutdown(0);
}
protected override IEnumerable<System.Reflection.Assembly> SelectAssemblies()
{
return new[] { typeof(TestBootStrapper).Assembly };
}
public IEnumerable<string> Requested
{
get { return _requested; }
}
public uint ReleasesFor<T>()
{
if (_releases.ContainsKey(typeof(T).FullName))
return _releases[typeof (T).FullName];
return 0u;
}
}
[Serializable]
public class SampleViewModel
{
}
Это может быть не совсем то, что вы хотите, но у меня была аналогичная проблема с моими приложениями WPF и их кодированными пользовательскими интерфейсами. В моем случае я использую сборку TFS (через шаблон Lab), и ее развертывание выводит результат нашей сборки; MSI и устанавливает это на целевом компьютере, тесты затем выполняются против установленного программного обеспечения.
Теперь, поскольку мы хотим протестировать программное обеспечение , установленное, мы добавили методы инициализации тестов, которые запускают тестируемый графический интерфейс, вызывая API MSI, чтобы получить папку установки для GUID продукта/компонента в нашем установщик.
Здесь извлечение кода, не забудьте заменить ваш продукт и компоненты GUIDS из вашего установщика)
/// <summary>
/// Starts the GUI.
/// </summary>
public void StartGui()
{
Console.WriteLine("Starting GUI process...");
try
{
var path = this.DetectInstalledCopy();
var workingDir = path;
var exePath = Path.Combine(path, "gui.exe");
//// or ApplicationUnderTest.Launch() ???
Console.Write("Starting new GUI process... ");
this.guiProcess = Process.Start(new ProcessStartInfo
{
WorkingDirectory = workingDir,
FileName = exePath,
LoadUserProfile = true,
UseShellExecute = false
});
Console.WriteLine("started GUI process (id:{0})", this.guiProcess.Id);
}
catch (Win32Exception e)
{
this.guiProcess = null;
Assert.Fail("Unable to start GUI process; exception {0}", e);
}
}
/// <summary>
/// Detects the installed copy.
/// </summary>
/// <returns>The folder in which the MSI installed the GUI feature of the cortex 7 product.</returns>
private string DetectInstalledCopy()
{
Console.WriteLine("Looking for install directory of CORTEX 7 GUI app");
int buffLen = 1024;
var buff = new StringBuilder(buffLen);
var ret = NativeMethods.MsiGetComponentPath(
"{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}", // YOUR product GUID (see WiX installer)
"{YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY}", // The GUI Installer component GUID
buff,
ref buffLen);
if (ret == NativeMethods.InstallstateLocal)
{
var productInstallRoot = buff.ToString();
Console.WriteLine("Found installation directory for GUI.exe feature at {0}", productInstallRoot);
return productInstallRoot;
}
Assert.Fail("GUI product has not been installed on this PC, or not for this user if it was installed as a per-user product");
return string.Empty;
}
/// <summary>
/// Stops the GUI process. Initially by asking nicely, then chopping its head off if it takes too long to leave.
/// </summary>
public void StopGui()
{
if (this.guiProcess != null)
{
Console.Write("Closing GUI process (id:[{0}])... ", this.guiProcess.Id);
if (!this.guiProcess.HasExited)
{
this.guiProcess.CloseMainWindow();
if (!this.guiProcess.WaitForExit(30.SecondsAsMilliseconds()))
{
Assert.Fail("Killing GUI process, it failed to close within 30 seconds of being asked to close");
this.guiProcess.Kill();
}
else
{
Console.WriteLine("GUI process closed gracefully");
}
}
this.guiProcess.Close(); // dispose of resources, were done with the object.
this.guiProcess = null;
}
}
И вот код оболочки API:
/// <summary>
/// Get the component path.
/// </summary>
/// <param name="product">The product GUI as string with {}.</param>
/// <param name="component">The component GUI as string with {}.</param>
/// <param name="pathBuf">The path buffer.</param>
/// <param name="buff">The buffer to receive the path (use a <see cref="StringBuilder"/>).</param>
/// <returns>A obscure Win32 API error code.</returns>
[DllImport("MSI.DLL", CharSet = CharSet.Unicode)]
internal static extern uint MsiGetComponentPath(
string product,
string component,
StringBuilder pathBuf,
ref int buff);