繁体   English   中英

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

[英]Java-Cucumber tests using spring dependency injection throws NullPointerException when Feature spans Step classes

I am using a fairly typical Maven architecture, Java-Cucumber, Selenium, with Spring Dependency Injection test system set up to test a dynamic Angular front end website. (pom.xml 中的版本) ArchitectureWSpringDI它工作得非常好,我可以轻松运行数百个测试,但我不能像使用 Ruby Watir 那样“干”出测试步骤。 One article states that Ruby has a "world" object that Java is lacking, but the Spring used for Dependency Injection is supposed to solve that

I've read a lot of "retaining state" posts, but nothing seems to apply to how this works, and a lot are several versions behind in Cucumber and Spring, though I am still using Java 8. Most of posts for retaining state seem在单个文件中的步骤之间,在单个测试中。

主要示例是其中之一,我希望能够使用我的@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 实例,而且它绝对不是打开新的浏览器 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 的几种变体,使用胶水和外胶配置移动到不同的目录中,但要么没有任何变化,要么我完全破坏了它。 这是现在正在工作的那个。

@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 之后有一些主要的 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的实例,但它们不是同一个实例!

最初这可能会让人很困惑,因为通常每个 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 MyProfilePage but 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

因此,当您尝试使用尝试使用MyProfilePageProfileSteps时,您最终会遇到 null 指针异常,因为从未设置过MyProfilePage中的WebDriver实例。

这里有一些解决方案,但它们都归结为通过使BasePageWeb成为组件并使用组合而不是 inheritance 来将 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;
    }
    ...
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM