java - 当 Feature 跨越 Step 类时,使用 spring 依赖注入的 Java-Cucumber 测试会抛出 NullPointerException
问题描述
我正在使用一个相当典型的 Maven 架构 Java-Cucumber Selenium,并设置了 Spring Dependency Injection 测试系统来测试动态 Angular 前端网站。(pom.xml 中的版本)ArchitectureWSpringDI它工作得非常好,我可以轻松地运行数百个测试,但我不能像使用 Ruby Watir 那样“干”出测试步骤。一篇文章指出 Ruby 有一个 Java 所缺乏的“世界”对象,但用于依赖注入的 Spring 应该可以解决这个问题
我已经阅读了很多“保留状态”的帖子,但似乎没有任何东西适用于它的工作原理,而且 Cucumber 和 Spring 中有很多版本落后,尽管我仍在使用 Java 8。大多数保留状态的帖子似乎在单个文件中的步骤之间,在单个测试中。
主要示例是其中之一,我希望能够使用我的@Given I login 步骤创建一个步骤文件,而不必将该步骤放在其他一百个步骤文件中。
如果我有这样的功能文件:
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"
与此 Step 文件匹配,请注意缺少 Given 步骤,因为它使用的是另一个文件中的步骤。
@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 实例,而且它绝对不会打开一个新的浏览器窗口,它只是崩溃并关闭。
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 的几种变体,使用胶水和外胶配置移动到不同的目录,但要么没有任何变化,要么我完全破坏了它。这是现在正在工作的那个。
@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 {
}
和我的春季赛跑者
@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>
解决方案
您有两个页面类MyAccountsPage
和MyProfilePage
. 虽然两者都扩展,因此and的BasePageWeb
任何实例也是实例,但它们不是同一个实例!MyAccountsPage
MyProfilePage
BasePageWeb
最初这可能会让人感到困惑,因为通常每个类只有一个实例,我们将实例和类视为同一事物。而是将类视为可以从中创建许多实例的模板。
现在,如果您附加调试器并在使用页面之前检查页面,您应该会看到如下内容:
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
因此,当您使用MyProfilePage MyProfilePage 中的WebDriver
步骤设置时。AccountsSteps
WebDriver is setup in
but not
MyAccountsPage@1001
- WebDriver driver = Webdriver@1003 <-- This one was set.
- other fields
MyProfilePage@1002
- WebDriver driver = null <--- This one is still null.
- other fields
因此,当您尝试使用时ProfileSteps
,MyProfilePage
您最终会遇到空指针异常,因为从未设置过WebDriver
in的实例。MyProfilePage
这里有一些解决方案,但它们都归结为通过制作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;
}
...
}
推荐阅读
- amazon-web-services - jupyter notebook - 从命令行运行笔记本(并开始运行单元)并从浏览器访问它
- android - 如何观察基于用户提交按钮的 API 调用,同时从提交操作所必需的编辑文本中获取值?
- node.js - libuv线程在nodejs中顺序执行
- c++ - SSL_write 返回 SSL_ERR_SYSCALL 而 ERR_get_error 和 errno 都是 0
- reactjs - React:理解错误边界的使用
- distributed - Amazon Dynamo:使用默克尔树的反熵副本同步
- php - .ics 文件和 Google 日历之间的时区错误
- java - 编译失败 - 静态变量示例
- php - 在 PHPSpreadhseet 中添加自定义字体
- kubernetes - 如何通过入口在具有相同主机的 Kubernetes 中公开多个应用程序