In this article you will learn about a C# web test automation framework called Atata.
Introduction
Atata Framework - C#/.NET web test automation full-featured framework based on Selenium WebDriver. It uses a fluent page object pattern; has a built-in logging system; contains a unique triggers functionality; has a set of ready-to-use components. One of the key ideas of the framework is to provide a simple and intuitive syntax for defining and using page objects. A page object implementation requires as less code as possible. You can describe a page object class without any methods and only have a set of properties marked with attributes representing page components.
The framework basically consists of the following concepts:
- AtataContext - configures test sessions.
- Components - controls and page objects.
- Attributes of control search - element locators, like
[FindById]
, [FindByName]
, [FindByXPath]
, etc. - Trigger attributes - a functionality that is automatically executed in response to certain events on a particular component.
- Behavior attributes - change the way how particular actions are executed.
- Verification functionality -
.Should.*
assertion, .ExpectTo.*
expectation as warning, .WaitTo.*
waiting.
Features
- WebDriver. Based on Selenium WebDriver and preserves all its features.
- Page Object Model. Provides a unique fluent page object pattern, which is easy to implement and maintain.
- Components. Contains a rich set of ready-to-use components for inputs, tables, lists, etc.
- Integration. Works on any .NET test engine (e.g. NUnit, xUnit, SpecFlow) as well as on CI systems like Jenkins, GitHub Actions, or TeamCity.
- Triggers. A bunch of triggers to bind with different events to extend component behavior.
- Verification. A set of fluent assertion methods and triggers for a component and data verification.
- Configurable. Defines the default component search strategies as well as additional settings. Atata.Configuration.Json provides flexible JSON configurations.
- Reporting/Logging. Built-in customizable logging; screenshots and snapshots capturing functionality.
- Extensible. Atata.HtmlValidation adds HTML page validation. Atata.Bootstrap and Atata.KendoUI provide extra components.
Background
An idea of the Atata Framework is to create complex, extensible and customizable web test automation framework for any kind of websites using Selenium WebDriver and C#/.NET.
References
A list of links related to the framework:
Usage
I would like to show the usage of the framework using the demo website. It is a simple website that contains the following: "Sign In" page, "Users" page, "User Details" page and "User Create/Edit" window.
The test project will use the NuGet packages: Atata, Atata.Bootstrap, Atata.WebDriverSetup, and NUnit.
I use NUnit but it is not required, you can use any .NET testing framework like MSTest or xUnit. But for me, NUnit fits the best.
Let's try to implement an auto-test for the following test case:
- Sign in on https://demo.atata.io/signin page.
- Click "New" button on the user list page.
- Create a new user.
- Verify that the new user is present on the user list page.
- Navigate to the user's details.
- Verify the user's details.
Any page can be represented with the page object. I will try to explain the Atata's stuff step by step. To start, we need to implement a page object class for "Sign In" page.
Sign In page
using Atata;
namespace SampleApp.UITests;
using _ = SignInPage;
[Url("signin")]
[VerifyTitle]
[VerifyH1]
public class SignInPage : Page<_>
{
public TextInput<_> Email { get; private set; }
public PasswordInput<_> Password { get; private set; }
public Button<UsersPage, _> SignIn { get; private set; }
}
SignInPage.cs
In Atata, you operate with controls, rather than IWebElement
's. A page object consists of controls. Any control like TextInput
wraps IWebElement
and has its own set of methods and properties for element interaction. Find out more about the components in the documentation.
Please note the 5th line of the above code:
using _ = SignInPage;
It is made to simplify the use of class type for the declaration of the controls, as every control has to know its owner page object (specify single or last generic argument). It's just syntactic sugar and, of course, you can declare the controls this way:
public TextInput<SignInPage> Email { get; private set; }
SignIn
button, as you can see, is defined with 2 generic arguments: the first one is the type of the page object to navigate to, after the button is clicked; the other one is the owner type. For buttons and links that don't perform any navigation, just pass single generic argument, the owner page object.
It is possible to mark properties with attributes to specify a finding approach (e.g. FindById
, FindByName
). In current case, it is not needed, as the default search for inputs is FindByLabel
and for buttons is FindByContentOrValue
, and it suits our needs. Find out more about the control search in the documentation.
There is also [Url]
attribute which specifies relative (can be absolute) URL of this page. It can be used when you navigate to the page object.
[VerifyTitle]
and [VerifyH1]
are the triggers that, in the current case, are executed upon the page object initialization (after the navigation to the page). If the string
value is not passed to these attributes, they use class name without the "Page
" ending in title case, as "Sign In". It can be totally configured. Find out more about the triggers in the documentation.
Users page
The "Users" page contains a table of users with CRUD actions.
using Atata;
namespace SampleApp.UITests;
using _ = UsersPage;
[VerifyTitle]
[VerifyH1]
public class UsersPage : Page<_>
{
public Button<UserEditWindow, _> New { get; private set; }
public Table<UserTableRow, _> Users { get; private set; }
public class UserTableRow : TableRow<_>
{
public Text<_> FirstName { get; private set; }
public Text<_> LastName { get; private set; }
public Text<_> Email { get; private set; }
public Content<Office, _> Office { get; private set; }
public Link<UserDetailsPage, _> View { get; private set; }
public Button<UserEditWindow, _> Edit { get; private set; }
[CloseConfirmBox]
public Button<_> Delete { get; private set; }
}
}
UsersPage.cs
In the UsersPage
class, you can see the usage of Table<TRow, TOwner>
and TableRow<TOwner>
controls. In UserTableRow
class, the properties of type Text
and Content
by default are being searched by the column header (FindByColumnHeader
attribute). It can also be configured. For example, the FirstName
control will contain "John
" value for the first row. The usage of the table will be shown in the test method below.
Delete
button is marked with CloseConfirmBox
trigger which accepts the confirmation window shown after the click on the button.
User Create/Edit window
It is quite a simple Bootstrap popup window with two tabs and regular input controls.
using Atata;
using Atata.Bootstrap;
namespace SampleApp.UITests;
using _ = UserEditWindow;
public class UserEditWindow : BSModal<_>
{
[FindById]
public GeneralTabPane General { get; private set; }
[FindById]
public AdditionalTabPane Additional { get; private set; }
[Term("Save", "Create")]
public Button<UsersPage, _> Save { get; private set; }
public class GeneralTabPane : BSTabPane<_>
{
public TextInput<_> FirstName { get; private set; }
public TextInput<_> LastName { get; private set; }
[RandomizeStringSettings("{0}@mail.com")]
public TextInput<_> Email { get; private set; }
public Select<Office?, _> Office { get; private set; }
[FindByName]
public RadioButtonList<Gender?, _> Gender { get; private set; }
}
public class AdditionalTabPane : BSTabPane<_>
{
public DateInput<_> Birthday { get; private set; }
public TextArea<_> Notes { get; private set; }
}
}
UserEditWindow.cs
The UserEditWindow
is inherited from BSModal<TOwner>
page object class. It is a component of Atata.Bootstrap package.
Save
button is marked with Term("Save", "Create")
attribute that specifies the values for the control search. It means that the button should be found by "Save
" or "Cancel
" text content.
Gender
and Office
controls use the following enum
s:
namespace SampleApp.UITests;
public enum Gender
{
Male,
Female
}
Gender.cs
namespace SampleApp.UITests;
public enum Office
{
Berlin,
London,
NewYork,
Paris,
Rome,
Tokio,
Washington
}
Office.cs
User Details page
using System;
using Atata;
namespace SampleApp.UITests;
using _ = UserDetailsPage;
public class UserDetailsPage : Page<_>
{
[FindFirst]
public H1<_> Header { get; private set; }
[FindByDescriptionTerm]
public Text<_> Email { get; private set; }
[FindByDescriptionTerm]
public Content<Office, _> Office { get; private set; }
[FindByDescriptionTerm]
public Content<Gender, _> Gender { get; private set; }
[FindByDescriptionTerm]
public Content<DateTime?, _> Birthday { get; private set; }
[FindByDescriptionTerm]
public Text<_> Notes { get; private set; }
}
UserDetailsPage.cs
Atata setup
The best place to configure Atata is a global set-up method that is executed once before all test.
using Atata;
using NUnit.Framework;
namespace SampleApp.UITests;
[SetUpFixture]
public class SetUpFixture
{
[OneTimeSetUp]
public void GlobalSetUp()
{
AtataContext.GlobalConfiguration
.UseChrome()
.WithArguments("start-maximized")
.UseBaseUrl("https://demo.atata.io/")
.UseCulture("en-US")
.UseAllNUnitFeatures()
.Attributes.Global.Add(
new VerifyTitleSettingsAttribute { Format = "{0} - Atata Sample App" });
AtataContext.GlobalConfiguration.AutoSetUpDriverToUse();
}
}
SetUpFixture.cs
Here we globally configure Atata with the following:
- Tell to use Chrome browser.
- Set the base site URL.
- Set the culture, which is used by the controls like
DateInput
. - Tell to use all Atata features for integration with NUnit, like logging to NUnit
TestContext
, taking screenshot and snapshots on test failure, etc. - Set format of the page title, as all the pages on the testing website have a page title like "Sign In - Atata Sample App".
AutoSetUpDriverToUse
sets up driver for the browser that we want to use, which is chromedriver.exe
in this case. Atata.WebDriverSetup package is responsible for that.
For more configuration options, please check the Getting Started / Configuration page in the docs.
Base UITestFixture class
Now let's configure NUnit to build AtataContext (start browser and do extra configuration) on test setup event and clean-up Atata (close browser, etc.) on test tear down event. We can create base test fixture class that will do that. Also we can put reusable Login
method there.
using Atata;
using NUnit.Framework;
namespace SampleApp.UITests;
[TestFixture]
public class UITestFixture
{
[SetUp]
public void SetUp() =>
AtataContext.Configure().Build();
[TearDown]
public void TearDown() =>
AtataContext.Current?.Dispose();
protected static UsersPage Login() =>
Go.To<SignInPage>()
.Email.Set("admin@mail.com")
.Password.Set("abc123")
.SignIn.ClickAndGo();
}
UITestFixture.cs
Here you can see a primitive usage of AtataContext
Build
and Dispose
methods.
As you can see in Login
method, navigation starts from Go static class. To keep the example simple, I use hard-coded credentials here, that can easily be moved to Atata.json config, for example.
User test
And finally, the test that will use all of the created above classes and enums.
using Atata;
using NUnit.Framework;
namespace SampleApp.UITests;
public class UserTests : UITestFixture
{
[Test]
public void Create() =>
Login()
.New.ClickAndGo()
.ModalTitle.Should.Equal("New User")
.General.FirstName.SetRandom(out string firstName)
.General.LastName.SetRandom(out string lastName)
.General.Email.SetRandom(out string email)
.General.Office.SetRandom(out Office office)
.General.Gender.SetRandom(out Gender gender)
.Save.ClickAndGo()
.Users.Rows[x => x.Email == email].View.ClickAndGo()
.AggregateAssert(page => page
.Header.Should.Equal($"{firstName} {lastName}")
.Email.Should.Equal(email)
.Office.Should.Equal(office)
.Gender.Should.Equal(gender)
.Birthday.Should.Not.BePresent()
.Notes.Should.Not.BePresent());
}
UserTests.cs
I prefer to use fluent page object pattern in the Atata tests. If you don't like such approach, use without fluent pattern.
You can use random or predefined values in the test, as you like.
The control verification starts with Should
property. There is a set of extension methods for different controls like: Equal
, Exist
, StartWith
, BeGreater
, BeEnabled
, HaveChecked
, etc.
That's all. Build project, run test and verify how it works.
Logging
Atata can generate log to different sources. As we configured AtataContext
with UseAllNUnitFeatures
, Atata will write logs to NUnit context. You can also use targets of NLog or log4net to write logs to files.
Here is a part of the test log:
2024-04-23 09:03:16.015 DEBUG Starting test: SampleApp.UITests.UserTests.Create
2024-04-23 09:03:16.025 TRACE > Initialize AtataContext
2024-04-23 09:03:16.025 TRACE - Set: BaseUrl=https://demo.atata.io/
2024-04-23 09:03:16.026 TRACE - Set: ElementFindTimeout=5s; ElementFindRetryInterval=0.5s
2024-04-23 09:03:16.026 TRACE - Set: WaitingTimeout=5s; WaitingRetryInterval=0.5s
2024-04-23 09:03:16.026 TRACE - Set: VerificationTimeout=5s; VerificationRetryInterval=0.5s
2024-04-23 09:03:16.026 TRACE - Set: Culture=en-US
2024-04-23 09:03:16.027 TRACE - Set: Artifacts=D:\dev\atata-samples\SampleApp.UITests\SampleApp.UITests\bin\Debug\net6.0\artifacts\20240423T090315\UserTests\Create
2024-04-23 09:03:16.027 TRACE - > Initialize Driver
2024-04-23 09:03:16.031 TRACE - - Created ChromeDriverService { Port=56159, ExecutablePath=D:\dev\atata-samples\SampleApp.UITests\SampleApp.UITests\bin\Debug\net6.0\drivers\chrome\124.0.6367.60\chromedriver.exe }
2024-04-23 09:03:16.658 TRACE - - Created ChromeDriver { Alias=chrome, SessionId=066ae9f79b9c545bc7f5d948b24f8027 }
2024-04-23 09:03:16.661 TRACE - < Initialize Driver (0.631s)
2024-04-23 09:03:16.662 TRACE < Initialize AtataContext (0.636s)
2024-04-23 09:03:16.696 INFO > Go to "Sign In" page by URL https://demo.atata.io/signin
2024-04-23 09:03:16.845 INFO < Go to "Sign In" page by URL https://demo.atata.io/signin (0.148s)
2024-04-23 09:03:16.854 TRACE > Execute trigger VerifyTitleAttribute { Case=Title, Match=Equals, Timeout=5, RetryInterval=0.5 } on Init against "Sign In" page
2024-04-23 09:03:16.858 INFO - > Assert: title should equal "Sign In - Atata Sample App"
2024-04-23 09:03:17.372 INFO - < Assert: title should equal "Sign In - Atata Sample App" (0.513s)
2024-04-23 09:03:17.373 TRACE < Execute trigger VerifyTitleAttribute { Case=Title, Match=Equals, Timeout=5, RetryInterval=0.5 } on Init against "Sign In" page (0.518s)
2024-04-23 09:03:17.373 TRACE > Execute trigger VerifyH1Attribute { Index=-1, Case=Title, Match=Equals, Timeout=5, RetryInterval=0.5 } on Init against "Sign In" page
2024-04-23 09:03:17.379 INFO - > Assert: "Sign In" <h1> heading should be present
2024-04-23 09:03:17.390 TRACE - - > Find visible element by XPath ".//h1[normalize-space(.) = 'Sign In']" in ChromeDriver
2024-04-23 09:03:17.418 TRACE - - < Find visible element by XPath ".//h1[normalize-space(.) = 'Sign In']" in ChromeDriver (0.027s) >> Element { Id=f.53D2E25CB3A6D1C5A23D224BA6F201A5.d.6D6B02A18C9BC5DF2012E42B9862A3F5.e.7 }
2024-04-23 09:03:17.419 INFO - < Assert: "Sign In" <h1> heading should be present (0.040s)
2024-04-23 09:03:17.419 TRACE < Execute trigger VerifyH1Attribute { Index=-1, Case=Title, Match=Equals, Timeout=5, RetryInterval=0.5 } on Init against "Sign In" page (0.046s)
2024-04-23 09:03:17.423 INFO > Set "admin@mail.com" to "Email" text input
2024-04-23 09:03:17.425 TRACE - > Execute behavior SetsValueUsingClearAndTypeBehaviorsAttribute against "Email" text input
2024-04-23 09:03:17.426 TRACE - - > Execute behavior ClearsValueUsingClearMethodAttribute against "Email" text input
2024-04-23 09:03:17.429 TRACE - - - > Find element by XPath "(.//*[@id = //label[normalize-space(.) = 'Email']/@for]/descendant-or-self::input[@type='text' or not(@type)] | .//label[normalize-space(.) = 'Email']/descendant-or-self::input[@type='text' or not(@type)])" in ChromeDriver
2024-04-23 09:03:17.441 TRACE - - - < Find element by XPath "(.//*[@id = //label[normalize-space(.) = 'Email']/@for]/descendant-or-self::input[@type='text' or not(@type)] | .//label[normalize-space(.) = 'Email']/descendant-or-self::input[@type='text' or not(@type)])" in ChromeDriver (0.012s) >> Element { Id=f.53D2E25CB3A6D1C5A23D224BA6F201A5.d.6D6B02A18C9BC5DF2012E42B9862A3F5.e.6 }
2024-04-23 09:03:17.443 TRACE - - - > Clear element { Id=f.53D2E25CB3A6D1C5A23D224BA6F201A5.d.6D6B02A18C9BC5DF2012E42B9862A3F5.e.6 }
2024-04-23 09:03:17.476 TRACE - - - < Clear element { Id=f.53D2E25CB3A6D1C5A23D224BA6F201A5.d.6D6B02A18C9BC5DF2012E42B9862A3F5.e.6 } (0.033s)
2024-04-23 09:03:17.477 TRACE - - < Execute behavior ClearsValueUsingClearMethodAttribute against "Email" text input (0.050s)
2024-04-23 09:03:17.477 TRACE - - > Execute behavior TypesTextUsingSendKeysAttribute against "Email" text input
2024-04-23 09:03:17.479 TRACE - - - > Send keys "admin@mail.com" to element { Id=f.53D2E25CB3A6D1C5A23D224BA6F201A5.d.6D6B02A18C9BC5DF2012E42B9862A3F5.e.6 }
2024-04-23 09:03:17.548 TRACE - - - < Send keys "admin@mail.com" to element { Id=f.53D2E25CB3A6D1C5A23D224BA6F201A5.d.6D6B02A18C9BC5DF2012E42B9862A3F5.e.6 } (0.069s)
2024-04-23 09:03:17.549 TRACE - - < Execute behavior TypesTextUsingSendKeysAttribute against "Email" text input (0.071s)
2024-04-23 09:03:17.549 TRACE - < Execute behavior SetsValueUsingClearAndTypeBehaviorsAttribute against "Email" text input (0.123s)
2024-04-23 09:03:17.549 INFO < Set "admin@mail.com" to "Email" text input (0.125s)
...
2024-04-23 09:03:19.691 DEBUG Finished test
Total time: 3.736s
Initialization: 0.707s | 18.9 %
Test body: 2.883s | 77.2 %
Deinitialization: 0.144s | 3.9 %
Download
Check out the sources of Atata on Atata GitHub page. Check the docs to find out more about Atata.
Get the sources of the demo test project on GitHub: Atata Sample App Tests. The demo project contains:
- 20+ different UI auto-tests.
- Atata configuration and settings set-up.
- Data input and verification.
- Validation verification functionality.
- Usage of triggers.
- Logging, screenshots and snapshots.
- Page HTML validation.
Contact
You can ask a question on Stack Overflow using atata tag or choose another contact option. Any feedback, issues and feature requests are welcome.
Atata tutorials
History
- 1st December, 2016: Initial version posted
- 2nd December, 2016: Sample sources added
- 4th April, 2017: Updated article content; added links to other Atata articles; updated sample sources
- 26th September, 2017: Updated sample sources to use Atata v0.14.0; updated article content
- 7th November, 2017: Updated sample sources to use Atata v0.15.0; updated article content
- 5th June, 2018: Updated sample sources to use Atata v0.17.0; updated article content
- 25th October, 2018: Updated sample sources to use Atata v1.0.0; updated "Features" and "Usage" sections content
- 15th May, 2019: Updated sample sources to use Atata v1.1.0; updated links to documentation that was moved to a new domain
- 2nd March, 2021: Updated sample sources to use Atata v1.10.0; updated the article content
- 23rd April, 2024: Updated sample sources to use Atata v3.0.0; updated the article content