我已经使用了一段时间,并且遇到了一些似乎可以使生活更轻松的事情。 我以为可以将其作为教程分享,所以我将向您介绍这些部分:
- 使用Maven设置Web项目,配置Selenium以在CI上作为集成测试运行
- 研究使用“页面对象”为网站中的页面建模的好方法,以及创建受保护的变量的其他方法。
- 使用JPA和Hibernate对数据库执行CRUD操作,并让Maven对它们执行集成测试,而无需进行有时会造成的任何昂贵且通常没有文档的设置。
这篇文章假定您对Java,Spring,Maven 2以及HTML感到满意。 您还需要在计算机上安装Firefox。 本教程旨在以其他方式与技术无关。
创建一个Webapp
首先,我们需要一个webapp进行测试。 使用maven-webapp-archetype创建一个项目,并将其称为“ selenuim-tutorial”。
要运行集成测试(IT),我们将使用Cargo插件。 这将启动和停止Jetty和Tomcat之类的容器。 您可以使用Cargo在一个命令中使用Jetty(默认设置)启动网站,而无需进行任何更改:
mvn cargo:run
并在浏览器中检查它:
http:// localhost:8080 / selenuim-tutorial
您将获得一个没有欢迎文件设置的404,因此将其添加到web.xml文件中:
<welcome-file-list>
<welcome-file>/index.jsp</welcome-file>
</welcome-file-list>
如果您运行货物:再次运行,您现在将看到“ Hello World!” 由Maven创建的页面。
配置货物
我们可以将Cargo设置为在运行测试之前启动Jetty容器,然后再将其停止。 这将使我们能够启动站点,运行集成测试,然后再将其停止。
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId>
<version>1.2.0</version>
<executions>
<execution>
<id>start</id>
<phase>pre-integration-test</phase>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>stop</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
您可以使用以下方法测试这项工作:
mvn verify
此时要注意的一件事是Cargo运行在端口8080上。如果您已经有一个进程在该端口上进行侦听,则可能会看到类似以下错误:
java.net.BindException: Address already in use
这可能是因为您已经在此端口上运行了另一个容器。 如果要在CI上运行它(它本身可以在端口8080上运行),则可能需要更改。 将这些行添加到插件设置中:
<configuration>
<type>standalone</type>
<configuration>
<properties>
<cargo.servlet.port>10001</cargo.servlet.port>
</properties>
</configuration>
</configuration>
现在该应用程序将在这里:
http:// localhost:10001 / selenuim-tutorial /
设置集成测试阶段
接下来,我们需要能够运行集成测试。 这需要Maven故障安全插件,并将适当的目标添加到pom:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.12</version>
<executions>
<execution>
<id>default</id>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
默认情况下,Failsafe期望测试匹配模式“ src / test / java / * / * IT.java”。 让我们创建一个测试来证明这一点。 请注意,我尚未从Junit 3.8.1更改过。 稍后我将解释原因。
这是一个基本的,不完整的测试:
package tutorial;
import junit.framework.TestCase;
public class IndexPageIT extends TestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
}
public void testWeSeeHelloWorld() {
fail();
}
}
测试有效:
mvn verify
您应该看到一个测试失败。
要使用Selenium进行测试,您需要向pom.xml添加测试范围的依赖项:
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-firefox-driver</artifactId>
<version>2.19.0</version>
<scope>test</scope>
</dependency>
现在,我们可以对测试进行一些更改:
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
…
private URI siteBase;
private WebDriver drv;
@Override
protected void setUp() throws Exception {
super.setUp();
siteBase = new URI("http://localhost:10001/selenuim-tutorial/");
drv = new FirefoxDriver();
}
...
public void testWeSeeHelloWorld() {
drv.get(siteBase.toString());
assertTrue(drv.getPageSource().contains("Hello World"));
}
稍后我们将删除这些硬编码值。
再次运行:
mvn verify
您应该不会看到任何故障。 您将拥有一个挥之不去的Firefox。 它不会关闭。 运行此测试100次,您将运行100个Firefox。 这将很快成为一个问题。 我们可以通过在测试中添加以下初始化块来解决此问题:
{
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
drv.close();
}
});
}
自然,如果我们创建另一个测试,我们很快就会违反DRY原则。 我们将在下一部分中讨论该问题,并查看需要数据库连接时发生的情况,以及其他一些方法来确保您的测试易于编写和维护。
Spring语境
在前面的示例中,应用程序的URI和使用的驱动程序都经过了硬编码。 假设您熟悉Spring上下文,那么更改这些内容很简单。 首先,我们将添加正确的依赖项:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>3.1.1.RELEASE</version>
<scope>test</scope>
</dependency>
这将使我们能够使用和应用程序上下文来注入依赖项。 但是我们还需要正确的Junit运行程序来测试它,可以在spring-test软件包中找到它:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>3.1.1.RELEASE</version>
<scope>test</scope>
</dependency>
现在,我们可以更新测试以使用它。 首先,我们需要创建src / test / resources / applicationContext-test.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="siteBase" class="java.net.URI">
<constructor-arg value="http://localhost:10001/selenuim-tutorial/" />
</bean>
<bean id="drv" class="org.openqa.selenium.firefox.FirefoxDriver" destroy-method="quit"/>
</beans>
Spring完成后将清除浏览器,因此我们可以从AbstractIT中删除关闭钩子。 这比让测试用例执行此操作更为健壮。
弹簧测试不适用于JUnit 3,它至少需要JUnit 4.5。 让我们在pom.xml中更新到版本4.10:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
最后,我们需要更新测试以同时使用Spring和JUnit 4.x:
package tutorial;
import static org.junit.Assert.assertTrue;
import java.net.URI;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.openqa.selenium.WebDriver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/applicationContext-test.xml" })
public class IndexPageIT {
@Autowired
private URI siteBase;
@Autowired
private WebDriver drv;
@Test
public void testWeSeeHelloWorld() {
...
这些更改将配置从硬编码值转移到XML配置。 现在,我们可以将要测试的位置更改为例如其他主机,并更改我们正在使用的Web驱动程序,这留给用户练习。
关于浏览器的快速说明。 我发现浏览器更新后,测试通常会开始失败。 似乎有两种解决方案:
- 升级到最新版本的Web驱动程序。
- 不要升级浏览器。
我出于安全考虑,在大多数情况下,我认为第一种选择是最好的
抽象IT
当前,您需要复制IoC的所有代码。 一个简单的重构可以解决这个问题。 我们将为所有测试和上拉通用功能创建一个超类。 出于重构原因,我将使用继承而不是合成,原因将在稍后介绍。
package tutorial;
import java.net.URI;
import org.junit.runner.RunWith;
import org.openqa.selenium.WebDriver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/applicationContext-test.xml" })
public abstract class AbstractIT {
@Autowired
private URI siteBase;
@Autowired
private WebDriver drv;
public URI getSiteBase() {
return siteBase;
}
public WebDriver getDrv() {
return drv;
}
}
package tutorial;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class IndexPageIT extends AbstractIT {
@Test
public void testWeSeeHelloWorld() {
getDrv().get(getSiteBase().toString());
assertTrue(getDrv().getPageSource().contains("Hello World"));
}
}
页面对象
“页面对象”是封装页面的单个实例并为该实例提供程序化API的对象。 基本页面可能是:
package tutorial;
import java.net.URI;
import org.openqa.selenium.WebDriver;
public class IndexPage {
/**
* @param drv
* A web driver.
* @param siteBase
* The root URI of a the expected site.
* @return Whether or not the driver is at the index page of the site.
*/
public static boolean isAtIndexPage(WebDriver drv, URI siteBase) {
return drv.getCurrentUrl().equals(siteBase);
}
private final WebDriver drv;
private final URI siteBase;
public IndexPage(WebDriver drv, URI siteBase) {
if (!isAtIndexPage(drv, siteBase)) { throw new IllegalStateException(); }
this.drv = drv;
this.siteBase = siteBase;
}
}
请注意,我提供了一个静态方法来返回我们是否在索引页上,并且已经对其进行了注释(对于这样的自填充方法,这是不必要的)。 页面对象形成一个API,值得记录。 您还将看到,如果URL不正确,我们将引发异常。 值得考虑使用什么条件来识别页面。 任何可能更改的内容(例如,页面标题,可能会在不同语言之间更改)都是一个糟糕的选择。 不变的东西和机器可读的东西(例如页面的路径)是不错的选择。 如果要更改路径,则需要更改测试。
现在让我们自己制造一个问题。 我想将其添加到index.jsp,但是生成HTML无法解析:
<% throw new RuntimeException(); %>
相反,我们将创建一个新的servlet,但首先需要将servlet-api添加到pom.xml中:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
package tutorial;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class IndexServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
throw new RuntimeException();
}
}
将其添加到web.xml并删除现在不必要的欢迎页面:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<servlet>
<servlet-name>IndexServlet</servlet-name>
<servlet-class>tutorial.IndexServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>IndexServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
更新IndexPageIT:
@Test
public void testWeSeeHelloWorld() {
getDrv().get(getSiteBase().toString());
new IndexPage(getDrv(), getSiteBase());
}
再次运行测试。 它通过了。 这可能不是您想要的行为。 Selenium没有提供通过WebDriver实例检查HTTP状态代码的方法。 容器之间的默认错误页面也没有足够一致(例如,与在Tomcat上运行时进行比较); 我们无法对错误页面的内容进行假设以判断是否发生错误。
我们的索引页面当前没有任何可机读的功能,可以让我们从错误页面中分辨出来。
要整理,请修改servlet以显示index.jsp:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
getServletContext().getRequestDispatcher("/index.jsp").forward(request, response);
}
当前index.jsp有点太简单了。 在index.jsp旁边创建一个名为create-order.jsp的新页面,并在index.jsp上创建指向该页面的链接。 我们可以为订单页面创建一个新类,并创建一个将我们从索引页面导航到订单页面的方法。
将以下内容添加到index.jsp中:
<a href="create-order.jsp">Create an order</a>
create-order.jsp现在可以为空。 我们还可以为其创建一个页面对象:
package tutorial;
import java.net.URI;
import org.openqa.selenium.WebDriver;
public class CreateOrderPage {
public static boolean isAtCreateOrderPage(WebDriver drv, URI siteBase) {
return drv.getCurrentUrl().equals(siteBase.toString() + "create-order.jsp");
}
private final WebDriver drv;
private final URI siteBase;
public CreateOrderPage(WebDriver drv, URI siteBase) {
if (!isAtCreateOrderPage(drv, siteBase)) { throw new IllegalStateException(); }
this.drv = drv;
this.siteBase = siteBase;
}
}
将以下依赖项添加到pom.xml中,这将为我们提供一些有用的注释:
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-support</artifactId>
<version>2.19.0</version>
<scope>test</scope>
</dependency>
我们现在可以充实IndexPage了:
@FindBy(css = "a[href='create-order.jsp']")
private WebElement createOrderLink;
public IndexPage(WebDriver drv, URI siteBase) {
if (!isAtIndexPage(drv, siteBase)) { throw new IllegalStateException(); }
PageFactory.initElements(drv, this);
this.drv = drv;
this.siteBase = siteBase;
}
对PageFactory.initElements的调用将填充带有@FindBy注释的字段,该字段具有与网页上的元素匹配的对象。 请注意,使用CSS选择器是为了以不太可能更改的方式定位链接。 其他方法包括使用链接文本来匹配页面上的元素(可能会因不同的语言而改变)。
现在,我们可以在IndexPages上创建一个方法,该方法导航到CreateOrderPages。
public CreateOrderPage createOrder() {
createOrderLink.click();
return new CreateOrderPage(drv, siteBase);
}
最后,我们可以在IndexPageIT中为此链接创建一个测试:
@Test
public void testCreateOrder() {
getDrv().get(getSiteBase().toString());
new IndexPage(getDrv(), getSiteBase()).createOrder();
assertTrue(CreateOrderPage.isAtCreateOrderPage(getDrv(), getSiteBase()));
}
执行mvn verify,您应该找到新的测试通过。 至此,我们有两个测试无法在它们之间进行清理。 他们在两个测试中使用相同的WebDriver实例,最后一页仍将打开,并且设置的所有cookie都将保持不变。 为多个测试创建单个WebDriver实例的优缺点。 主要优点是减少了打开和关闭浏览器的时间成本,但是一个缺点是,每次测试,Cookie设置和弹出窗口打开后,浏览器实际上都会变脏。 我们可以使用AbstractIT中合适的setUp方法确保每次测试之前它都是干净的:
@Before
public void setUp() {
getDrv().manage().deleteAllCookies();
getDrv().get(siteBase.toString());
}
有其他方法可以解决,我将让您自行研究在每次测试之前创建新的WebDriver实例的方法。
@FindBy批注在窗体上使用时特别有用。 向create-order.jsp添加新表单:
<form method="post" name="create-order">
Item: <input name="item"/> <br/>
Amount: <input name="amount"/><br/>
<input type="submit"/>
</form>
将这些WebElement添加到CreateOrderPage,并添加一种提交表单的方法:
@FindBy(css = "form[name='create-order'] input[name='item']")
private WebElement itemInput;
@FindBy(css = "form[name='create-order'] input[name='amount']")
private WebElement amountInput;
@FindBy(css = "form[name='create-order'] input[type='submit']")
private WebElement submit;
public CreateOrderPage(WebDriver drv, URI siteBase) {
if (!isAtCreateOrderPage(drv, siteBase)) { throw new IllegalStateException(); }
PageFactory.initElements(drv, this);
this.drv = drv;
this.siteBase = siteBase;
}
public CreateOrderPage submit(String item, String amount) {
itemInput.sendKeys(item);
amountInput.sendKeys(amount);
submit.click();
return new CreateOrderPage(drv, siteBase);
}
最后,我们可以为此创建一个测试:
package tutorial;
import static org.junit.Assert.*;
import org.junit.Test;
public class CreateOrderPageIT extends AbstractIT {
@Test
public void testSubmit() {
new IndexPage(getDrv(), getSiteBase()).createOrder().submit("foo", "1.0");
}
}
结论
您可能要注意的一件事是,submit方法不需要将金额作为您期望的数字。 您可以创建一个测试以查看提交的是字符串而不是数字。 集成测试的编写可能很耗时,并且由于诸如元素ID或输入名称之类的事物的更改而容易损坏。 结果,创建它们所获得的最大好处是,最初仅在您站点内的业务关键路径上创建它们,例如,产品订购,客户注册流程和付款。
在本教程的下一部分中,我们将研究如何使用一些数据支持测试以及由此带来的挑战。
参考:我们的JCG合作伙伴 Alex Collins在Alex Collins博客上的教程 : 教程:与Selenium的集成测试-第1部分 , 教程:与Selenium的集成测试-第2部分 。
翻译自: https://www.javacodegeeks.com/2012/04/integration-testing-with-selenium.html