当功能跨越 Step 类时,使用 spring 依赖注入(inject)的 Java-Cucumber 测试会抛出 NullPointerException

标签 java spring selenium dependency-injection cucumber

我正在使用相当典型的 Maven 架构、Java-Cucumber、Selenium,并设置 Spring 依赖注入(inject)测试系统来测试动态 Angular 前端网站。 (pom.xml 中的版本)ArchitectureWSpringDI它工作得非常好,我可以轻松运行数百个测试,但我无法像使用 Ruby Watir 那样“干燥”测试步骤。一篇文章指出 Ruby 拥有 Java 所缺乏的“世界”对象,但用于依赖注入(inject)的 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 实例,并且它绝对不会打开一个新的浏览器窗口,它只是崩溃并关闭。

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 {

}

还有我的 Spring Runner

@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));
    }
}

然后,当我有特定主题时,我可以创建仅适用于该子区域的方法,我会扩展基本页面,并将子页面注入(inject)到“步骤”页面中。

@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 之后,关于框架如何扫描注释等方面发生了一些重大的 Cucumber 更改,但我无法确定这是否相关。

供引用。包含所有版本和包含的依赖项的 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>

最佳答案

您有两个页面类 MyAccountsPageMyProfilePage。虽然两者都扩展了 BasePageWeb,因此 MyAccountsPageMyProfilePage 的任何实例也是 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

因此,当您使用 AccountsSteps 中的步骤设置 WebDriver 时,WebDriver 是在 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

因此,当您尝试使用 ProfileSteps 来尝试使用 MyProfilePage 时,您最终会遇到空指针异常,因为 WebDriver 的实例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;
    }
    ...
}

关于当功能跨越 Step 类时,使用 spring 依赖注入(inject)的 Java-Cucumber 测试会抛出 NullPointerException,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66925972/

相关文章:

java - 如何使用 JAXB 使基类字段成为子类 xml 模式的属性

java - 使用 Java 和 TestNG 使用 WebDriver 在不同的操作系统和浏览器上同时执行测试

java - 我如何控制 Spring 从队列接收的速率?

c# - 如何使用 Selenium WebDriver 将缩放重置为 Chrome 上的默认值?

java - 安装后无法启动 Glassfish,Ubuntu 18.04

java - 如果我们可以简单地覆盖父类(super class)的方法或使用抽象类,为什么还要使用接口(interface)呢?

java - 如何在具有多个 servlet 的 tomcat 中使用 DispatcherServlet

java - 关闭执行程序服务(等待终止时)与等待取消已提交任务(使用 Submit 的 future)之间的比较

python - Selenium (如果在屏幕上定位无法找到元素)

java - 如何在服务员的每次迭代中重新加载页面