Smoke Test Your Environment With a PowerShell DSL and Selenium ChromeDriver

2. November 2012 13:50 by Administrator in PowerShell, DevOps  //  Tags:   //   Comments (0)

You can do just about anything in PowerShell and recently I've been looking how I can create a nice DSL in PowerShell in order to run automated Selenium browser tests post deployment.

It's very common to use Selenium to create acceptance tests but the key here is I'm creating a suite of smoke tests that can be run against any environment (including live) which are designed to be very quick.

What is a smoke test?

A smoke test is designed to be a quick test of the target system to ensure that the application is operable and doesn't "smoke" when being used.  

Here's some examples that I run:

  • Does the deployed build number match the current version being deployed?
  • Does the homepage load and match a given title?
  • Can I login into the application?
  • Is the secure area actually secure (SSL)?
  • Can I hit a page that uses a service which is known to cause problems? 

About my technology selection

Why PowerShell and not C#?  Because I can!...Our deployment scripts are heavily based on PowerShell, yes I could have integrated .Net and perhaps distributed NUnit but with the PowerShell DSL it's easy for a tester to open the tests and add/modify without the need for Visual Studio and compiling.

Why Chrome and not IE or Firefox?  Well IE is just horrible and you have to fiddle with trust zones to make Selenium work properly.  I did use Firefox initially but with all the updates and popups it wasn't very reliable and ignoring SSL certificates seemed to be tricky as well although possible eventually.  Chrome makes it very easy to ignore invalid SSL certificates for lower environment testing and is much more stable in terms of popups etc.

Getting Started

For this example I'm using WebDriver 2.25.1 and more importantly ChromeDriver.exe 22.0.1203b.  I had a lot of problems using ChromeDriver.exe 23.0.1240.0 which I'll detail later.

I use some C# classes to store the results (this was bit of an experiment) so you'll need to update PowerShell to use .Net 4:

In the following folder %windir%\System32\WindowsPowerShell\v1.0 create a new file called powershell.exe.config with the following contents:

<?xml version="1.0"?> 
<configuration> 
    <startup useLegacyV2RuntimeActivationPolicy="true"> 
        <supportedRuntime version="v4.0.30319"/> 
        <supportedRuntime version="v2.0.50727"/> 
    </startup> 
</configuration> 
  1. Create the following files and folder structure:
    c:\SmokeTests\RunSmokeTests.ps1 
    c:\SmokeTests\SmokeTestDSL.psm1 
    c:\SmokeTests\Tools\WebDriver
  2. Download WebDriver 2.25.1 from http://selenium.googlecode.com/files/selenium-dotnet-2.25.1.zip
    Extract net40 into c:\SmokeTests\Tools\WebDriver
  3. Download ChromeDriver.exe from http://code.google.com/p/chromedriver/downloads/list into c:\SmokeTests\Tools\WebDriver 

Create the DSL

Edit c:\SmokeTests\SmokeTestDSL.psm1 and add the following:

Add-Type @'
using System.Collections.Generic;

public class SmokeTestResults
{
 private IList<SmokeTestResult> smokeTestResults = new List<SmokeTestResult>();

 public IList<SmokeTestResult> Results { 
 get { return smokeTestResults; }
 } 

 public int Errors {get; set;} 
}

public class SmokeTestResult
{
 public string StoryTitle {get; set;} 
 public bool Passed {get; set;} 
 public string ErrorMessage {get; set;}
}
'@    

$script:driver = $null
$script:baseUrl = $null
$script:buildNumber = $null
$script:smokeTestResults = $null
$script:testsAsWarnings = $true
	
function InitializeSmokeTests() {
	param(
		[ValidateNotNull()]
		$baseUrl,
		[ValidateNotNull()]
		$buildNumber,
		[ValidateNotNull()]
		$scriptBlockToExecute,
		[bool]
		$testsAsWarnings
	)
	
	Write-Host "Initializing smoke tests..."
	$webDriverDir = "$PSScriptRoot\tools\WebDriver"

	ls -Name "$webDriverDir\*.dll"	|
	foreach { Add-Type -Path "$webDriverDir\$_" }
	
	$script:testsAsWarnings = $testsAsWarnings;

	$capabilities = New-Object OpenQA.Selenium.Remote.DesiredCapabilities
	$capabilityValue = @("--ignore-certificate-errors")
	$capabilities.SetCapability("chrome.switches", $capabilityValue)
	$options = New-Object OpenQA.Selenium.Chrome.ChromeOptions
	$script:driver = New-Object OpenQA.Selenium.Chrome.ChromeDriver($options)
	$script:driver.Manage().Timeouts().ImplicitlyWait([System.TimeSpan]::FromSeconds(20)) | Out-Null
	
	$script:buildNumber = $buildNumber	
	
	if (!$baseUrl.StartsWith("http", [System.StringComparison]::InvariantCultureIgnoreCase)) {
		$baseUrl = "http://" + $baseUrl
	}
	if (!$baseUrl.EndsWith("/")) {
		$baseUrl = $baseUrl + "/"
	}	
	$script:baseUrl = $baseUrl
	$script:smokeTestResults = New-Object SmokeTestResults
	
	WriteWarningOrBubbleException $scriptBlockToExecute

	return $script:smokeTestResults
}

function WriteWarningOrBubbleException([scriptblock] $testScriptToExecute){
	try 
	{
		& $testScriptToExecute
	}
	catch 
	{
		if ($script:testsAsWarnings) 
		{
			$theErrorMessage = $_
			Write-Warning $theErrorMessage
		}
		else 
		{
			throw
		}
	} 
	finally 
	{
		DisposeSmokeTests
	}
}

