Rocketcress is a collection of libraries that help you to easily write Integration- and UI-Tests in C# and MSTest.
- ๐ Library List
- ๐ Explanation of terms
- ๐ฑโ๐ Getting Started
- ๐ Explanation of Functions
- ๐จ Create UIMaps
- ๐ Dos and Don'ts
- ๐ฎ FAQ
Name | NuGet.org | Description |
---|---|---|
Rocketcress.Core | Contains core functionality of for all Rocketcress libraries and tests. | |
Rocketcress.Core.Windows | Contains core functionality specifically for Windows of for all Rocketcress libraries and tests. | |
Rocketcress.Composition | Extends Rocketcress.Core with System.ComponentModel.Composition specific functionalities. | |
Rocketcress.Mail | Extends Rocketcress.Core with e-mail functionality. | |
Rocketcress.Selenium | You can reference this library if you want to write tests using the Selenium framework. | |
Rocketcress.SourceGenerators | Contains C# 9.0 Source Generators that are useful to test projects. | |
Rocketcress.UIAutomation | You can reference this library if you want to write tests using the UIAutomation framework. |
The following packages are also available as Slim variants which do not require a reference to MSTest v2:
Name | NuGet.org |
---|---|
Rocketcress.Core.Slim | |
Rocketcress.Core.Windows.Slim | |
Rocketcress.Composition.Slim | |
Rocketcress.Mail.Slim | |
Rocketcress.Selenium.Slim | |
Rocketcress.UIAutomation.Slim |
Term | Description |
---|---|
UIMap | A UIMap is a class that wraps the interaction with a specific control or view of an application. It contains sub-controls, methods and properties which are used to easily interact with a specific control or view without knowing how to find it in the UI. |
Location Key | An object that specifies how a control/element is searched in the UI. The location key is specified by an object of type By which is contained in both Selenium and UIAutomation libraries. |
Creating a project that uses Rocketcress is fairly simple and can be done in the following few steps:
- Create a new project using the Project Template
MSTest Test Project (.NET Core)
- If you want to target .NET Framework, use the same template and change the
TargetFramework
of that created csproj tonet48
- If you want to target .NET Framework, use the same template and change the
- Add the Rocketcress NuGet packages that you need
- It is recommended to always add
Rocketcress.SourceGenerators
- Depending on what test you are writing use the following NuGet packages:
- IntegrationTest:
Rocketcress.Core
- Selenium UITest:
Rocketcress.Selenium
- If you need to run tests in Firefox or IE, add the NuGet package(s) for the specific driver(s)
- Chrome and Edge are supported out of the box
- UIAutomation UITest:
Rocketcress.UIAutomation
- IntegrationTest:
- It is recommended to always add
- Add the following property to the csproj file:
<CopySettings>true</CopySettings>
- Create a
settings.json
file somewhere in the project and set the "Build Action" in properties to "C# analyzer additional file" so that the source generator can automatically generate a C# class (SettingValues
) that you can use to load settings.
Some useful things are already done in base classes that can be used in test classes (e.g. some logging). The following base classes exist and shopuld be used depending on the test:
Class Name | Description |
---|---|
Rocketcress.Selenium.SeleniumTestBase | Base class for all Selenium tests. |
Rocketcress.UIAutomation.UIAutomationTestBase | Base class for all UIAutomation tests. |
Rocketcress.Core.Base.TestBase<TSettings, TContext> | Base class for all other tests. |
The general procedure to add a new test class is the following:
- Create a new file with the template
Class
to the test project - Add the
TestClass
code attribute to the class - Inherit from one of the base classes above
- Add a test method by using the
testm
snippet or adding theTestMethod
code attribute to a public method - Use the
CreateAndInitializeContext()
to create a Rocketcress test context- For Selenium tests this will automatically create and start a new Web Driver which is then available in the Context via the
Driver
property. - For UIAutomation use the
Launch
orAttach
static methods on theApplication
class to start or attach to an application. This will automatically set the propertyApplication
on the test context.
- For Selenium tests this will automatically create and start a new Web Driver which is then available in the Context via the
At the start of each test method, a Rocketcress test context should be created. Mind that the Rocketcress test context is disposable, so it is recommended to use the using var
keywords.
These are two examples for tests in test classes for Selenium and UIAutomation:
- Selenium:
// [...] [TestClass] public class LoginTests : SeleniumTestBase { [TestMethod] public void Selenium_Login_Success() { using var ctx = CreateAndInitializeContext(); var mainView = MainView.Login(ctx.Driver); mainView.Logoff(true); } }
- UIAutomation
// [...] [TestClass] public class LoginViewTests : UIAutomationTestBase { [TestMethod] public void UIA_Login_Success() { using var ctx = CreateAndInitializeContext(); var app = Application.Launch(ctx, /* FilePath */); var mainView = MainView.Login(app); mainView.Logoff(); } }
The setting classes in the libraries already contain a lot of properties. These can be read about in the code itself. But there are also properties called OtherSettings
, KeyClasses
and SettingTypes
that are special.
The OtherSettings
property contains a list of custom settings. These settings can be of any type and the type can be specified by prepending a tag to the property name (e.g. "[str] MyString": "Hello world!"
).
The tags are defined by the SettingTypes
property. This property is an array of type definitions that contain two properties:
TagName
: The name of the tag that can be used by custom settingsTypeName
: The type name that is used when generating the settings class (use the full qualified name of the type)
The KeyClasses
property is used to structure the settings. A prefix with a name can be specified to group custom settings which names start with the given prefix (e.g. "TL_": "Translation"
will group all custom settings which names are starting with "TL_").
{
/* [...] */
"OtherSettings": {
"[int] MyId": 710,
"[str] MyString": "SERVICEDESK",
"[str] TL_MyTranslation": "Reference No."
},
"KeyClasses": {
"TL_": "Translation"
},
"SettingsTypes": [
{
"TagName": "str",
"TypeName": "string"
},
{
"TagName": "int",
"TypeName": "int"
}
]
}
This settings.json will generate the following C# file:
// [...]
#region Setting Key Classes
[AddKeysClass(typeof(TranslationKeys))]
public static class SettingKeys
{
public static readonly string MyId = "[int] MyId";
public static readonly string MyString = "[str] MyString";
}
public static class TranslationKeys
{
public static readonly string MyTranslation = "[str] TL_MyTranslation";
}
#endregion
#region Setting Classes
public static class SettingValues
{
// [...]
public static int MyId
=> _properties.GetProperty(() => SettingsLoader.Settings.Get<int>(SettingKeys.MyId));
public static string MyString
=> _properties.GetProperty(() => SettingsLoader.Settings.Get<string>(SettingKeys.MyString));
}
public static class TranslationValues
{
// [...]
public static string MyTranslation
=> _properties.GetProperty(() => SettingsLoader.Settings.Get<string>(TranslationKeys.MyTranslation));
}
#endregion
// [...]
It is possible to add more settings files for different environment. For once the "settings_debug.json" is used when the test is executed with the DEBUG
configuration. The test base classes detect it automatically when initializing a test. If you want to manually set this value you can do so by setting the static property TestHelper.IsDebugConfiguration
. It is also possible to create a settings file for a specific environment by naming the settings file "settings_[MachineName].json" (replacing [MachineName]
by the Name of the Computer; e.g. "settings_LAP-MASC1.json").
The settings files can be placed anywhere in the project (even in subfolders).
Please remember that you need a settings.json
with the Build Action "C# analyzer additional file" and a reference to the Rocketcress.SourceGenerators
NuGet package so that the settings class is generated.
Also the property <CopySettings>true</CopySettings>
needs to be added to the project, so that the settings files are all copied to the output directory while building the project.
By default the source generator will detect the settings class that is used to deserialize the json files. If you want to specify your own class, you can set the SettingsType
property on the AdditionalFiles
Tag for the settings.json file in your csproj.
It is not needed to copy the whole "settings.json" file for the environment specific settings files (or the debug file). It is possible to just create an empty JSON file and specify only the properties that should be overwritten. Also the tag of custom settings can be omitted.
For example this file will use all settings from the settings file above but overwrites the timeout and AdminUserId
:
{
"Timeout": "00:05:00",
"OtherSettings": {
"AdminUserId": 4711,
}
}
As you can see, also the KeyClasses
and SettingsTypes
can be omitted. These properties are only used from the main settings.json.
The interaction with the UI is mostly done by the control classes provided in the libraries (Selenium: WebElement
, UIAutomation: UITestControl
). For these classes there are different derived classes for specific controls (e.g. WpfTextBox for a TextBox control in WPF) which contains more specialized actions.
All actions are done by calling methods or setting/getting property values. For example, the Click()
method will click on the control and setting the Text
property of the WpfTextBox
will set the text of the TextBox.
There are a lot of actions, which would be to much to explain here. All actions can be seen in the code.
In Selenium there is another class that can be used to create UIMaps - the View
class. Classes that derive from View
should represent a browser page (basically the body
element of a web page). So it cannot be interacted with directly, but contains all elements that are on that page. A View
needs to specify a location key which is used to identify if the view is loaded completely. This should be any element that loads last on the page.
Also an important note is, that with the help of the SetFocus
method, the driver focus can be switched to a specific view. In Selenium this is normally done by calling the driver with a specific window handle. This is done automatically by the View
base class.
The Wait
class is one of the most impotant classes in the libraries. It handles wait actions which are exceptionally important for UI Tests.
The Wait
class has one method Until
which is the start of a fluent API. In this you need to provide a Func<T>
which is the wait condition. The Wait
class will wait until that function returns a value that does not equal to the default
value of the given type T
. That means if T
is bool
the method will wait until the function returns true
.
There are methods to specific the timeout (default is the one from Wait.Options.DefaultTimeout
which is set in the test context initialize to the provided settings) and the wait between checks (TimeGap
) (default: 100ms). But one of the most important methods is the ThrowOnFailure
method which tells the wait to execute Assert.Fail
if the wait runs into a timeout. If so, the message
is used as the fail message.
After configuring the wait action, the actual wait needs to be started using the Start()
method. That method will return a result object with a couple of information about the wait action (e.g. the time it waited). One property of that result is also the Value
which returns the last return value of the wait condition.
For example this call will wait until the element myElement
does have the text success
in it. It checks every second and throws an AssertException
with the message "Text is wrong." if the timeout of 5 minutes is exceeded:
var myElement = new WebElement(/* [...] */);
Wait.Until(() => myElement.Text == "success")
.WithTimeout(TimeSpan.FromMinutes(5))
.WithTimeGap(1000)
.ThrowOnFailure("Text is wrong.")
.Start();
The TestHelper
class contains a lot of useful methods/properties. Like the following:
IsDebugConfiguration
: Returns a value indicating wether the test is executed in debug configurationRetryAction
: Retries a specific action until it returns an expected value; other overloads will retry an action until it will not throw an exceptionTry
: A shortened version oftry { } catch { }
RunPowerShell
: Runs a PowerShell script and returns the Exit Code, Standard Output and Standard ErrorLoopUntilAllFinished
: Runs a list of functions in parallel until all have returned a value; will rerun functions that already completed so the result will be the most up to date values of these functionsRunWithTimeout
: Runs an action and stops it if the specified timeout is exceeded.
Before a UIMap for a control/element is created, the base class which to be use needs to be determined.
- Selemium: Always use the
View
class for web pages andWebElement
class for elements - UIAutomation: Determine the properties "FrameworkId" and "ControlType" from the control for which to create the UIMap (use Inspect for this) - that will lead to the name of the base class
- Example: A control with FrameworkId = "Wpf" and ControlType = "Button" should use the class
WpfButton
- Example: A control with FrameworkId = "Wpf" and ControlType = "Button" should use the class
It is recommended to use the Rocketcress.SourceGenerators
NuGet package. This package includes a source generator that will already generate a lot of boilerplate code.
It generates:
- All constructors from the base class, calling the respective constructors on the base class
- Initialize methods (override for the
InitializeControls
/Initialize
methods) - Partial methods for each initialization step
- Initialization code for UIMap controls
Using the source generator you just need to create a class drived from WebElement
, View
or UITestControl
or a class that already derives from one of these classes or derivatives. After that add the GenerateUIMapParts
attribute to the class and add the partial
keyword.
Example:
// [...]
[GenerateUIMapParts]
public partial class MyControl : WebElement
{
// [...]
}
When using the source generators adding controls to a UIMap is quite simple. You just need to add a new Property with the UIMapControl
attribute. By default the source generator will generate a location key for you depending on the property name. You can control this behavior using the IdStyle
property on the UIMapControl
attribute. You can also provide a custom location key by setting the property using the generated InitUsing<T>
method.
Example:
// [...]
[GenerateUIMapParts]
public partial class MyControl : WebElement
{
[UIMapControl]
public WebElement MyControl { get; private set; } // location key will be: By.Id("MyControl")
[UIMapControl]
public WebElement MyOtherControl { get; private set; } = InitUsing<WebElement>(() => By.XPath("./input[@type='button']"));
}
Let's say you have a view and want to add a control to the views UIMap. Start by overriding the method Initialize
(UIAutomation) or InitializeControls
(Selenium) from the base class.
After that create a field and a property for the control under the Initialize
/InitializeControls
method and initialize the property in that method.
Example (Selenium):
// [...]
public class MyView : View
{
public MyView() : base() { }
public MyView(WebDriver driver) : base(driver) { }
protected override void InitializeControls()
{
base.InitializeControls();
MyControl = new WebElement(ByMyControl);
}
private static readonly By ByMyControl = By.Id("my-fancy-control");
public WebElement MyControl { get; private set; }
}
In this case there is a control that has other controls as child controls. In this case the procedure is mostly the same as for views, but this
should be passed into the child control, to only search for sub controls.
Important: If a XPath is specified for child controls, the XPath needs to start with a dot (e.g. ./div
), so the search only happens in the context of the parent control.
Example (Selenium):
// [...]
public class MyControl : WebElement
{
public MyControl(By locationKey) : base(locationKey) { }
public MyControl(IWebElement element) : base(element) { }
public MyControl(By locationKey, ISearchContext searchContext) : base(locationKey, searchContext) { }
protected MyControl() : base() { }
protected override void InitializeControls()
{
base.InitializeControls();
MyChildControl = new WebElement(ByMyControl, this);
}
private static readonly By ByMyChildControl = By.XPath("./input[@type='button']");
public WebElement MyChildControl { get; private set; }
}
As described in the Explanation of terms, a location key is basically an object that describes where a control/element can be found in the UI.
There are major differences of defining such a location key in Selenium and UIAutomation. But in both libraries a class called By
is used.
For the Selenium library the native Selenium class OpenQA.Selenium.By
is used. With this class it is possible to define a location key with the following search criteria:
ClassName
: Find an element by its CSS classId
: Find an element by its IDXPath
: Find an element by a XPathCssSelector
: Find an element using a CSS selectorLinkText
/PartialLinkText
: Find an element by its link textName
: Find an element by its nameTagName
: Find an element by its tag name
The full API documentation for that class can be found here.
To find the correct properties or XPaths use the browsers buildin developer tools.
The native UIAutomation framework uses a very strange and complicated search mechanism. Searching for controls can also be done manually, so a better search engine was implemented in the Rocketcress.UIAutomation library. The search engine uses a location key, like Selenium, in which a variety of search criteria can be added. The class to use here is the Rocketcress.UIAutomation.By
and the following criteria can be specified:
AutomationId
: Find an element by its "AutomationId" propertyControlType
: Find an element by its "ControlType" propertyClassName
: Find an element by its "ClassName" propertyName
: Find an element by its "Name" propertyFramework
: Find an element by its "Framework" propertyHelpText
: Find an element by its "HelpText" propertyProcessId
: FInd an element by its "ProcessId" propertyItemStatus
: Find an element by its "ItemStatus" propertyAccessKey
: Find an element by its "AccessKey" propertyAcceleratorKey
: Find an element by its "AcceleratorKey" property
ChildOf
: Find an element that is child of an element found by another location keyHasChild
: Find an element that has child elements that match a specified location keyRelativeTo
: Find an element that is relative (so neighbor) to an element found by another location key
Property
: Find an element with a specific property valuePatternAvailable
/PatternNotAvailable
: Find an element that has or has not a specific UIAutomation patternCondition
: Find an element by a custom condition
Scope
/Descendants
/Skip
/Take
/MaxDepth
: Set the scope of the location key
Unlike Selenium, multiple search criteria can be added to one By
object by executing the And[...]
methods like this: By.ControlType(ControlType.Button).AndAutomationId("MyButton")
With the methods FindFirst
and FindAll
a control can be searched. Though it is recommended to use the class UITestControl
or any of its derived classes to search for controls.
To find the correct properties use the Inspect tool.
CPath is a syntax for describing a path to a UIAutomation control in the control tree. It is a smaller and modified version of the XPath syntax. CPath can be easily combined with the normal By-Method-Syntax.
|[MaxDepth] [Condition] [Child]
โโดโ โโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโ
//{3}Window[@name='ClassicDesk' and ./Edit[@id='ServerName']]/Button[@id='submit']
โโ โโโโฌโโ โโฌโโโโโโโโโโโโโโโโโ โโฌโ โโฌโโโโโโโโโโโโโโโโโโโโโโ
| | |PropertyCondition | |HashChildCondition
|Path |[ControlType] |Operator
- Direct child (
/
|./
) - MaxDepth will be ignored - Descendant (
//
|.//
) - MaxDepth determines the maximal descendant depth to search for (Default is 5) - Parent (
..
|./..
) - MaxDepth will be ignored - Ascendant (
...
|./...
) - MaxDepth determines the maximal ascendant depth to search for (Default is 5) - Relative (
/<
|/>
|/<>
|/.<
|/.>
|/.<>
) - MaxDepth will be ignored
Searches for a control that is on the same level..
include the element in the search<
include preceding elements in the search>
include subsequent elements in the search
- Composite Path (
<path1>|<path2>|[...]
) - Combines multiple paths like an or-statement
E.g.:..|/
matches either the parent or the direct child; - Combine Paths without conditions with
*
as control type
E.g.:./..*..*//button
: <Parent> -> <Parent> -> <Descendant buttons> - Max search depth (
{<MaxDepth>}
) - Determines the maximum search depth; can be used only after Descendant or Ascendant
All control types from the class System.Windows.Automation.ControlType can be used. Control types are case insensitive and -
characters are ignored. There are though custom aliases for the current control types:
Type Name | Aliases | Description |
---|---|---|
Text | text , label |
A TextBlock/Label control |
Edit | edit , textbox |
A TextBox/PasswordBox control |
Tab | tab , tablist |
A Tab control |
- PropertyCondition (
@property[~=]<value>
)
Matches an element by one of its properties. All properties defined in System.Windows.Automation.AutomationElement are supported (properties are case insensitive and-
characters are ignored).
The following string matching options are available:=
or==
: Case sensitive equality=~
: Case insensitive equality~=
: Case sensitive contains~
or~~
: Case insensitive contains
- HasChildCondition (
<cpath>
)
Matches an element that has the sub element defined by the given cpath - Condition Operators:
and
/or
You can also use parentheses to group conditions like[...] and ([...] or [...])
.
and
will bind stronger thanor
, so[...] or [...] and [...]
is the same as[...] or ([...] and [...])
.
- Always think about wait actions. Is a wait needed, on what to wait and how long should the timeout be.
This is the most common cause of unstable tests if not done correctly! - Use the settings as often as possible, instead of hardcoding information into the test.
- When creating UIMaps always add the same constructors as their base class.
- When initializing a control in a UIMap of another control, pass in
this
as the "searchContext" (Selenium) or "parent" (UIAutomation) parameters to the constructor of the child control.
- Try to avoid the usage of
Thread.Sleep
orTask.Delay
; use theWait
class instead. - Never use custom settings in a UIMap project.
- Try to avoid using translated strings in a location key.
- Try to avoid searching for elements in a Test directly; add a UIMap to one of the UIMap libraries instead.
- (Selenium) Never search for elements directly on the driver; create an instance of the
WebElement
class instead. - (Selenium) Never use Windows specific actions (like sending keys with Windows.Forms), only interact with controls and/or the driver.
If you are an employee of the Serviceware SE, contact the PANDA Team from PD Processes which already has a lot of experience with these libraries; otherwise create an Issue on Github.
The Driver is available in the following locations:
- Inside a test class: Use the
CurrentDriver
property - On a View of WebElement: Use the
Driver
property - Anywhere else: Use the
Driver
property ofSeleniumTestContext.CurrentContext
There are multiple ways of telling the Selenium library what browser to use. The library checks the locations in the following order (first wins):
- When the test is executed via Azure DevOps Pipelines using a Test Plan in which the test is associated to a configuration (checked by getting property "TestConfiguration" of the MSTest TestContext) with the following criteria (first wins):
- Contains "chrome" (case insensitive): Google Chrome is used
- Contains "firefox" (case insensitive): Mozilla Firefox is used
- Contains "ie", "internet explorer" or "internetexplorer" (case insensitive): Microsoft Internet Explorer is used
- Contains "edge" (case insensitive): Microsoft Edge (Chromium) is used
- The test has a "Rocketcress.Selenium.BrowserDataSourceAttribute" code attribute associated.
- The fallback location is always the property "DefaultBrowser" in the settings.json. If the property if not provided, Chrome is used.
Selenium tests can be executed in multiple browsers with the following options:
- Add the test to a Test Plan in Azure DevOps Server and set multiple configurations on the test (containing the name of the browser to test).
This option only works in Azure DevOps Pipelines. - Using the
Rocketcress.Selenium.BrowserDataSourceAttribute
code attribute on the test method.
This option works locally and in Azure DevOps Pipelines. When using Azure DevOps Pipelines, the first option is recommended, because you can then differentiate test results between browsers.
A new web driver can be created by executing the CreateAndSwitchToNewDriver
method from the SeleniumTestContext
. The current driver can be switched by using the SwitchCurrentDriver
method or by executing the SetFocus
of an existing instance of a View
.
The IEDriver for Selenium is not the best, so it is really slow. If you run into issues with expiring timeouts in Internet Explorer, try adjusting the timeout. You can also increase the timeout in the tests by either settings the Timeout
property on the Settings
or passing in a different timeout value in the .WithTimeout()
method on a wait action.
You can also check with the GetBrowser()
method of the driver if the IE is currently used and just increase the timeout then.
Generally it is recommended to not use IE at all.
None right now. Questions and Answers will be added here if any occur.