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 fluent page object pattern; has unique logging system; contains triggers functionality; has a set of ready to use components. Supports .NET Framework 4.0+ and .NET Core/Standard 2.0+.
The Framework basically consists of the following concepts:
- Components (controls and page objects)
- Attributes of the control search
- Settings attributes
- Triggers
- Verification attributes and methods
Features
- WebDriver. Based on Selenium WebDriver and preserves all its features.
- Page Object Model. Provides unique fluent page object pattern that 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, Azure DevOps 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 the 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 and screenshot capturing functionality.
- Extensible. Atata.Bootstrap and Atata.KendoUI packages have a set of ready to use components. Framework supports any kind of extending.
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:
To install it from NuGet Package Manager Console, run Install-Package Atata
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 Edit window.
The test project will use the NuGet packages: Atata, Atata.Bootstrap, Atata.WebDriverSetup, Selenium.WebDriver, NUnit and NUnit3TestAdapter.
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 the 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. The page object consists of the controls. Any control like TextInput
wraps the IWebElement
and has its own set of methods and properties for the interaction with it. 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 a 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 the buttons and links that don't perform any navigation, just pass single generic argument, the owner page object.
It is possible to mark the properties with the attributes to specify the finding approach (e.g. FindById
, FindByName
). In the 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 this 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 the table of the 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
" 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 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 / Set Up page in the docs.
Base UITestFixture Class
Now let's configure NUnit to build AtataContext (start browser and do extra configuration) on test set up event and cleanup 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?.CleanUp();
}
protected UsersPage Login()
{
return 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 CleanUp
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 App.config or Atata.json, 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()
{
Office office = Office.NewYork;
Gender gender = Gender.Male;
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.Set(office)
.General.Gender.Set(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.Exist()
.Notes.Should.Not.Exist());
}
}
}
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. Check the docs to find out more about Atata.
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:
2021-03-02 12:50:42.4649 INFO Starting test: Create
2021-03-02 12:50:42.4917 TRACE > Set up AtataContext
2021-03-02 12:50:42.4937 TRACE - Set: BaseUrl=https://demo.atata.io/
2021-03-02 12:50:42.4977 TRACE - Set: ElementFindTimeout=5s; ElementFindRetryInterval=0.5s
2021-03-02 12:50:42.4980 TRACE - Set: WaitingTimeout=5s; WaitingRetryInterval=0.5s
2021-03-02 12:50:42.4982 TRACE - Set: VerificationTimeout=5s; VerificationRetryInterval=0.5s
2021-03-02 12:50:42.4986 TRACE - Set: Culture=en-US
2021-03-02 12:50:42.5067 TRACE - Set: DriverService=ChromeDriverService on port 64593
2021-03-02 12:50:43.4007 TRACE - Set: Driver=ChromeDriver (alias=chrome)
2021-03-02 12:50:43.4029 TRACE < Set up AtataContext (0.910s)
2021-03-02 12:50:43.4917 INFO Go to "Sign In" page
2021-03-02 12:50:43.5439 INFO Go to URL "https://demo.atata.io/signin"
2021-03-02 12:50:44.9231 TRACE > Execute trigger VerifyTitleAttribute { Case=Title, Match=Equals, Format="{0} - Atata Sample App" } on Init against "Sign In" page
2021-03-02 12:50:44.9370 INFO - > Assert: title should equal "Sign In - Atata Sample App"
2021-03-02 12:50:45.9745 INFO - < Assert: title should equal "Sign In - Atata Sample App" (1.037s)
2021-03-02 12:50:45.9752 TRACE < Execute trigger VerifyTitleAttribute { Case=Title, Match=Equals, Format="{0} - Atata Sample App" } on Init against "Sign In" page (1.052s)
2021-03-02 12:50:45.9773 TRACE > Execute trigger VerifyH1Attribute { Index=-1, Case=Title, Match=Equals } on Init against "Sign In" page
2021-03-02 12:50:45.9880 INFO - > Assert: "Sign In" <h1> heading should exist
2021-03-02 12:50:46.0225 TRACE - - > Find visible element by XPath ".//h1[normalize-space(.) = 'Sign In']" in ChromeDriver
2021-03-02 12:50:46.0754 TRACE - - < Find visible element by XPath ".//h1[normalize-space(.) = 'Sign In']" in ChromeDriver (0.051s) >> Element { Id=a694ecd2-0874-4ba3-b61f-e4e3eb821f0a }
2021-03-02 12:50:46.0756 INFO - < Assert: "Sign In" <h1> heading should exist (0.087s)
2021-03-02 12:50:46.0758 TRACE < Execute trigger VerifyH1Attribute { Index=-1, Case=Title, Match=Equals } on Init against "Sign In" page (0.098s)
2021-03-02 12:50:46.0842 INFO > Set "admin@mail.com" to "Email" text input
2021-03-02 12:50:46.0889 TRACE - > Execute behavior ValueSetUsingClearAndSendKeysAttribute against "Email" text input
2021-03-02 12:50:46.0968 TRACE - - > Find visible element by XPath ".//label[normalize-space(.) = 'Email']" in ChromeDriver
2021-03-02 12:50:46.1321 TRACE - - < Find visible element by XPath ".//label[normalize-space(.) = 'Email']" in ChromeDriver (0.035s) >> Element { Id=bc2450f6-27bb-497b-80aa-ff428b95d440 }
2021-03-02 12:50:46.1501 TRACE - - > Find visible element by XPath ".//*[normalize-space(@id) = 'email']/descendant-or-self::input[@type='text' or not(@type)]" in ChromeDriver
2021-03-02 12:50:46.1803 TRACE - - < Find visible element by XPath ".//*[normalize-space(@id) = 'email']/descendant-or-self::input[@type='text' or not(@type)]" in ChromeDriver (0.030s) >> Element { Id=3baa8d49-2ac4-4f69-900e-e6be31daaa14 }
2021-03-02 12:50:46.1815 TRACE - - > Clear element { Id=3baa8d49-2ac4-4f69-900e-e6be31daaa14 }
2021-03-02 12:50:46.2280 TRACE - - < Clear element { Id=3baa8d49-2ac4-4f69-900e-e6be31daaa14 } (0.046s)
2021-03-02 12:50:46.2291 TRACE - - > Send keys "admin@mail.com" to element { Id=3baa8d49-2ac4-4f69-900e-e6be31daaa14 }
2021-03-02 12:50:46.3052 TRACE - - < Send keys "admin@mail.com" to element { Id=3baa8d49-2ac4-4f69-900e-e6be31daaa14 } (0.076s)
2021-03-02 12:50:46.3055 TRACE - < Execute behavior ValueSetUsingClearAndSendKeysAttribute against "Email" text input (0.216s)
2021-03-02 12:50:46.3057 INFO < Set "admin@mail.com" to "Email" text input (0.221s)
2021-03-02 12:50:46.3059 INFO > Set "abc123" to "Password" password input
2021-03-02 12:50:46.3061 TRACE - > Execute behavior ValueSetUsingClearAndSendKeysAttribute against "Password" password input
2021-03-02 12:50:46.3066 TRACE - - > Find visible element by XPath ".//label[normalize-space(.) = 'Password']" in ChromeDriver
2021-03-02 12:50:46.3378 TRACE - - < Find visible element by XPath ".//label[normalize-space(.) = 'Password']" in ChromeDriver (0.031s) >> Element { Id=461e982a-c6c4-414f-ac9f-c7c7bd16baeb }
2021-03-02 12:50:46.3476 TRACE - - > Find visible element by XPath ".//*[normalize-space(@id) = 'password']/descendant-or-self::input[@type='password']" in ChromeDriver
2021-03-02 12:50:46.3756 TRACE - - < Find visible element by XPath ".//*[normalize-space(@id) = 'password']/descendant-or-self::input[@type='password']" in ChromeDriver (0.027s) >> Element { Id=a92d523a-a4c9-4ab6-9455-477cef964b0d }
2021-03-02 12:50:46.3759 TRACE - - > Clear element { Id=a92d523a-a4c9-4ab6-9455-477cef964b0d }
2021-03-02 12:50:46.4203 TRACE - - < Clear element { Id=a92d523a-a4c9-4ab6-9455-477cef964b0d } (0.044s)
2021-03-02 12:50:46.4205 TRACE - - > Send keys "abc123" to element { Id=a92d523a-a4c9-4ab6-9455-477cef964b0d }
2021-03-02 12:50:46.4810 TRACE - - < Send keys "abc123" to element { Id=a92d523a-a4c9-4ab6-9455-477cef964b0d } (0.060s)
2021-03-02 12:50:46.4813 TRACE - < Execute behavior ValueSetUsingClearAndSendKeysAttribute against "Password" password input (0.175s)
2021-03-02 12:50:46.4815 INFO < Set "abc123" to "Password" password input (0.175s)
2021-03-02 12:50:46.4837 INFO > Click "Sign In" button
2021-03-02 12:50:46.4862 TRACE - > Execute behavior ClickUsingClickMethodAttribute against "Sign In" button
2021-03-02 12:50:46.4892 TRACE - - > Find visible element by XPath ".//*[self::input[@type='button' or @type='submit' or @type='reset'] or self::button][normalize-space(.) = 'Sign In' or normalize-space(@value) = 'Sign In']" in ChromeDriver
2021-03-02 12:50:46.5177 TRACE - - < Find visible element by XPath ".//*[self::input[@type='button' or @type='submit' or @type='reset'] or self::button][normalize-space(.) = 'Sign In' or normalize-space(@value) = 'Sign In']" in ChromeDriver (0.028s) >> Element { Id=0994387f-fd82-49f6-ab43-8b90c3aee738 }
2021-03-02 12:50:46.5186 TRACE - - > Click element { Id=0994387f-fd82-49f6-ab43-8b90c3aee738 }
2021-03-02 12:50:46.6419 TRACE - - < Click element { Id=0994387f-fd82-49f6-ab43-8b90c3aee738 } (0.123s)
2021-03-02 12:50:46.6421 TRACE - < Execute behavior ClickUsingClickMethodAttribute against "Sign In" button (0.155s)
2021-03-02 12:50:46.6423 INFO < Click "Sign In" button (0.158s)
2021-03-02 12:50:46.6544 INFO Go to "Users" page
...
Download
Check out the sources of Atata Framework on Atata GitHub page.
Get the sources of the demo test project on GitHub: Atata Sample App Tests. The demo project contains:
- 20+ different auto-tests
- Validation verification functionality
- Logging functionality using NLog
- Screenshot capturing
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