Java-使用 spring 依赖项注入的 Cucumber 测试在功能跨越步骤 类 时抛出 NullPointerException
Java-Cucumber tests using spring dependency injection throws NullPointerException when Feature spans Step classes
我正在使用相当典型的 Maven 架构,Java-Cucumber、Selenium,以及 Spring 依赖注入测试系统设置来测试动态 Angular 前端网站。 (pom.xml 中的版本)ArchitectureWSpringDI 它工作得非常好,我可以轻松地 运行 进行数百次测试,但我无法像使用 Ruby瓦提尔。一篇文章指出 Ruby 有一个 Java 缺少的“世界”对象,但是用于依赖注入的 Spring 应该可以解决这个问题
我读了很多“保留状态”的帖子,但似乎没有任何内容适用于它的工作原理,Cucumber 和 Spring 中有很多落后的版本,尽管我仍在使用 Java 8. 大多数保留状态的帖子似乎都在单个文件中的步骤之间,在单个测试中。
主要示例是众多示例中的一个,我希望能够使用我的@Given I 登录步骤创建一个步骤文件,而不必将该步骤放在其他一百个步骤文件中。
如果我有这样的功能文件:
Feature: As an account holder I examine account details
Scenario: View personal summary widget info on details page
Given Log into "web" on "dev" as "username" with "password" using "chrome"
When I tap the first account section
Then I see a list of transactions
并将其与包含所有步骤的步骤文件匹配,如下所示
@SpringBootTest
public class AccountsSteps {
private final MyAccountsPage page;
@Autowired
public AccountsSteps(MyAccountsPage page){
this.page = page;
}
@Given("Log into {string} on {string} as {string} with {string} using {string}")
public void logIntoOnAsWithUsing(String app, String env, String user, String pass, String browser) {
page.loadAny(env, app, browser);
page.sendUsername(user);
page.sendPassword(pass);
page.loginButtonClick();
}
@When("I tap the first account section")
public void iTapTheFirstAccountSection() {
page.waitForListOfElementType(WebElement);
page.clickFirstAccountLink();
}
@Then("I see a list of transactions")
public void iSeeAListOfTransactions() {
By selector = By.cssSelector("div.container");
page.waitForLocateBySelector(selector);
Assert.assertTrue(page.hasTextOnPage("Account details"));
}
}
一切正常,但如果我有另一个功能使用相同的@Given,那么上面和下面的是准确的,所以它不会在新步骤文件中创建新步骤。
Feature: As an account owner I wish to edit my details
Scenario: My profile loads and verifies the correct member's name
Given Log into "web" on "dev" as "username" with "password" using "chrome"
When I use the link in the Self service drop down for My profile
Then the Contact Details tab loads the proper customer name "Firstname Lastname"
与此步骤文件匹配,请注意缺少给定步骤,因为它使用的是另一个文件中的步骤。
@SpringBootTest
public class MyProfileSteps {
private final MyProfilePage page;
@Autowired
public MyProfileSteps(MyProfilePage page){
this.page = page;
}
@When("I use the link in the Self service drop down for My profile")
public void iUseTheLinkInTheSelfServiceDropDownForMyProfile() {
page.clickSelfServiceLink();
page.clickMyProfileLink();
}
@Then("the Contact Details tab loads the proper customer name {string}")
public void theContactDetailsTabLoadsTheCustomerName(String fullName) {
System.out.println(page.getCustomerNameFromProfile().getText());
Assert.assertTrue(page.getCustomerNameFromProfile().getText().contains(fullName));
page.teardown();
}
}
我终于找到了问题的症结所在。切换到不同步骤文件中的步骤时,它会抛出异常。
When I use the link in the Self service drop down for My profile
java.lang.NullPointerException
at java.util.Objects.requireNonNull(Objects.java:203)
at org.openqa.selenium.support.ui.FluentWait.<init>(FluentWait.java:106)
at org.openqa.selenium.support.ui.FluentWait.<init>(FluentWait.java:97)
at projectname.pages.BasePage.waitForClickableThenClickByLocator(BasePage.java:417)
at projectname.pages.BasePageWeb.clickSelfServiceLink(BasePageWeb.java:858)
at projectname.steps.MyProfileSteps.iUseTheLinkInTheSelfServiceDropDownForMyProfile(MyProfileSteps.java:39)
at ✽.I use the link in the drop down for My profile(file:///Users/name/git/project/tests/projectname/src/test/resources/projectname/features/autocomplete/my_profile.feature:10)
我特意将它们全部绑定在一起,因此测试每次只调用一个新的 Selenium 实例,而且它绝对不会打开新的浏览器 window,它只会崩溃并关闭。
public interface WebDriverInterface {
WebDriver getDriver();
WebDriver getDriverFire();
void shutdownDriver();
WebDriver stopOrphanSession();
}
并且有几个配置文件会 运行 不同的配置,但我主要的本地测试 WebDriverInterface 看起来像这样。
@Profile("local")
@Primary
@Component
public class DesktopLocalBrowsers implements WebDriverInterface {
@Value("${browser.desktop.width}")
private int desktopWidth;
@Value("${browser.desktop.height}")
private int desktopHeight;
@Value("${webdriver.chrome.mac.driver}")
private String chromedriverLocation;
@Value("${webdriver.gecko.mac.driver}")
private String firefoxdriverLocation;
public WebDriver local;
public WebDriver local2;
public DesktopLocalBrowsers() {
}
@Override
public WebDriver getDriver() {
System.setProperty("webdriver.chrome.driver", chromedriverLocation);
System.setProperty("webdriver.chrome.silentOutput", "true");
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.addArguments("--disable-extensions");
chromeOptions.addArguments("window-size=" + desktopWidth + "," + desktopHeight);
local = new ChromeDriver(chromeOptions);
return local;
}
@Override
public WebDriver getDriverFire() {
System.setProperty("webdriver.gecko.driver", firefoxdriverLocation);
FirefoxBinary firefoxBinary = new FirefoxBinary();
FirefoxOptions firefoxOptions = new FirefoxOptions();
firefoxOptions.setLogLevel(FirefoxDriverLogLevel.FATAL);
firefoxOptions.setBinary(firefoxBinary);
local2 = new FirefoxDriver(firefoxOptions);
return local2;
}
@Override
public void shutdownDriver() {
try{
local.quit();
}catch (NullPointerException npe){
local2.quit();
}
}
public WebDriver stopOrphanSession(){
try{
if(local != null){
return local;
}
}catch (NullPointerException npe){
System.out.println("All Drivers Closed");
}
return local2;
}
}
我有相当标准的跑步者。我已经尝试了 Cucumber Runner 的几种变体,移动到不同的目录,使用胶水和 extraglue 配置,但要么什么都没有改变,要么我完全打破它。这是现在正在运行的那个。
@RunWith(Cucumber.class)
@CucumberOptions(features = "src/test/resources/projectname/features/",
glue = "backbase",
// extraGlue = "common", // glue and extraGlue cannot be used together
plugin = {
"pretty",
"summary",
"de.monochromata.cucumber.report.PrettyReports:target/cucumber",
})
public class RunCucumberTest {
}
和我的 Spring 亚军
@RunWith(SpringRunner.class)
@CucumberContextConfiguration
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class SpringContextRunner {
}
以及开箱即用的应用程序页面以供参考。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
为了防止有人发现它对集思广益或诊断有用,我的页面对象从 BasePage 开始,它变得太大了,因为它包含所有常用方法,但看起来像这样。
public abstract class BasePageWeb {
@Value("${projectname.retail.dev}")
private String devUrl;
@Value("${projectname.retail.sit}")
private String sitUrl;
@Value("${projectname.retail.uat}")
private String uatUrl;
protected WebDriver driver;
public WebDriverWait wait;
protected final WebDriverInterface webDriverInterface;
public BasePageWeb(WebDriverInterface webDriverInterface) {
this.webDriverInterface = webDriverInterface;
}
// env choices: lcl, dev, sit, uat -> app choices: web, id, emp, cxm -> browser choices: chrome, fire
public void loadAny(String env, String app, String browser) {
if (browser.equals("chrome")) {
driver = this.webDriverInterface.getDriver();
} else if (browser.equals("fire")) {
driver = this.webDriverInterface.getDriverFire();
}
wait = new WebDriverWait(driver, 30);
String url = "";
String title = "";
switch (app) {
case "web":
switch (env) {
case "dev":
url = devUrl;
title = "Log in to Project Name";
break;
case "sit":
url = sitUrl;
title = "Log in to Project Name";
break;
case "uat":
url = uatUrl;
title = "Log in to Project Name";
break;
}
break;
default:
System.out.println("There were no matches to your login choices.");
}
driver.get(url);
wait.until(ExpectedConditions.titleContains(title));
}
}
然后,当我有特定主题时,我可以创建仅适用于该子区域的方法,我会扩展基本页面,并将子页面注入步骤页面。
@Component
public class MyAccountsPage extends BasePageWeb {
public MyAccountsPage(WebDriverInterface webDriverInterface) {
super(webDriverInterface);
}
// Find the Product Title Elements, Convert to Strings, and put them all in a simple List.
public List<String> getAccountInfoTitles(){
List<WebElement> accountInfoTitlesElements =
driver.findElements(By.cssSelector("div > .bb-account-info__title"));
return accountInfoTitlesElements.stream()
.map(WebElement::getText)
.collect(Collectors.toList());
}
}
如果有人能看出我做错了什么,或者提出调查建议,我将不胜感激。我知道在 6.6.0 之后有一些主要的黄瓜变化,关于框架如何扫描注释等,但我无法确定这是否相关。
供参考。 pom.xml 包含所有版本和包含的依赖项。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>java-cucumber-generic</groupId>
<artifactId>java-cucumber-generic-web</artifactId>
<version>1.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<java.version>1.8</java.version>
<cucumber.version>6.6.0</cucumber.version>
<junit.version>4.13</junit.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<port>8358</port>
<cucumber.reporting.version>5.3.1</cucumber.reporting.version>
<cucumber.reporting.config.file>automation-web/src/test/resources/projectname/cucumber-reporting.properties</cucumber.reporting.config.file>
<org.mapstruct.version>1.3.1.Final</org.mapstruct.version>
<h2database.version>1.4.200</h2database.version>
<appium.java.client.version>7.3.0</appium.java.client.version>
<guava.version>29.0-jre</guava.version>
<reporting-plugin.version>4.0.83</reporting-plugin.version>
<commons-text.version>1.9</commons-text.version>
<commons-io.version>2.8.0</commons-io.version>
</properties>
<dependencies>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java8</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- Added beyond original archetype -->
<!-- https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-surefire-plugin -->
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12.4</version>
<scope>test</scope>
</dependency>
<!-- To make Wait Until work -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- Cucumber Reporting -->
<dependency>
<groupId>net.masterthought</groupId>
<artifactId>cucumber-reporting</artifactId>
<version>${cucumber.reporting.version}</version>
</dependency>
<dependency>
<groupId>de.monochromata.cucumber</groupId>
<artifactId>reporting-plugin</artifactId>
<version>${reporting-plugin.version}</version>
</dependency>
<!-- For dependency injection https://cucumber.io/docs/cucumber/state/#dependency-injection -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-spring</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2database.version}</version>
<scope>test</scope>
</dependency>
<!-- To generate getters, setters, equals, hascode, toString methods -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Java client, wrapped by Appium -->
<dependency>
<groupId>io.appium</groupId>
<artifactId>java-client</artifactId>
<version>${appium.java.client.version}</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-text -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>${commons-text.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<encoding>UTF-8</encoding>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<!-- Added beyond original archetype -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12.4</version>
<configuration>
<testFailureIgnore>true</testFailureIgnore>
</configuration>
</plugin>
</plugins>
</build>
</project>
您有两个页面 classes MyAccountsPage
和 MyProfilePage
。虽然两者都扩展了 BasePageWeb
,因此 MyAccountsPage
和 MyProfilePage
的任何实例也是实例 BasePageWeb
,但它们不是同一个实例!
一开始这可能会让人很困惑,因为通常每个 class 只有一个实例,我们将实例和 class 视为同一事物。而是将 class 视为可以制作许多实例的模板。
现在,如果您附加调试器并在使用页面之前检查页面,您应该会看到如下内容:
MyAccountsPage@1001
- WebDriver driver = null <--- field inherited from BasePageWeb
- other fields
MyProfilePage@1002 <--- different memory address, so different instance!
- WebDriver driver = null <--- field inherited from BasePageWeb
- other fields
因此,当您使用 AccountsSteps
中的步骤设置 WebDriver
时,WebDriver is setup in
MyProfilePagebut not
MyProfilePage`。
MyAccountsPage@1001
- WebDriver driver = Webdriver@1003 <-- This one was set.
- other fields
MyProfilePage@1002
- WebDriver driver = null <--- This one is still null.
- other fields
因此,当您尝试使用尝试使用 MyProfilePage
的 ProfileSteps
时,您最终会遇到空指针异常,因为 MyProfilePage
中 WebDriver
的实例是从不设置。
这里有一些解决方案,但它们都归结为通过使 BasePageWeb
成为组件并使用组合而不是继承来将 webdriver 保持在单个实例中。
@Component
@ScenarioScope
public class BasePageWeb {
...
}
public class AccountsSteps {
private final BasePageWeb basePageWeb;
private final MyAccountsPage page;
@Autowired
public AccountsSteps(BasePageWeb basePageWeb, MyAccountsPage page){
this.basePageWeb = basePageWeb;
this.page = page;
}
@Given("Log into {string} on {string} as {string} with {string} using {string}")
public void logIntoOnAsWithUsing(String app, String env, String user, String pass, String browser) {
basePageWeb.loadAny(env, app, browser);
page.sendUsername(user);
page.sendPassword(pass);
page.loginButtonClick();
}
....
@Component
@ScenarioScope
public class MyAccountsPage {
private final BasePageWeb basePageWeb;
public MyAccountsPage(BasePageWeb basePageWeb) {
this.basePageWeb = basePageWeb;
}
...
}
@Component
@ScenarioScope
public class MyProfilePage {
private final BasePageWeb basePageWeb;
public MyProfilePage(BasePageWeb basePageWeb) {
this.basePageWeb = basePageWeb;
}
...
}
我正在使用相当典型的 Maven 架构,Java-Cucumber、Selenium,以及 Spring 依赖注入测试系统设置来测试动态 Angular 前端网站。 (pom.xml 中的版本)ArchitectureWSpringDI 它工作得非常好,我可以轻松地 运行 进行数百次测试,但我无法像使用 Ruby瓦提尔。一篇文章指出 Ruby 有一个 Java 缺少的“世界”对象,但是用于依赖注入的 Spring 应该可以解决这个问题
我读了很多“保留状态”的帖子,但似乎没有任何内容适用于它的工作原理,Cucumber 和 Spring 中有很多落后的版本,尽管我仍在使用 Java 8. 大多数保留状态的帖子似乎都在单个文件中的步骤之间,在单个测试中。
主要示例是众多示例中的一个,我希望能够使用我的@Given I 登录步骤创建一个步骤文件,而不必将该步骤放在其他一百个步骤文件中。
如果我有这样的功能文件:
Feature: As an account holder I examine account details
Scenario: View personal summary widget info on details page
Given Log into "web" on "dev" as "username" with "password" using "chrome"
When I tap the first account section
Then I see a list of transactions
并将其与包含所有步骤的步骤文件匹配,如下所示
@SpringBootTest
public class AccountsSteps {
private final MyAccountsPage page;
@Autowired
public AccountsSteps(MyAccountsPage page){
this.page = page;
}
@Given("Log into {string} on {string} as {string} with {string} using {string}")
public void logIntoOnAsWithUsing(String app, String env, String user, String pass, String browser) {
page.loadAny(env, app, browser);
page.sendUsername(user);
page.sendPassword(pass);
page.loginButtonClick();
}
@When("I tap the first account section")
public void iTapTheFirstAccountSection() {
page.waitForListOfElementType(WebElement);
page.clickFirstAccountLink();
}
@Then("I see a list of transactions")
public void iSeeAListOfTransactions() {
By selector = By.cssSelector("div.container");
page.waitForLocateBySelector(selector);
Assert.assertTrue(page.hasTextOnPage("Account details"));
}
}
一切正常,但如果我有另一个功能使用相同的@Given,那么上面和下面的是准确的,所以它不会在新步骤文件中创建新步骤。
Feature: As an account owner I wish to edit my details
Scenario: My profile loads and verifies the correct member's name
Given Log into "web" on "dev" as "username" with "password" using "chrome"
When I use the link in the Self service drop down for My profile
Then the Contact Details tab loads the proper customer name "Firstname Lastname"
与此步骤文件匹配,请注意缺少给定步骤,因为它使用的是另一个文件中的步骤。
@SpringBootTest
public class MyProfileSteps {
private final MyProfilePage page;
@Autowired
public MyProfileSteps(MyProfilePage page){
this.page = page;
}
@When("I use the link in the Self service drop down for My profile")
public void iUseTheLinkInTheSelfServiceDropDownForMyProfile() {
page.clickSelfServiceLink();
page.clickMyProfileLink();
}
@Then("the Contact Details tab loads the proper customer name {string}")
public void theContactDetailsTabLoadsTheCustomerName(String fullName) {
System.out.println(page.getCustomerNameFromProfile().getText());
Assert.assertTrue(page.getCustomerNameFromProfile().getText().contains(fullName));
page.teardown();
}
}
我终于找到了问题的症结所在。切换到不同步骤文件中的步骤时,它会抛出异常。
When I use the link in the Self service drop down for My profile
java.lang.NullPointerException
at java.util.Objects.requireNonNull(Objects.java:203)
at org.openqa.selenium.support.ui.FluentWait.<init>(FluentWait.java:106)
at org.openqa.selenium.support.ui.FluentWait.<init>(FluentWait.java:97)
at projectname.pages.BasePage.waitForClickableThenClickByLocator(BasePage.java:417)
at projectname.pages.BasePageWeb.clickSelfServiceLink(BasePageWeb.java:858)
at projectname.steps.MyProfileSteps.iUseTheLinkInTheSelfServiceDropDownForMyProfile(MyProfileSteps.java:39)
at ✽.I use the link in the drop down for My profile(file:///Users/name/git/project/tests/projectname/src/test/resources/projectname/features/autocomplete/my_profile.feature:10)
我特意将它们全部绑定在一起,因此测试每次只调用一个新的 Selenium 实例,而且它绝对不会打开新的浏览器 window,它只会崩溃并关闭。
public interface WebDriverInterface {
WebDriver getDriver();
WebDriver getDriverFire();
void shutdownDriver();
WebDriver stopOrphanSession();
}
并且有几个配置文件会 运行 不同的配置,但我主要的本地测试 WebDriverInterface 看起来像这样。
@Profile("local")
@Primary
@Component
public class DesktopLocalBrowsers implements WebDriverInterface {
@Value("${browser.desktop.width}")
private int desktopWidth;
@Value("${browser.desktop.height}")
private int desktopHeight;
@Value("${webdriver.chrome.mac.driver}")
private String chromedriverLocation;
@Value("${webdriver.gecko.mac.driver}")
private String firefoxdriverLocation;
public WebDriver local;
public WebDriver local2;
public DesktopLocalBrowsers() {
}
@Override
public WebDriver getDriver() {
System.setProperty("webdriver.chrome.driver", chromedriverLocation);
System.setProperty("webdriver.chrome.silentOutput", "true");
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.addArguments("--disable-extensions");
chromeOptions.addArguments("window-size=" + desktopWidth + "," + desktopHeight);
local = new ChromeDriver(chromeOptions);
return local;
}
@Override
public WebDriver getDriverFire() {
System.setProperty("webdriver.gecko.driver", firefoxdriverLocation);
FirefoxBinary firefoxBinary = new FirefoxBinary();
FirefoxOptions firefoxOptions = new FirefoxOptions();
firefoxOptions.setLogLevel(FirefoxDriverLogLevel.FATAL);
firefoxOptions.setBinary(firefoxBinary);
local2 = new FirefoxDriver(firefoxOptions);
return local2;
}
@Override
public void shutdownDriver() {
try{
local.quit();
}catch (NullPointerException npe){
local2.quit();
}
}
public WebDriver stopOrphanSession(){
try{
if(local != null){
return local;
}
}catch (NullPointerException npe){
System.out.println("All Drivers Closed");
}
return local2;
}
}
我有相当标准的跑步者。我已经尝试了 Cucumber Runner 的几种变体,移动到不同的目录,使用胶水和 extraglue 配置,但要么什么都没有改变,要么我完全打破它。这是现在正在运行的那个。
@RunWith(Cucumber.class)
@CucumberOptions(features = "src/test/resources/projectname/features/",
glue = "backbase",
// extraGlue = "common", // glue and extraGlue cannot be used together
plugin = {
"pretty",
"summary",
"de.monochromata.cucumber.report.PrettyReports:target/cucumber",
})
public class RunCucumberTest {
}
和我的 Spring 亚军
@RunWith(SpringRunner.class)
@CucumberContextConfiguration
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class SpringContextRunner {
}
以及开箱即用的应用程序页面以供参考。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
为了防止有人发现它对集思广益或诊断有用,我的页面对象从 BasePage 开始,它变得太大了,因为它包含所有常用方法,但看起来像这样。
public abstract class BasePageWeb {
@Value("${projectname.retail.dev}")
private String devUrl;
@Value("${projectname.retail.sit}")
private String sitUrl;
@Value("${projectname.retail.uat}")
private String uatUrl;
protected WebDriver driver;
public WebDriverWait wait;
protected final WebDriverInterface webDriverInterface;
public BasePageWeb(WebDriverInterface webDriverInterface) {
this.webDriverInterface = webDriverInterface;
}
// env choices: lcl, dev, sit, uat -> app choices: web, id, emp, cxm -> browser choices: chrome, fire
public void loadAny(String env, String app, String browser) {
if (browser.equals("chrome")) {
driver = this.webDriverInterface.getDriver();
} else if (browser.equals("fire")) {
driver = this.webDriverInterface.getDriverFire();
}
wait = new WebDriverWait(driver, 30);
String url = "";
String title = "";
switch (app) {
case "web":
switch (env) {
case "dev":
url = devUrl;
title = "Log in to Project Name";
break;
case "sit":
url = sitUrl;
title = "Log in to Project Name";
break;
case "uat":
url = uatUrl;
title = "Log in to Project Name";
break;
}
break;
default:
System.out.println("There were no matches to your login choices.");
}
driver.get(url);
wait.until(ExpectedConditions.titleContains(title));
}
}
然后,当我有特定主题时,我可以创建仅适用于该子区域的方法,我会扩展基本页面,并将子页面注入步骤页面。
@Component
public class MyAccountsPage extends BasePageWeb {
public MyAccountsPage(WebDriverInterface webDriverInterface) {
super(webDriverInterface);
}
// Find the Product Title Elements, Convert to Strings, and put them all in a simple List.
public List<String> getAccountInfoTitles(){
List<WebElement> accountInfoTitlesElements =
driver.findElements(By.cssSelector("div > .bb-account-info__title"));
return accountInfoTitlesElements.stream()
.map(WebElement::getText)
.collect(Collectors.toList());
}
}
如果有人能看出我做错了什么,或者提出调查建议,我将不胜感激。我知道在 6.6.0 之后有一些主要的黄瓜变化,关于框架如何扫描注释等,但我无法确定这是否相关。
供参考。 pom.xml 包含所有版本和包含的依赖项。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>java-cucumber-generic</groupId>
<artifactId>java-cucumber-generic-web</artifactId>
<version>1.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<java.version>1.8</java.version>
<cucumber.version>6.6.0</cucumber.version>
<junit.version>4.13</junit.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<port>8358</port>
<cucumber.reporting.version>5.3.1</cucumber.reporting.version>
<cucumber.reporting.config.file>automation-web/src/test/resources/projectname/cucumber-reporting.properties</cucumber.reporting.config.file>
<org.mapstruct.version>1.3.1.Final</org.mapstruct.version>
<h2database.version>1.4.200</h2database.version>
<appium.java.client.version>7.3.0</appium.java.client.version>
<guava.version>29.0-jre</guava.version>
<reporting-plugin.version>4.0.83</reporting-plugin.version>
<commons-text.version>1.9</commons-text.version>
<commons-io.version>2.8.0</commons-io.version>
</properties>
<dependencies>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java8</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- Added beyond original archetype -->
<!-- https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-surefire-plugin -->
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12.4</version>
<scope>test</scope>
</dependency>
<!-- To make Wait Until work -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- Cucumber Reporting -->
<dependency>
<groupId>net.masterthought</groupId>
<artifactId>cucumber-reporting</artifactId>
<version>${cucumber.reporting.version}</version>
</dependency>
<dependency>
<groupId>de.monochromata.cucumber</groupId>
<artifactId>reporting-plugin</artifactId>
<version>${reporting-plugin.version}</version>
</dependency>
<!-- For dependency injection https://cucumber.io/docs/cucumber/state/#dependency-injection -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-spring</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2database.version}</version>
<scope>test</scope>
</dependency>
<!-- To generate getters, setters, equals, hascode, toString methods -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Java client, wrapped by Appium -->
<dependency>
<groupId>io.appium</groupId>
<artifactId>java-client</artifactId>
<version>${appium.java.client.version}</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-text -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>${commons-text.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<encoding>UTF-8</encoding>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<!-- Added beyond original archetype -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12.4</version>
<configuration>
<testFailureIgnore>true</testFailureIgnore>
</configuration>
</plugin>
</plugins>
</build>
</project>
您有两个页面 classes MyAccountsPage
和 MyProfilePage
。虽然两者都扩展了 BasePageWeb
,因此 MyAccountsPage
和 MyProfilePage
的任何实例也是实例 BasePageWeb
,但它们不是同一个实例!
一开始这可能会让人很困惑,因为通常每个 class 只有一个实例,我们将实例和 class 视为同一事物。而是将 class 视为可以制作许多实例的模板。
现在,如果您附加调试器并在使用页面之前检查页面,您应该会看到如下内容:
MyAccountsPage@1001
- WebDriver driver = null <--- field inherited from BasePageWeb
- other fields
MyProfilePage@1002 <--- different memory address, so different instance!
- WebDriver driver = null <--- field inherited from BasePageWeb
- other fields
因此,当您使用 AccountsSteps
中的步骤设置 WebDriver
时,WebDriver is setup in
MyProfilePagebut not
MyProfilePage`。
MyAccountsPage@1001
- WebDriver driver = Webdriver@1003 <-- This one was set.
- other fields
MyProfilePage@1002
- WebDriver driver = null <--- This one is still null.
- other fields
因此,当您尝试使用尝试使用 MyProfilePage
的 ProfileSteps
时,您最终会遇到空指针异常,因为 MyProfilePage
中 WebDriver
的实例是从不设置。
这里有一些解决方案,但它们都归结为通过使 BasePageWeb
成为组件并使用组合而不是继承来将 webdriver 保持在单个实例中。
@Component
@ScenarioScope
public class BasePageWeb {
...
}
public class AccountsSteps {
private final BasePageWeb basePageWeb;
private final MyAccountsPage page;
@Autowired
public AccountsSteps(BasePageWeb basePageWeb, MyAccountsPage page){
this.basePageWeb = basePageWeb;
this.page = page;
}
@Given("Log into {string} on {string} as {string} with {string} using {string}")
public void logIntoOnAsWithUsing(String app, String env, String user, String pass, String browser) {
basePageWeb.loadAny(env, app, browser);
page.sendUsername(user);
page.sendPassword(pass);
page.loginButtonClick();
}
....
@Component
@ScenarioScope
public class MyAccountsPage {
private final BasePageWeb basePageWeb;
public MyAccountsPage(BasePageWeb basePageWeb) {
this.basePageWeb = basePageWeb;
}
...
}
@Component
@ScenarioScope
public class MyProfilePage {
private final BasePageWeb basePageWeb;
public MyProfilePage(BasePageWeb basePageWeb) {
this.basePageWeb = basePageWeb;
}
...
}