原文连接:https://dzone.com/articles/top-15-ui-test-automation-best-practices-you-shoul-1
创建UI测试自动化框架的前15个最佳实践:
1. 不要只依赖于UI自动化测试
2. 考虑使用BDD框架。
3. 切记一定要使用测试设计模式和原则。
4. 千万不要使用Thread.sleep(),除非有特定的测试要求。
5. 不要试图在所有目标浏览器上运行所有测试用例。
6. 将自动化测试用例与测试框架分开。
7. 使自动化测试框架可移植。
8. 采用清晰明了的命名规则
9. 使用软断言检查Web页面上会出现的特定检查项。
10. 对失败执行的用例进行截屏
11. 用简单的测试用例替代注释
12. 遵循“绿色测试运行”策略。
13. 使用数据驱动替代重复测试。
14. 所有的测试用例都应该是独立的。
15. 设置详细的自动化测试报告。
不要仅仅依赖UI测试自动化。理想情况下,如果从测试周期中删除了整个UI自动化套件,其他测试能够在软件发布中捕获到90%的现有bug。需要记住,高级测试应该仅仅是第三个防御系统,用以捕获了前两个级别未捕获的所有其他问题。
如下图所示:
上图是有由最成功、最聪明的敏捷和Scrum指导者之的MikeCohn最初设计的敏捷测试金字塔模型,如果想要有一个可靠的测试自动化流水线,可以参考测试用例在每个阶段要实现的推荐比例。
为什么金字塔是这样构造的?
首先,低级别测试用例(low-level test (LLT))的执行速度非常快。单元测试比API测试速度快得多,而API测试比UI测试速度快得多。
为什么这个很重要?主要是因为更快的测试会给你更快的反馈,更快反馈可以尽早发现问题,从而节省大量的成本。
其次,在QA自动化管道中,较早地执行低级测试。通常,每次在代码提交到代码库 之前执行单元测试。如果项目是这样运作的,这样不仅捕获了bug,还阻止了bug进入项目代码库。
第三,低级测试用例(LLT)比高级测试测试用例(HLT)更稳定。下图很好地代表了低级测试(黑色)和高级测试(白色)的稳定性。这就是为什么,在自动化过程中,我首先选择了黑暗面……
文章开头提到的整个敏捷测试自动化金字塔在世界各地的许多著名公司都获得了巨大的成功。这就是为什么我们选择将它列入最佳实践图表的原因。
当然,您应该始终运行所有这些测试类型!但是也应该考虑是否所有的高级别用例(HLT)是否可以通过低级别用例(LLT)替代。 此时我们应该考虑使用UI测试 执行。
设计模式是软件设计中常见问题的可重用解决方案。我们可以说,每个模式都是针对特定问题的特定解决方案的特定示例,无论编程语言或范例。补充设计模式,我们有设计原则。设计原则提供了良好构建和可维护的软件所需遵循的指导原则或规则。模式只适用于特定的问题,设计原则都适用于整个环境
这与UI自动化测试有什么关系?正如前面所讨论的,UI测试是一条坎坷不平的道路,但我有个好消息。你不是第一个走这条路的司机。您可以将设计模式和原则视为强大的导航器。一个告诉你如何安全驾驶(设计原则),而另一个告诉你如何处理每个特定的障碍 (设计模式)。
在这一节中,我将讨论两种模式:页面对象模式,它已经成为测试自动化工程师中最流行的webUI自动化模式,以及脚本模式,它组织页面对象模式并改进它。
PageObjects模式的概念大约是在10年前引入的。它的目的是使UI自动化测试一致,避免代码重复,提高可读性,并组织web页面交互的代码。在web测试期间,总是需要与在这些页面上呈现的web页面和web元素交互(按钮、输入元素、图像等)。PageObjects模式接受这一需求,并在此基础上应用面向对象的编程原则,强制与所有页面和元素进行交互,就像使用对象一样。
例如,如果您需要单击一个按钮,您不需要关心如何在测试中检索该按钮,因为它已经在页面对象中处理了。您应该拥有正在查找的页面的对象,它应该已经包含您正在查找的按钮的对象。您所需要的就是对这个按钮对象使用一个引用,并在其上应用“单击”操作。你可以考虑所有的页面和网络元素:
如上图所示,该图是一个没有涉及模式的实例,编写测试用例需要对每个页面元素都进行操作交互。
WebDriverwebDriver = hisDriver;
webDriver.navigate().to("blazemeter.com");
String heading = webDriver.findElement(By.cssSelector(HEADING_ELEMENT)).getText();
Assert.assertEquals(heading,"Welcome to the Simple TravelAgency!");
Assert.assertEquals(webDriver.getTitle(),"BlazeDemo");
SelectflightsFromSelectElement = new Select(webDriver.findElement(By.cssSelector(FLIGHT_FROM_SELECT)));
SelectflightsToSelectElement = new Select(webDriver.findElement(By.id(FLIGHT_TO_SELECT)));
flightsFromSelectElement.selectByValue("Boston");
flightsToSelectElement.selectByValue("New York");
webDriver.findElement(By.cssSelector(FIND_FLIGHTS)).click();
Assert.assertTrue(webDriver.findElements(By.cssSelector(FLIGHTS_ROW_ELEMENTS)).size()> 0);
如果我们应用PageObjects模式并重构相同的测试,我们会看到一个完全不同的图片:
homePage.open();
homePage.waitUntilPageLoaded();
Assert.assertEquals(homePage.getTitle(), "BlazeDemo");
Assert.assertEquals(homePage.getHeadingText(), "Welcome to the Simple Travel Agency!");
homePage.findFlights("Boston", "New York");
Assert.assertTrue(flightsPage.getFlights().size() > 0);
综上所述,PageObjects的好处如下所示
· 使测试用例更清晰、更容易阅读。
· 组织结构代码。
· 避免重复(我们不应该两次指定相同的页定位符)。
· 在测试维护上节省大量时间,使UI自动化管道更快=>降低成本。
如果想让这个测试用例更清晰、更易于维护,您可以引入一个更高级别的抽象步骤或关键字。在不同的框架中,可能会看到这些模块的不同名称,但是它们的原理是相同的。步骤(关键字)是您可以在任何测试中重用的操作模块。一旦这些步骤(关键字)模块被编写出来,您所需要的就是对测试中的模块进行引用,可以使用这些特定模块提供的所有功能。
例如,我们创建一个模块,将所有与航班搜索相关的功能聚集在我们的网站上,那么测试将是这样的:
flightsSearchSteps.openHomePage();
flightsSearchSteps.verifyThatHomePageIsOpened();
flightsSearchSteps.verifyThatTitleAndHeadingTextIsCorrect();
flightsSearchSteps.searchFlightsBetween("Boston", "New York");
assertThat(flightsSearchSteps.foundFlights().size(), greaterThan(0));
但是,即使使用PageObjects模式,可能迟早会遇到一个矛盾,这将使您陷入一个非常沉重的框架。为什么?因为除了设计模式之外,您还需要注意设计原则。
例如,当PageObjects模式成为一个被称为“大型类”的反模式的原因时,这是一个常见的情况。当在某些页面上有太多元素时,就会出现这种情况,因此表示页面的对象的函数数量可能会变得非常大(这就是所谓的“大型”模式)。这违背了面向对象编程中最重要的设计原则:SOLID:
单一职责原则
开闭原则
Liskov替换原则
接口替换原则
依赖性倒置原则
主要违反单一责任原则,但原则的意思是每个 类都应该有一个单一的功能。RobertC. Martin(一位著名的软件工程师和合作者)用这些词描述了这个原则:“一个类应该只有一个理由去改变。”这就是为什么PageObjects可能会违背这个原则,因为页面类可以包含数百个功能,它们可以执行许多不同的操作。
不用担心,我们不会详细讨论每个原则的含义。你可以在网上浏览许多文章来获得一个想法(例如,你可以从wiki开始)。但是,需要知道的是,为了遵循页面对象模式的可靠原则,我们应该始终关心如何在页面和web元素之间分离操作,并在一段时间内进行额外的代码重构,以保持框架的可维护性。
提示 ScreenplayPattern,该模式旨在从头开始遵循SOLIDprinciples。简单地说Screenplay 是验收测试(包括UI测试)的设计模式,它允许您轻松地遵循可靠的原则。我建议您查看本文,这将给您一个关于剧本模式及其在UI自动化测试中使用的优秀解释。但是,即使没有详细说明,也可以用Screenplay 的模式来写我们之前的测试,来自己感受下不同:
givenThat(yuri).wasAbleTo(Start.withHomePage());
then(yuri).should(
seeThat(Application.information(),
displays("title",equalTo("BlazeDemo")),
displays("heading",equalTo("Welcome to the Simple Travel Agency!"))
)
);
when(yuri).attemptsTo(
SearchFlights.betweenCountries("Boston","New York")
);
then(yuri).should(
seeThat(
TheFlights.displayed(),
hasSize(greaterThan(0))
)
;
无论web应用程序的复杂性如何,这都是需要遵循的一条黄金法则。sleep或exacttimeout (具体thread. Sleep()函数和类似物)阻塞您的测试线程,以获得精确指定的秒数。换句话说,它让您有能力暂停测试。你什么时候需要这样的能力?
web应用程序的行为取决于许多因素,如网络速度、机器性能或应用服务器上的当前负载。由于所有这些因素,不可能总是预测加载特定页面或web元素需要多长时间。这就是为什么有时可能想要添加一个超时和暂停脚本检查执行至少在一定的时间内。
如果不知道如何正确处理这个问题,将永远无法看到UI自动化的稳定性。
让我们假设在我们的测试中,我们将打开主页并验证主页的标题。非常简单。您只需要实现两个函数。一个打开页面,第二个验证标题元素是否显示,是否具有正确的值。但是如果我们知道我们的应用程序需要7-8秒才能启动呢?
Selenium是一个web浏览器自动化工具,它运行得非常快,而且说得非常快,甚至比你还快。它可以以毫秒 为单位打开页面,并在应用程序本身仍然启动时尝试获取标题元素的文本。在这种情况下....请不要写这样的代码:
yuri.openHomePage();
Thread.sleep(10000);
yuri.verifyThatTitleAndHeadingTextIsCorrect();
这是UI自动化测试稳定性最强的杀手。为什么?我们失去了时间,因为你知道在95%的情况下,应用程序应该在7-8秒内启动和运行。因此,每次我们损失大约2-3秒的执行时间。
你觉得这没什么吗?我见过很多项目,有3000个UI测试。每次需要打开应用程序并等待它启动和运行。也许你想在3个不同的浏览器上运行它?3000(测试)* 3(浏览器)* 2.5(平均损失秒)=22500(秒)=375(分钟)=6.25小时!综上所述,在这种情况下,我们损失了大约6个小时,仅仅因为我们不想正确地创建它。
如果有一天应用程序加载需要10.5秒,会发生什么情况呢?仅仅因为一些网络延时。我们测试用例将执行失败。但是当你尝试在第二天在本地运行它时,它运行得非常好。这是在测试中使用这种等待方式可能会遇到的麻烦的一个例子。
我猜你会发现这很糟糕,对吧?那么如何处理这种情况呢?可以在Selenium官方文档中找到答案——隐式和显式等待!完全按照这个顺序。
隐式等待告诉浏览器等待所有元素的指定时间。如果此时没有找到某个元素,则将其报告为失败。如果发现元素比指定的时间快,就继续前进,不要等待全部时间。例如,如果隐式等待5秒,但是元素在2秒后出现,那么我们的脚本将不会等待3秒的剩余时间。这对于UI自动化测试来说是一个巨大的时间节省。
以下是如何通过使用Selenium来指定Java中的隐式等待:
WebDriver driver = new FirefoxDriver();
driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);
那么显示等待是什么呢?显式等待是为需要而设计的,当某个web元素或操作需要比其他web元素或操作加载更长的时间时。如果您的应用程序以一种很长的时间开始(7-8秒)的方式工作,但是在那之后,它运行得非常快?没有必要将隐式等待指定为10甚至15秒,因为您的应用程序加载很慢。为此,可以使用显式等待,它等待指定时间的特定条件。
下面是我们如何通过使用显式等待的思想重写前面的示例:
yuri.openHomePage();
withTimeoutOf(15, TimeUnit.SECONDS).waitFor(MAIN_HEADING_LABEL);
yuri.verifyThatTitleAndHadingTextIsCorrect();
在这种情况下,我们也不会浪费任何时间,脚本执行会在预期元素被发现之后立即继续。
看起来清楚,对吧?不像你想的那样清楚…官方的Selenium网站显示了这个非常重要的提示:“不要混合隐式和显式等待。这样做会导致不可预测的等待时间。例如,设置隐式等待10秒和显式等待15秒,可能导致超时20秒后发生。
您可以使用这篇文章以及这篇文章来了解为什么它更好,并且只使用显式的等待。
为了使框架易于维护,需要关心它的结构。通过结构,我指的是你组织代码的方式。基本原则很简单——应该清楚地将测试与测试自动化框架功能分开。换句话说,测试部分中的每个类 都应该表示一个测试站点,而此类的每个函数都应该是一个测试。
假设我们有一个项目,所有UI自动化测试都应该测试一个web应用程序。然后你可能想要遵循这种分离方法:
当系统由多个相互连接的UI应用程序组成时,可能会出现另一种情况。在这种情况下,最好使用您的测试自动化框架来创建一个单独的模块,该框架将在不同的测试模块之间共享(针对每个应用程序)。
如果您需要在CI服务器上运行您的测试?如果UI测试自动化框架不能移植,将是一个非常棘手的任务。以下建议可以帮助解决测试框架可移植性的问题。
首先,不要在本地机器上存储测试自动化文件!如果您拥有测试执行所需的测试自动化文件,则应该将它们附加到框架中。如果它们相对较小,则可以将它们与框架本身一起存储在控制版本中。如果它们很大,那么您可以使用外部存储,比如AmazonS3或任何其他云存储。然后,在第一次测试执行过程中,如果文件还没有出现,则实现一种机制,将这些文件下载到正确的位置。
同样的原理也适用于网络驱动程序。出于某种原因,从项目到项目,在需要安装web驱动程序以运行测试时,我仍然看到许多情况。此外,有时您需要一个特定的web驱动程序版本,如果文档不太好,您可以花很多时间来查看第一个绿色测试。这应该如何处理?
有一个很好的辅助工具叫做WebDriverManager。它负责整个驱动程序的下载和配置工作流。您所需要做的就是在您的框架中配置一个额外的java依赖项,当您决定在另一台没有安装web驱动的机器上运行测试时,所有的web驱动程序将被自动下载和配置!
WebDriverManager的安装非常简单,在这个页面上有几个简单的步骤。然而,当我第一次使用Serenity framework时,我发现它并不那么简单。
Serenity framework有自己的web驱动程序配置工作流。我无法在web上找到合适的解决方案,所以如果您还决定使用Serenity framework,那么这个部分可能会很有用。解决方案的主要思想是,Serenity framework具有自定义web驱动程序的机制。您所需要创建一个单独的类,它扩展了Serenity framework的DriverSource类,并使用WebDriverManager创建所需的驱动程序:
public class AutoDownloadedWebDriver implements DriverSource {
public enum WebBrowsers {
CHROME, CHROME_HEADLESS
}
private BaseConfiguration baseConfiguration = new BaseConfiguration();
@Override
public WebDriver newDriver() {
WebBrowsers browserType = WebBrowsers.valueOf(baseConfiguration.getProperty("browser"));
switch (browserType) {
case CHROME:
ChromeDriverManager.getInstance().setup();
return new ChromeDriver();
…… other drivers……....
之后,只需要在你的Serenity framework中指定几行。conf文件中设置以下文件:
webdriver.driver = provided
webdriver.provided.type = mydriver
thucydides.driver.capabilities = mydriver
webdriver.provided.mydriver = common.AutoDownloadedWebDriver //reference to your created custom web driver
通过使用这种配置,您不再需要关心web驱动程序的配置。每件事都将自动安装,整个团队合作将节省很多时间
测试名称应该非常清楚,并提供一个自我描述性的概念,表示这个测试用例来测试什么确切的功能。为什么?首先,需要考虑好维护。其次,要让整个团队都能明白该刚发的用途。此外,如果用例执行失败,可以迅速确认是修改那个方法引起的。
糟糕的测试名称的例子:
@Test
publicvoidflightSearchTest() {.....}
糟糕是因为它没有告诉任何关于测试场景的细节。这是一个命名良好的测试的例子:
@Test
publicvoiduserShouldBeAbleToFindFlightsFromBostonToNewYork() {.....}
这个测试名称要好得多,因为在测试失败的情况下,您可以立即了解哪些功能失败了,您不需要进入测试,并验证它实际执行了哪些功能。这只是一个品味问题,但许多工程师倾向于使用“_”分离方法而不是CamelCase:
@Test
publicvoiduser_should_be_able_to_find_flights_from_Boston_to_New_York() {.....}
断言的设计方法是,如果断言失败,则测试失败。最初,断言是为单元测试而设计的。这是一个很好的实践,因为每个单元测试应该只做一个特定的断言。
但是在UI自动化中,您可能想要验证一行中的几件事情。假设您有几个将要验证的UI元素,其中两个有一些意料之外的值。使用经典断言,在测试执行之后,您只会注意到一个错误,然后测试就会失败。这意味着你的测试做得很好!它抓住了一个错误!但是,第二个问题呢?你怎样才能抓住这个问题?是的,只有在第一个问题解决之后。这可能需要几天,有时是几周。这就是为什么我们有兴趣立即抓住所有的问题!在这里,您可以使用软断言的机制获得巨大的好处。
这就是为什么要记住软断言的原因。当您需要断言一个条件,但要让测试继续时,就使用这种类型的断言。通过使用软断言,即使您的断言失败,您的测试执行流也将继续。最后,它将总结失败的断言列表,并让您知道所有发现的问题。
实现软断言的方法有很多。我倾向于通过一个名为AssertJ的强大的断言框架来使用软断言。如果您从未听说过它,那么您一定要检查我的其他文章,它显示了使用第三方断言框架可以获得的好处。
下面是一个基于我们测试的软断言示例:
SoftAssertions softly = new SoftAssertions();
softly.assertThat(homePage.getTitle()).isEqualTo("BlazeDemo");
softly.assertThat(homePage.getHeadingText()).isEqualTo("Welcome to the Simple Travel Agency!");
softly.assertAll();
这一最佳实践将帮助您在调查测试失败的原因时节省大量时间。您可以实现一种机制,在测试失败的情况下,使浏览器屏幕截图。如果您还没有这个机制,或者您刚刚开始创建UI测试自动化框架,请记住这一重要提示。
根据您使用或不使用的工具,为失败的步骤创建屏幕快照的实现可能会有所不同。至于我,我更喜欢使用令人敬畏的Serenityframework,它有一个内置的屏幕截图机制。此外,它允许您免费保存所有测试步骤的屏幕截图,因为它是内置的框架功能,所以您甚至不需要关心它的实现。
当您使用这个框架来处理您的测试执行时,这只是Serenityframework报告的一小部分。
因此,对于每个步骤,您可以看到相关的屏幕快照,它显示了测试步骤中web应用程序的状态。非常方便的和有用的。
测试用例应该总是清晰易懂。如果你有一种感觉,你需要留下注释来理解这句话的含义,那么你需要退后一步,再想想自己做错了什么。让我们假设在这个测试中,我们需要等待主页面被完全加载。我们可以这样做:
withTimeoutOf(10, TimeUnit.SECONDS).waitFor(MAIN_HEADING_LABEL);// waiting for main page to load
它工作吗?是的!清楚吗?我们留下了注释,所以不!请不要在用例中这样做。相反,您可以创建一个函数,将该代码放入其中,并赋予该函数一个合理的名称。之后,在测试中,我们可以用:
homePage.waitUntilPageLoaded();
现在不需要任何评论了。尽量简化所有的测试,而不要在附近发表任何评论。
一方面,这是最简单的原则之一。但另一方面,大多数工程师忽略了这条规则。通过“绿色测试运行”策略,我们的意思是,如果某些测试失败并且是红色的,那么100%您在测试中的应用程序中有一个问题。
有时,有些情况下,应用程序已经有了优先级低的bug列表,并且团队在可预见的将来不会解决这些问题。在这种情况下,大多数测试自动化工程师都忽略了这些测试。他们将它们留在运行中,并在测试执行结束时完成许多红色测试。一旦测试执行完成,他们就会检查失败的测试,并验证所有的红色测试都是由于这些现有的bug而导致的失败,或者是否存在一些新的问题。
没有,没有!这不是基于最佳实践的方法。首先,每次执行完成时,您都不知道是否有一些意料之外的问题。如果结果是红色的,并且它仍然是红色的,那么执行运行状态告诉您什么都没有。其次,要了解您是否真的有一些意外的错误,或者如果所有这些错误都是预期的,您需要花费一些时间。如果只有一次,那就太好了。但是测试结果验证是一个重复的过程,你可能每天都在做。你一次又一次地进行同样的不必要的检查,损失了大量的时间和精力。
相反,如果您的运行中的测试失败了,那么您可以做的最好的事情是将它们分离到单独的运行中,并在主测试执行中忽略它们。在调查失败的构建时,这将为您节省很多时间。当您从构建中分离出所有预期的失败时,您知道如果测试执行导致至少一个红色失败的测试,那么它就是一个真正的新问题。在任何其他情况下,它们都应该是绿色的。
13. Use Data-Driven Instead of Repeated Tests(使用数据驱动替代重复测试)
当您需要使用不同的数据来测试相同的工作流时,数据驱动的测试非常有用。让我们假设您想要验证不同城市之间的航班搜索。这就是在没有数据驱动方法的情况下测试的样子:
@Test
public void verifySearchBetweenBostonAndNewYork(){
homePage.open();
homePage.waitUntilPageLoaded();
homePage.findFlights(Boston, NewYork);
Assert.assertTrue(flightsPage.getFlights().size() > 0);
}
@Test
public void verifySearchBetweenBostonAndBerlin(){
homePage.open();
homePage.waitUntilPageLoaded();
homePage.findFlights(Boston, Berlin);
Assert.assertTrue(flightsPage.getFlights().size() > 0);
}
@Test
public void verifySearchBetweenPortlandAndRome(){
homePage.open();
homePage.waitUntilPageLoaded();
homePage.findFlights(Portland, Rome);
Assert.assertTrue(flightsPage.getFlights().size() > 0);
}
但如果你需要测试10或100种组合呢?我们的测试文件将会增长得非常快,并且变得不方便使用和无法维护。主要原因是,如果我们需要在工作流本身中做一个小小的更改,那么我们需要对所有的测试应用相同的更改!这就是数据驱动测试能给你带来巨大好处的地方。通过使用数据驱动的测试,您只能使用一个测试和一个数据数组,我们将使用这些数据来运行所有不同的数据组合:
private final City cityFrom;
private final City cityTo;
@TestData
public static Collection
看起来干净?现在你只有一个你需要维护的。如果您想要用另外一个组合来编写一个测试,那么您所需要的就是将额外的组合添加到测试数据数组中。
您可以通过“/src/test/java/ui/pageobject/DataDrivenExampleTest”找到我们项目中数据驱动测试的一个很好的例子。java”路径。
你可能不同意,但我坚信所有的测试都应该是独立的。依赖关系将使您的测试难以阅读和维护。在并行自动化运行期间,您肯定会遇到麻烦,因为在并行测试期间,您不能保证在运行中测试的顺序。如果您需要实现一个对许多测试有效的先决条件,只需使用“Before”方法,并按照它在测试执行期间只运行一次的方式进行配置。
测试自动化报告对于优化您作为QA自动化工程师的工作非常重要。理想情况下,您不应该花费超过10-20%的时间来验证来自不同测试执行的测试结果。
关于如何进行这一步,有很多选择。您可以使用像TestNG(本文所述)这样的基本测试执行工具来设置报告。您可以与测试管理工具(如Zephyr、x射线或TestRail)进行集成。或者,您可以使用一个高级框架来提供这种能力。
在我的自动化框架中,我喜欢使用宁静框架,它为您提供出色的实时测试报告,它显示了根据执行结果、类型、标记、功能等分组的所有测试。除此之外,它对每个测试都有非常详细的步骤说明,在结果分析中非常有用。我强烈建议检查我们的测试自动化框架,它是使用宁静框架开发的。现在,试试这个报告。您所需要做的就是通过在项目根的命令行中运行指定的命令来执行所有的测试:
mvn clean verify
After that, the result report file will belocated through this path: “/target/site/serenity/index.html”.
UI测试自动化并不不稳定。UI测试自动化框架的稳定性取决于您。真实、稳定和可靠的UI自动化是一项艰苦的工作,但它也很有趣。在您的框架创建之初,您有一个选择:您是否将使用一个工具来帮助您实现您的目标并解决您的问题,或者您是否会创建您将在日常的基础上与之斗争的软件?对我来说,答案是明确的。