Tessian’s mission is to secure the human layer by empowering people to do their best work, without security getting in their way.
In part one, we went over the decisions that led the CSI team to start automating its UI application with a focus on the process drivers and journey. Today we’re going to start going over the technical challenges, solutions, and learnings along the way. It would be good if you had a bit of understanding of how to use WinAppDriver for UI testing. As there are a multitude of beginner tutorials, this post will be more in depth. All code samples are available as a complete solution here.
As I’m sure many others have done before, we started by adapting winappdriver samples into our own code base. After we had about 20 tests up and running, it became clear that taking some time to better architect common operations would help in fixing tests as we targeted more versions of Outlook, Windows, etc. Simple things like how long to wait for a window to open, or how long to wait to receive an email can be impacted by the test environment, and it quickly becomes tedious to change these in 20 different places whenever we have a new understanding/solution on the best way to do these operations.
A good place to start when writing UI tests is just getting the tests to open the application. There are plenty of samples online that show you how to do this, but there are a few things that the samples leave each of us to solve on our own that I think would be helpful to share with the larger Internet community.
And when code keeps repeating itself, it’s time to abstract this code into interfaces and classes. So, we have both: an interface and a base class:
Don’t worry, we’ll get into the bits. The main point of this class is it pertains to starting/stopping, or attaching/detaching to applications and that we’re storing enough information about the application under test to do those operations.
In the constructor, the name of the process is used to determine if we can attach to an already running process, whereas the path to the executable is used if we don’t find a running process and need to start a fresh instance. The process name can be found in the Task Manager’s Details tab.
I can’t tell you how many times I’ve clicked run on my tests only to have them all fail because I forgot to start the WinAppDriver process beforehand. WinAppDriver is the application that drives the mouse and keyboard clicks, along with getting element IDs, names, classes, etc of the application under test. Using the same solution WinAppDriver’s examples show for starting any application, you can start the WinAppDriver process as well.
Using IManageSession and BaseSession<T> above, we get:
The default constructor just calls BaseSession<WinAppDriverProcess> with the name of the process and the path to the executable.
So you can see that StartSession here is implemented to be thread safe. This ensures that only one instance can be created in a test session, and that it’s created safely in an environment where you run your tests across multiple threads. It then queries the base class about whether the application you’re starting is already running or not. If it is running, we attach to it. If it’s not, we start a new instance and attach to that. Here are those methods:
These are both named Unsafe to show that they’re not thread safe, and it’s up to the calling method to ensure thread safety. In this case, that’s StartSession().
And for completeness, StopSession does something very similar except it queries BaseSession<T> to see if we own the process (i.e. it was started as a fresh instance and not attached to), or not. If we own it, then we’re responsible for shutting it down, but if we only attach to it, then leave it open.
Desktop sessions can be useful ways to test elements from the root of the Windows Desktop. This would include things like the Start Menu, sys-tray, or file explorer windows. We use it for our sys-tray icon functionality, but regardless of what you need it for, WinAppDriver’s FAQ provides the details, but I’ve made it work here using IManageSession and BaseSession<T>:
It’s a lot simpler since we’d never be required to start the root session. It’s still helpful to have it inherit from BaseSession<T> as that will provide us some base functionality like storing the instance in a Singleton and knowing how long to wait for windows to appear when switching to/from them.
This includes all the Office applications. WinAppDriver’s FAQ has some help on this, but I think I’ve improved it a bit with the do/while loop to wait for the main window to appear. The other methods look similar to the above, so I’ve collapsed them for brevity.
So how do we put all this together and make a test run? Glad you asked!
I make fairly heavy use of NUnit’s class and method level attributes to ensure things get set up correctly depending on the assembly, namespace, or class a test is run in. Mainly, I have a OneTimeSetup for the whole assembly that starts WinAppDriver and attaches to the Desktop root session.
Then I separate my tests into namespaces that correspond to the application under test – in this case, it’s Outlook.
I then use a OneTimeSetup in that namespace that starts Outlook (or attaches to it).
Finally, I use SetUp and TearDown attributes on the test classes to ensure I start and end each test from the main application window.
All that allows you to write (the somewhat verbose) test:
For this post we went into the details on how to organize and code your Sessions for UI testing. We showed you how to design them so you can reuse code between different application sessions. We also enabled them to either start the application or connect to an already running application instance (and how the Session object can determine which to do itself). Finally, we put it all together and created a basic test that drives Outlook’s UI to compose a new Email message and send it.
Stay tuned for the next post where we’ll delve into how to handle all the dialog windows your UI needs – to interact with and abstract that away – so you can write a full test with something that looks like this: