Headless automated tests are all the rage. This article will discuss a strategy for keeping your tests simple and maintainable by adopting a well known design pattern to abstract away from the Type Provider framework.
The Type Provider framework for Dynamics 365 Finance Operations (D365FO) provides a way for automated integration tests to interface with forms, without the need for web browser automation products like Selenium. Automated tests that require web browser automation can be slow to execute, fragile and expensive to maintain. This is something to avoid if you want your tests to recoup the upfront investment.
In this article we are going to discuss a strategy for encapsulating the Type Provider framework using the Page Object Model to:
- Keep our tests as simple, readable and maintainable as possible.
- Protect our automated test investment from
- future changes to the D365FO test frameworks (the Type Providers replaced Form Adaptors in July 2017)
- refactoring of existing functionality
- implementation updates from 3rd party ISVs
- Allow test engineers to define tests and stub the page model whilst feature engineers develop the functionality and fill in the stubs.
This article isn’t going to explain how to configure or build your models for use with Type Providers. For that, I would recommend heading over to Chris Lowe’s most excellent LexisNexis blogs:
The Page Object Model
The diagram below shows how the classes in this project are structured:
The PageContext wraps the ClientContext and is the starting point for navigation through the application. It also sets up the persona so that role-based authorisation is applied during the test.
The Pages do stuff and tell you things (or do things and tell you stuff). Each Page wraps one or more FormAdaptorTypeProviders.
In the case of a form that comprises other quick forms or form parts, it might make sense to maintain a single page for all of these type providers depending on how much reuse would be possible in maintaining a page for each.
Each page extends a BasePage which contains common functionality (e.g. determining if the underlying form is read-only etc..)
The Tests drive the pages and decide between right and wrong (actual vs expected). Tests extend a base test class which is somewhere else to hold reusable functionality across all tests (setting inheritable test attributes for example.)
Our tests tell the next generation of engineers how the application functions. Each test should makes its intention clear and should be easy to understand and maintain to prevent it falling into disrepair.
The following example shows how a test is arranged into it’s three common parts – Arrange, Act and Assert.
We begin by creating a page context which accepts an optional persona parameter to set the security context for the test (PersonaB in this example is the collections manager.) We need to use the using keyword here as the class implements IDisposable and performs its own clean-up to dispose of unmanaged objects.
Once we have the page context, we create our fixtures using our fixture helpers and then ask the page context to return a reference to the “customer list” page. Notice that the test doesn’t need to know about the underlying AOT implementation of the customer list.
We filter the customer list to our well-known customer and then ask for a reference to the “customer transactions” page before we proceed to create a new case.
We could alternatively skip more complex navigation by passing the customer directly to the “customer transactions” page constructor like this:
Finally, our assertion decides whether the action had the correct affect on the application.
The TestFixtureHelpers are responsible for setting up dependent data to ensure there is a well-known and fixed environment for the tests to run. This in turn helps to ensure that test results are repeatable.
In the example above we follow the default path as we don’t care too much about the specifics of the customer fixture:
Alternatively, we could have called getDefaultCustomerBuffer directly from the test and then applied a variation to the customer before we sent it back to newCustomerFromBuffer for creation.
The TestDataHelper supplies the reference data (e.g. Tax codes, Category codes) and other unique and randomly generated values to be used by the fixture helper.
This avoids hard-coding “magic strings” within tests and makes the test suite portable so that it can execute against different target deployments with differing customer configuration.
The methods on the pages either return a reference to the page itself, or a reference to a different page. This simplifies the navigation experience and enables the “fluent” API style.
An exception was thrown while initializing part “Microsoft.VisualStudio.Web.ProjectSystem.UnitTest.KUnitTestContainerDiscoverer”.
I don’t think any amount of recompiling will fix this one – Visual Studio needs a restart.
Other things to try
There are a number of other attributes that are supported by the test framework (some of these are inheritable and others are not).
Finally, whilst automated headless tests are important, don’t forget about or neglect your unit and integration testing strategy!