Selenium+TestNG中工具类继承基类触发NullPointerException问题
我在Selenium+TestNG自动化测试框架里定义了BaseSeleniumTest基类,里面初始化了若干protected变量和通用方法。测试类、页面对象类继承这个基类后,能正常访问基类的protected变量。但当我创建继承该基类的CreationDialogUtils工具类,在其中调用基类的checkInvoicePage()方法时,areaOfBusiness等变量为null,触发了NullPointerException。调试发现,从工具类进入基类时,所有变量都是null。
基类代码
public class BaseSeleniumTest { private static final Logger logger = LoggerFactory.getLogger(BaseSeleniumTest.class); protected String browserName; protected String tenantName; protected String userRole; protected String testType; protected AreaOfBusiness areaOfBusiness; protected static ThreadLocal<WebDriver> driver = new ThreadLocal<>(); protected static ThreadLocal<WebDriverWait> wait = new ThreadLocal<>(); protected static ThreadLocal<WebDriverWait> longWait = new ThreadLocal<>(); protected BasePage basePage; @Parameters({"profile", "browserName", "userRole", "testType", "areaOfBusiness", "tenantName"}) @BeforeTest(alwaysRun = true) public void setup(@Optional("local") String profile, @Optional("chrome") String browserName, @Optional String userRole, @Optional String testType, @Optional("BMW") AreaOfBusiness areaOfBusiness, @Optional("Autohaus Huber") String tenantName) throws IOException { this.browserName = browserName; this.userRole = userRole; this.testType = testType; this.tenantName = tenantName; this.areaOfBusiness = areaOfBusiness; ConfigurationManager.loadProperties(profile); driver.set(createBrowserSession(browserName)); wait.set(getWait()); longWait.set(getLongWait()); navigateToBackOffice(driver.get(), areaOfBusiness); //Login process // Instantiate page objects } @Parameters({"browserName", "userRole", "testType", "areaOfBusiness", "tenantName"}) @BeforeClass(alwaysRun = true) public void beforeClassInitialization(@Optional("chrome") String browserName, @Optional String userRole, @Optional String testType, @Optional("BMW") AreaOfBusiness areaOfBusiness, @Optional("Autohaus Huber") String tenantName) { this.browserName = browserName; this.userRole = userRole; this.testType = testType; this.tenantName = tenantName; this.areaOfBusiness = areaOfBusiness; this.basePage = new BasePage(driver.get()); } protected WebDriverWait getWait() { return new WebDriverWait(driver.get(), Duration.ofSeconds(10)); } protected WebDriverWait getLongWait() { return new WebDriverWait(driver.get(), Duration.ofSeconds(300)); } public WebDriver createBrowserSession(String browser) { some code.... return webDriver; } @AfterTest(alwaysRun = true) public void tearDown() { if (driver.get() != null) { logger.info("TearDown Called!"); logger.info("Browser Closed."); driver.get().quit(); } } public void checkErrorPage() { String errorPage = MessageFormat.format(ConfigurationManager.getBackOfficeBaseUrl(), areaOfBusiness.getValue()) + ERROR_PAGE; if (errorPage.equals(driver.get().getCurrentUrl())) { logger.info("Error page occurred, redirecting to base URL"); String redirectToBaseUrl = MessageFormat.format(ConfigurationManager.getBackOfficeBaseUrl(), areaOfBusiness.getValue()); driver.get().navigate().to(redirectToBaseUrl); } } }
测试类代码
public class CreateNewInvoiceFieldsValidationTest extends BaseSeleniumTest { protected InvoiceCreationPage invoiceCreation; protected ValidationErrorPage validationErrorPage; protected CreationDialogUtils creationDialogUtils; @BeforeClass(alwaysRun = true) public void createNewInvoiceFieldsValidationBeforeClass() { invoiceCreation = new InvoiceCreationPage(driver.get()); validationErrorPage = new ValidationErrorPage(driver.get()); creationDialogUtils = new CreationDialogUtils(); } @Test(description = "To test if the user does not check any of the title checks box and verify validation message" + "must be displayed") public void tc019NewCaseFieldTitleValidationTest() throws Exception { creationDialogUtils.openInvoiceCreationDialogBox(); //Invoice creation form //Customer data var customerData = randomCustomerData(); customerData.setTitleIndexValue(4); creationDialogUtils.fillCustomerData(invoiceCreation, customerData); //Invoice data var invoiceData = invoiceData(); creationDialogUtils.fillInvoiceDetails(invoiceCreation, invoiceData, CREATE, INVOICE_CREATION); //Click send invoice button invoiceCreation.clickSendButton(); //Verify validation error message var errorMessageData = validationErrors(getErrorMessage("requiredError", "Title")); checkValidationErrors(validationErrorPage, errorMessageData, "title"); //Close the form invoiceCreation.clickCancelButton(); }
工具类代码
public class CreationDialogUtils extends BaseSeleniumTest { protected InvoiceCreationPage invoiceCreation; protected OfferPage offerCreation; protected RepairOrderPage repairOrder; public CreationDialogUtils() { invoiceCreation = new InvoiceCreationPage(driver.get()); offerCreation = new OfferPage(driver.get()); repairOrder = new RepairOrderPage(driver.get()); } //some methods .... // 出现问题的方法 public void openInvoiceCreationDialogBox() { checkInvoicePage(); invoiceCreation.clickFormCloseButton(); invoiceCreation.clickCreateInvoiceButton(); } }
问题原因与解决办法
原因
TestNG的@BeforeTest、@BeforeClass等注解仅作用于带有@Test注解的测试类,不会触发普通工具类的基类初始化方法。通过new CreationDialogUtils()创建工具类实例时,基类的setup()和beforeClassInitialization()完全没执行,导致areaOfBusiness等非静态成员变量全部为null;虽然driver是static ThreadLocal,但工具类的非静态依赖变量仍未初始化。另外,工具类继承测试基类本身就是不合理的设计——测试基类是为测试生命周期服务的,工具类应该是独立的功能封装,不该耦合测试生命周期逻辑。
解决办法
方案1:重构工具类,通过构造方法传入依赖(推荐)
取消工具类对测试基类的继承,改为通过构造方法传入所需的测试类实例或依赖变量:
public class CreationDialogUtils { private final InvoiceCreationPage invoiceCreation; private final BaseSeleniumTest testInstance; // 传入测试类实例和页面对象 public CreationDialogUtils(BaseSeleniumTest testInstance, InvoiceCreationPage invoiceCreation) { this.testInstance = testInstance; this.invoiceCreation = invoiceCreation; } public void openInvoiceCreationDialogBox() { testInstance.checkInvoicePage(); // 调用测试类的基类方法 invoiceCreation.clickFormCloseButton(); invoiceCreation.clickCreateInvoiceButton(); } // 其他方法同理,需要基类变量或方法时通过testInstance调用 }
测试类中初始化工具类时传入当前测试实例:
@BeforeClass(alwaysRun = true) public void createNewInvoiceFieldsValidationBeforeClass() { invoiceCreation = new InvoiceCreationPage(driver.get()); validationErrorPage = new ValidationErrorPage(driver.get()); creationDialogUtils = new CreationDialogUtils(this, invoiceCreation); // 传入当前测试类实例 }
方案2:抽取通用方法到独立工具类
把checkInvoicePage()这类通用逻辑从测试基类中抽离,放到独立的工具类中,让测试类和CreationDialogUtils直接调用:
public class SeleniumUtils { private static final Logger logger = LoggerFactory.getLogger(SeleniumUtils.class); public static void checkInvoicePage(AreaOfBusiness areaOfBusiness, WebDriver driver) { String errorPage = MessageFormat.format(ConfigurationManager.getBackOfficeBaseUrl(), areaOfBusiness.getValue()) + ERROR_PAGE; if (errorPage.equals(driver.getCurrentUrl())) { logger.info("Error page occurred, redirecting to base URL"); String redirectToBaseUrl = MessageFormat.format(ConfigurationManager.getBackOfficeBaseUrl(), areaOfBusiness.getValue()); driver.navigate().to(redirectToBaseUrl); } } }
工具类中调用该方法时传入所需参数:
public void openInvoiceCreationDialogBox(AreaOfBusiness areaOfBusiness, WebDriver driver) { SeleniumUtils.checkInvoicePage(areaOfBusiness, driver); invoiceCreation.clickFormCloseButton(); invoiceCreation.clickCreateInvoiceButton(); }
方案3:手动调用基类初始化方法(不推荐)
如果坚持让工具类继承基类,需要在创建实例时手动调用基类的初始化方法并传入参数:
// 测试类中初始化工具类 creationDialogUtils = new CreationDialogUtils(); // 手动传入测试类中已初始化的参数 creationDialogUtils.beforeClassInitialization(browserName, userRole, testType, areaOfBusiness, tenantName);
这种方法容易导致重复初始化、资源冲突,仅作为临时应急方案。
内容的提问来源于stack exchange,提问作者Raza Sultan