function ExecuteCommand($storyName, [ScriptBlock]$testScript){
	$result = New-Object SmokeTestResult
	$result.StoryTitle = $storyName
	$result.Passed = $false
	try 
	{
		if ($script:driver -ne $null) {
			& $testScript			
		}
		$result.Passed = $true
	}
	catch{
		$result.ErrorMessage = $_
	}
	finally{
		$script:smokeTestResults.Results.Add($result)
	}
}

function DisposeSmokeTests() {
	if ($script:driver -ne $null) { 
		$script:driver.Quit()
	}
}

function UrlBuilder($path){	
	return $baseUrl + $path
}

function When($name, [ScriptBlock] $fixture){
	Write-Host "-------------------------------------------------------------"
	Write-Host $name
	Write-Host "-------------------------------------------------------------"
	ExecuteCommand $name $fixture
}


function NavigateTo([string]$navigateTo){
	if ($script:driver -ne $null) { 
		$url = UrlBuilder $navigateTo

		Write-Host "Navigating to $url"
		$script:driver.Navigate().GoToUrl($url) 
	}
}

function TypeIntoField(){
	param(
		[ValidateNotNull()]
		$fieldID,
		[ValidateNotNull()]
		$keys
	)

	if ($script:driver -ne $null) { 
		Write-Host "Typing $keys into $fieldID"
		$script:driver.FindElement([OpenQA.Selenium.By]::Id($fieldID)).SendKeys($keys)
	}
}

function Click($fieldID){
	if ($script:driver -ne $null) { 
		Write-Host "Clicking $fieldID"
		$script:driver.FindElement([OpenQA.Selenium.By]::Id($fieldID)).Click()
	}
}

function ValidateFieldExists($fieldID){
	if ($script:driver -ne $null) { 
		if ($script:driver.FindElement([OpenQA.Selenium.By]::Id($fieldID)) -ne $null) {
			Write-Host "Validated $fieldID exists"
		}
		else {
			Throw "$fieldID doesn't exist"
		}
	}
}

function ValidatePageHasTitle() {
	Param(
		[Parameter(Mandatory=$true)] 
		$titleToValidate
	)

	if ($script:driver -ne $null) {
		if(!($titleToValidate -ieq $script:driver.Title)){
			Throw "$($script:driver.Title) doesn't contain $titleToValidate"
		}
		
		Write-Host "Validated page has title $titleToValidate"
	}
}

function ValidateIsSecureRequest(){
	$currentURL = New-Object System.Uri($script:driver.Url)
	$isSecure = ($currentURL.Scheme -ieq "https")
	
	if(!($isSecure)){
		throw "$currentURL is not using HTTPS"
	}
	Write-Host "Validated request is using HTTPS"
}

 

Create a runner

Edit c:\SmokeTests\RunSmokeTests.psm1 and add the following:

$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
Import-Module "$scriptPath\SmokeTestDSL.psm1"

function ExecuteSmokeTests([bool]$testsAsWarnings, $baseUrl, $buildNumber, [string]$smokeTests){
	$scriptBlockToExecute = [scriptblock]::Create($smoketests)
	$smokeTestResults = InitializeSmokeTests $baseUrl $buildNumber $scriptBlockToExecute $testsAsWarnings	

	return $smokeTestResults		
}

function GenerateSummary($results){
	$results | foreach {
				Write-Host $_.Name
				$Host.ui.rawui.ForegroundColor = "white"
				$columns = `
					@{Expression={if ($_.Passed -eq "Passed") { $Host.ui.rawui.ForegroundColor = "green" ; $_.Passed } else{ $Host.ui.rawui.foregroundcolor = "red" ; $_.Passed } };label="Passed";width=6;align='left'}, `
					@{Expression={$_.Storytitle};label="Story";width=80}, `
					@{Expression={$_.ErrorMessage};label="Error Message";width=30}
				
				$results.Results | Format-Table $columns -Wrap
				$Host.ui.rawui.ForegroundColor = "white"
			}
}

function Main(){
	$smokeTests = {
		When "I go to the SSL page, verify title and check is secure" {
			NavigateTo "sslcheck.html"
			ValidatePageHasTitle "Fortify - SSL Encryption Check"
			ValidateIsSecureRequest
		}
		
		When "I search for SSL results should be displayed" {
			NavigateTo "sslcheck.html"
			TypeIntoField "sbi" "ssl certs"
			Click "sbb"
			ValidateFieldExists "tpab1"
		}		
	}

	$results = ExecuteSmokeTests $true "https://www.fortify.net" 123 $smokeTests
	GenerateSummary $results
}

Main

 NOTES

- I pass in a build number but it's not used in this example, in my real deployment a build number is passed in from Team City which is validated against a known file on the target site which contains the actual build number.  As a quick test of a node in the webfarm if it doesn't have the expected build number then something has gone badly wrong!

Run the tests!

From a PowerShell prompt run the tests:

 

 

 

When you run the tests you should see Chrome fire up and navigate through the stories.

I mentioned that the WebDriver and ChromeDriver versions are important so if you see the following error:
ERROR:ipc_sync_channel.cc(378)] Canceling pending sends

Make sure you check the ChromeDriver version, for me the latest version at the time of writing did not work and I had to use 1203b.

Summary

Using PowerShell and Selenium allows you to create a flexible framework of smoke tests that you can incorporate into your build pipeline to quickly spot problems before the users do.  By creating a DSL non technical users such as testers can add their own tests to the suite as they see fit.

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading