Ben Power
Ben Power

Reputation: 1885

Launch and call methods on WPF app from unit test project

I would like to launch a WPF app and call methods on the ViewModel to control the app for the purpose of integration testing. Something like:

    [Test]
    public void Test1()
        var application = new MyApp();
        application.InitializeComponent();
        application.Run();

(OK, this stops the test execution at this point, presumably passing control to the WPF app. Not sure how to deal with this. Launch in a separate thread or something?)

Then I'd like to be able to get and set values on the ViewModel, something like this:

        application.MyViewModel.SomeProperty = "A value!";

The goal here is to test the WPF app in an integrated sense without resorting to WinAppDriver, White, CodedUI or anything messy like that. Ideas?

Upvotes: 3

Views: 2381

Answers (2)

mm8
mm8

Reputation: 169400

It doesn't make much sense to create an App and call Run() on it in a unit or integration test. You will only be able to create one App per AppDomain.

What you should do is to structure your code in a way so you don't have to test anything else than the ViewModel class alone. This means that all your application logic should be implemented in the ViewModel, or in classes that the ViewModel uses one way or another.

This is a design pattern known as Model-View-ViewModel (MVVM) and it's the recommended design pattern to use when developing XAML-based UI applications. There is a reason for this - testing is one of them.

You'll find a lot of information about MVVM if you search for it. This should provide a good starting point.

Upvotes: 1

Peter Duniho
Peter Duniho

Reputation: 70701

You'll either need a separate thread to manipulate the view model, or you'll need to execute code in the dispatcher thread to do that. I prefer the latter, but either would work. The former would require you being careful about using the dispatcher to marshal some operations to the UI thread; view model property changes don't need that, because WPF will do that automatically for you, but other things like direct calls to UI object methods — e.g. Window.Close() — do.

Here's an example of what you might do using the dispatcher thread to do all the testing code:

[TestMethod]
public void TestWpfApp()
{
    Thread thread = new Thread(() =>
    {
        var application = new App();
        Application.ResourceAssembly = System.Reflection.Assembly.GetExecutingAssembly();
        application.InitializeComponent();
        application.Dispatcher.InvokeAsync(() =>
        {
            _TestApplication(application);
        }, System.Windows.Threading.DispatcherPriority.ApplicationIdle);
        application.Run();
    });

    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    thread.Join();
}

private static async void _TestApplication(Application application)
{
    Window window = application.MainWindow;
    ViewModel viewModel = (ViewModel)window.DataContext;

    await Task.Delay(TimeSpan.FromSeconds(5));
    viewModel.Text = "Hello World!";
    await Task.Delay(TimeSpan.FromSeconds(5));
    window.Close();
}

The basic structure is to set up a thread suitable for running the WPF UI (it has to be an STA thread, and you shouldn't be messing with the unit test's thread, so creating a new thread for this purpose is required), and then in that thread, do the usual WPF setup plus queue via InvokeAsync() to the dispatcher a call to the main testing method, to have it start executing once the WPF code has started running.

Naturally, this example assumes a ViewModel class with a Text property, and the main window's DataContext property set to an instance of this ViewModel. In my sample program, I just bound the Text property to a TextBlock.Text property. Obviously, you could do whatever you want with your view model.

Note that I had to explicitly set Application.ResourceAssembly. In Visual Studio Community 2017, which is what I'm using at the moment, the unit test framework runs the text in a context where Assembly.GetEntryAssembly() returns null, which breaks WPF's resource loading. Setting it explicitly fixes that (I'm using Assembly.GetExecutingAssembly(), because I put the unit test code in the same assembly with my sample WPF program…obviously, if you keep them in different assemblies, you'd have to find the right assembly some other way).

In my testing, using System.Windows.Threading.DispatcherPriority.ApplicationIdle in the call to Dispatch.InvokeAsync() wasn't strictly required. I found the MainWindow and DataContext properties initialized fine. But I prefer to explicitly wait for ApplicationIdle, just to make sure those have been fully initialized and that the WPF program itself is ready to start accepting whatever input you have in mind for your tests.

Upvotes: 7

Related Questions