Atata - C# Web 测试自动化框架






4.89/5 (50投票s)
基于 Selenium WebDriver 的 Atata C#/.NET Web UI 测试自动化全功能框架简介
引言
Atata Framework - 基于 Selenium WebDriver 的 C#/.NET Web 测试自动化全功能框架。它使用流畅的页面对象模式;内置日志系统;包含独特的触发器功能;拥有一套现成的组件。该框架的关键理念之一是为定义和使用页面对象提供简单直观的语法。页面对象实现的实现代码应尽可能少。您可以描述一个页面对象类,而无需任何方法,只需拥有一组用属性标记的代表页面组件的属性。
该框架主要包含以下概念
- AtataContext - 配置测试会话。
- Components - 控件和页面对象。
- 控件搜索属性 - 元素定位符,如
[FindById]
、[FindByName]
、[FindByXPath]
等。 - 触发器属性 - 在特定组件上响应特定事件而自动执行的功能。
- 行为属性 - 更改特定操作的执行方式。
- 验证功能 -
.Should.*
断言,.ExpectTo.*
预期(作为警告),.WaitTo.*
等待。
特点
- WebDriver。基于 Selenium WebDriver 并保留其所有功能。
- Page Object Model (页面对象模型)。提供独特的流畅页面对象模式,易于实现和维护。
- Components (组件)。包含丰富的现成组件,适用于输入框、表格、列表等。
- 集成。可在任何 .NET 测试引擎(例如 NUnit、xUnit、SpecFlow)以及 Jenkins、GitHub Actions 或 TeamCity 等 CI 系统上运行。
- Triggers (触发器)。一组触发器,可与不同事件绑定以扩展组件行为。
- Verification (验证)。一套用于组件和数据验证的流畅断言方法和触发器。
- 可配置。定义默认组件搜索策略以及其他设置。 Atata.Configuration.Json 提供灵活的 JSON 配置。
- Reporting/Logging (报告/日志)。内置可定制的日志记录;屏幕截图和快照捕获功能。
- 可扩展。 Atata.HtmlValidation 添加 HTML 页面验证。 Atata.Bootstrap 和 Atata.KendoUI 提供额外的组件。
背景
Atata Framework 的理念是使用 Selenium WebDriver 和 C#/.NET 为任何类型的网站创建复杂、可扩展且可定制的 Web 测试自动化框架。
参考文献
与该框架相关的链接列表
- 文档
- GitHub - Atata
- GitHub - Atata 示例应用测试
- Atata Templates Visual Studio 扩展
- NuGet - Atata
用法
我想通过 演示网站 来展示该框架的用法。这是一个简单的网站,包含以下内容:“登录”页面、“用户”页面、“用户详情”页面以及“用户创建/编辑”窗口。
测试项目将使用 NuGet 包:Atata、Atata.Bootstrap、Atata.WebDriverSetup 和 NUnit。
我使用 NUnit,但这并非必需,您可以使用任何 .NET 测试框架,如 MSTest 或 xUnit。但对我而言,NUnit 最为合适。
让我们尝试为一个测试用例实现一个自动测试
- 在 https://demo.atata.io/signin 页面登录。
- 在用户列表页面点击“新建”按钮。
- 创建一个新用户。
- 验证新用户是否出现在用户列表页面。
- 导航到用户详情。
- 验证用户详情。
任何页面都可以用页面对象来表示。我将尝试逐步解释 Atata 的相关内容。首先,我们需要为“登录”页面实现一个页面对象类。
登录页面
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; }
}
在 Atata 中,您操作的是控件,而不是 IWebElement
。页面对象由控件组成。任何控件,如 TextInput
,都封装了 IWebElement
,并拥有自己的元素交互方法和属性。有关 组件 的更多信息,请参阅文档。
请注意上面代码的第 5 行
using _ = SignInPage;
它是为了简化声明控件的类类型的用法,因为每个控件都必须知道它的所有者页面对象(指定单个或最后一个泛型参数)。这只是语法糖,当然,您也可以这样声明控件
public TextInput<SignInPage> Email { get; private set; }
如您所见,SignIn
按钮定义了 2 个泛型参数:第一个是在点击按钮后导航到的页面对象的类型;另一个是所有者类型。对于不执行任何导航的按钮和链接,只需传递单个泛型参数,即所有者页面对象。
可以使用属性标记属性来指定查找方法(例如 FindById
、FindByName
)。在这种情况下,这是不必要的,因为输入框的默认搜索是 FindByLabel
,按钮的默认搜索是 FindByContentOrValue
,这足以满足我们的需求。有关 控件搜索 的更多信息,请参阅文档。
还有一个 [Url]
属性,它指定该页面的相对(可以是绝对)URL。在导航到页面对象时可以使用它。
[VerifyTitle]
和 [VerifyH1]
是触发器,在本例中,它们在页面对象初始化时(导航到页面后)执行。如果未将 string
值传递给这些属性,它们将使用类名去掉“Page
”后缀(以大写字母开头的形式),例如“Sign In”。这完全可以配置。有关 触发器 的更多信息,请参阅文档。
用户页面
“用户”页面包含一个带有 CRUD 操作的用户表格。
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
类中,您可以看到 Table<TRow, TOwner>
和 TableRow<TOwner>
控件的使用。在 UserTableRow
类中,Text
和 Content
类型的属性默认通过列标题(FindByColumnHeader
属性)进行搜索。这也可以配置。例如,FirstName
控件将包含第一行的“John
”值。表格的使用将在下面的测试方法中展示。
Delete
按钮标记了 CloseConfirmBox
触发器,该触发器可以处理点击按钮后显示的确认窗口。
用户创建/编辑窗口
这是一个相当简单的 Bootstrap 弹出窗口,包含两个选项卡和常规的输入控件。
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
继承自 BSModal<TOwner>
页面对象类。这是 Atata.Bootstrap 包中的一个组件。
Save
按钮标记了 Term("Save", "Create")
属性,该属性指定了控件搜索的值。这意味着按钮应通过“Save
”或“Cancel
”文本内容进行查找。
Gender
和 Office
控件使用以下 enum
:
namespace SampleApp.UITests;
public enum Gender
{
Male,
Female
}
namespace SampleApp.UITests;
public enum Office
{
Berlin,
London,
NewYork,
Paris,
Rome,
Tokio,
Washington
}
用户详情页面
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; }
}
Atata 设置
配置 Atata 的最佳位置是全局设置方法,该方法在所有测试之前执行一次。
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();
}
}
在这里,我们对 Atata 进行全局配置,如下所示:
- 指定使用 Chrome 浏览器。
- 设置基础站点 URL。
- 设置区域设置,
DateInput
等控件将使用该区域设置。 - 指示使用所有 Atata 功能与 NUnit 集成,例如日志记录到 NUnit
TestContext
,在测试失败时截取屏幕截图和快照等。 - 设置页面标题的格式,因为测试网站上的所有页面的标题都类似于“Sign In - Atata Sample App”。
AutoSetUpDriverToUse
设置我们要使用的浏览器的驱动程序,在本例中是chromedriver.exe
。 Atata.WebDriverSetup 包负责此功能。
有关更多配置选项,请查看文档中的 入门/配置 页面。
基础 UITestFixture 类
现在,让我们配置 NUnit,以便在测试设置事件时构建 AtataContext(启动浏览器并执行额外配置),并在测试拆卸事件时清理 Atata(关闭浏览器等)。我们可以创建一个基础测试夹具类来完成此操作。我们还可以将可重用的 Login
方法放在里面。
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();
}
在这里,您可以看到 AtataContext
的 Build
和 Dispose
方法的基本用法。
如您在 Login
方法中看到的,导航从 Go
静态类开始。为保持示例简单,我在此处使用了硬编码的凭据,这些凭据可以轻松地移至 Atata.json 配置,例如。
用户测试
最后,是使用上面创建的所有类和枚举的测试。
using Atata;
using NUnit.Framework;
namespace SampleApp.UITests;
public class UserTests : UITestFixture
{
[Test]
public void Create() =>
Login() // Returns UsersPage.
.New.ClickAndGo() // Returns UserEditWindow.
.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() // Returns UsersPage.
.Users.Rows[x => x.Email == email].View.ClickAndGo() // Returns UserDetailsPage.
.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());
}
我更喜欢在 Atata 测试中使用流畅的页面对象模式。如果您不喜欢这种方法,请省略流畅模式。
您可以根据需要使用测试中的随机值或预定义值。
控件验证从 Should
属性开始。对于不同的控件,有一系列扩展方法,例如:Equal
、Exist
、StartWith
、BeGreater
、BeEnabled
、HaveChecked
等。
就是这些了。构建项目,运行测试,并验证其工作原理。
日志记录
Atata 可以将日志生成到不同的目标。由于我们使用 UseAllNUnitFeatures
配置了 AtataContext
,Atata 将日志写入 NUnit 上下文。您还可以使用 NLog 或 log4net 的目标将日志写入文件。
这是测试日志的一部分
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 %
下载
查看 Atata 在 Atata GitHub 页面 上的源代码。 查看文档 以了解有关 Atata 的更多信息。
在 GitHub 上获取演示测试项目的源代码:Atata Sample App Tests。该演示项目包含
- 20 多个不同的 UI 自动测试。
- Atata 配置和设置。
- 数据输入和验证。
- 验证消息验证功能。
- 触发器用法。
- 日志记录、屏幕截图和快照。
- 页面 HTML 验证。
联系方式
您可以使用 atata 标签 在 Stack Overflow 上提问,或者选择其他 联系方式。欢迎任何反馈、问题和功能请求。
Atata 教程
历史
- 2016 年 12 月 1 日:发布初始版本
- 2016 年 12 月 2 日:添加了示例源代码
- 2017 年 4 月 4 日:更新了文章内容;添加了其他 Atata 文章的链接;更新了示例源代码
- 2017 年 9 月 26 日:更新了示例源代码以使用 Atata v0.14.0;更新了文章内容
- 2017 年 11 月 7 日:更新了示例源代码以使用 Atata v0.15.0;更新了文章内容
- 2018 年 6 月 5 日:更新了示例源代码以使用 Atata v0.17.0;更新了文章内容
- 2018 年 10 月 25 日:更新了示例源代码以使用 Atata v1.0.0;更新了“功能”和“用法”部分的内
- 2019 年 5 月 15 日:更新了示例源代码以使用 Atata v1.1.0;更新了指向已迁移到新域的文档的链接
- 2021 年 3 月 2 日:更新了示例源代码以使用 Atata v1.10.0;更新了文章内容
- 2024 年 4 月 23 日:更新了示例源代码以使用 Atata v3.0.0;更新了文章内容