Search code examples
powershellselenium-webdriverscopepester

PowerShell module functions cannot access variables in caller's scope


I'm using Pester with Selenium WebDriver. WebDriver is initialized in 'BeforeAll' block within corresponding 'Describe' block and the resulting instance is assigned to $driver variable. Then, in 'Describe' and 'It' block I call my custom functions that reside in external PowerShell module that is autoloaded with PowerShell. I expect that these functions have access to $driver variable defined in 'BeforeAll' block, but it does not happen and I get the following error message:

RuntimeException: You cannot call a method on a null-valued expression.

Here is the code from Search.Tests.ps1 Pester script:

Describe "Search for something" -Tag something {    

BeforeAll {
    $driver = New-WebDriver
    $driver.Navigate().GoToUrl('http://example.com')
}

AfterAll {
    $driver.Close()
    $driver.Dispose()
    $driver.Quit()
}

Find-WebElement -Selector ('some_selector')

    It "Something is found in search results" {
        GetTextFrom-WebElement -Selector ('some_selector') | Should Be 'something'
    }
}

Find-WebElement and GetTextFrom-WebElement are helper functions that use $driver to get element by CSS and extract element's inner text.

I investigated the issue and found a workaround, but I don't think it's an elegant way to go. The workaround is to redefine $driver in each helper function in the external PowerShell module right after the param block like this:

$driver = $PSCmdlet.GetVariableValue('driver')

This way the functions can see $driver and everything works.

My question: is it possible to do something, so the functions always have access to $driver without a need to redefine driver in each of them?


Solution

  • "I expect that these functions [defined in a PowerShell module] have access to $driver variable defined in 'BeforeAll' block..."

    They don't, and you probably shouldn't rely on that behaviour even if they did.

    Variables Defined in Pester Scriptblocks Aren't Accessible from Modules

    Variables defined in BeforeAll{},BeforeEach{},Context{}, and It{} blocks are not accessible from a module under test when the x.Tests.ps1 file is invoked by Invoke-Pester (reference). If the x.Tests.ps1 file happens to be invoked directly (ie. by pressing F5 in ISE) then variables defined in BeforeAll{} are accessible from a module under test. Relying on that behavior precludes that test from running in bigger batches, so should be avoided.

    Reliance of Implicit Accessibility of External Variables Should be Avoided

    It seems like your custom module expects that $driver is defined somewhere outside the module and is implicitly accessible from inside the module. That raises the following question: Where did the author of the custom module intend $driver to be defined? As a script variable in the module? As a global variable? Both of those are pretty awkward public interfaces for a module because it is difficult to control whether the correct value for $driver is indeed available to the module. If the module does rely on such behavior, I suggest changing the custom module to explicitly accept your $driver object (or at least the information required to create that object).

    If you cannot change the custom module, you might be able to get by with changing your variable references from $driver to $global:driver. You should really try to avoid that though, because using global variables in that way will likely result in any of a variety of problems at some point.