使用spring依赖注入的javacumber测试在特性跨越step类时抛出nullpointerexception

r7xajy2e  于 2021-07-13  发布在  Java
关注(0)|答案(1)|浏览(329)

我正在使用一个相当典型的maven架构,javacumber,selenium,以及spring依赖注入测试系统来测试一个动态的前端网站(在pom.xml)架构中的版本它工作得非常好,我可以很容易地运行数百个测试,但是我不能像使用ruby watir那样“干燥”测试步骤。一篇文章指出ruby有一个java缺少的“world”对象,但是用于依赖注入的spring应该可以解决这个问题
我读过很多“retaining state”的帖子,但似乎没有什么适用于它的工作原理,很多都是cucumber和spring的几个版本,尽管我仍然在使用java8。大多数用于保留状态的post似乎都在单个文件中的步骤之间,在单个测试中。
主要的例子是,我希望能够有一个带有@given i login步骤的steps文件,而不必将该步骤放在其他一百个step文件中。
如果我有这样一个功能文件:

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

并将其与包含所有步骤的steps文件匹配,如下所示

@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的几个变体,使用glue和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>
f87krz0w

f87krz0w1#

你有两个页面类 MyAccountsPage 以及 MyProfilePage . 当两者都延伸时 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

所以当你设置 WebDriver 使用中的步骤 AccountsSteps ,的 WebDriver is setup in MyProfile页面 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

所以当你尝试使用 ProfileSteps 试图利用 MyProfilePage 最终会出现空指针异常,因为 WebDriverMyProfilePage 从未设置。
这里有一些解决方案,但它们都归结为通过 BasePageWeb 使用组合而不是继承的组件。

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

相关问题