Automated Testing for WordPress, Part 4: Practical Considerations

In part 3, we saw how to write and run Behat tests. In this final post, I'll discuss some specific implementation problems we encountered and how we were able to solve them.

Authentication

Out of the box, Behat isolates scenarios from each other, i.e. each scenario gets a fresh instance of the context class with a fresh browser session. While this is generally a good thing, it was a little inconvenient in our case, since our site requires users to be authenticated. With complete scenario isolation, we'd have to go through the login process for every scenario, which would be time-consuming and needlessly repetitive.

Tip: The solution I came up with was to store a hash table of authentication cookies in a static class variable keyed by user name. (It has to be static because, as noted above, each scenario gets a fresh context class instance.) That way, you only have to log in as a given user once during your test suite (which also serves as a test of the login page) and the user's authentication tokens can be re-used for subsequent scenarios requiring authentication as that user:

// in FeatureContext.php

// Store session tokens for reauthentication
private static $credentialStore = array();

// ... other code ...

/**
* Authenticates a user with password from configuration.
*
* @Given /^I am logged in as "([^"]*)"$/
*/
public function iAmLoggedInAs($username) {
  if ($authCookie = $this->getCredentials($username)) {
	echo "Reusing authentication tokens for $username";
	$this->setAuthCookie($authCookie);
  }
  else {
	$password = $this->fetchPassword($username);
	$this->doLogin($username, $password);
  }
}

public function doLogin ($username, $password) {

	// Visit login page, enter credentials, submit login form...

	// ... after successful login:

	$this->storeCredentials($username);
}

public function storeCredentials ($username) {
	if ($credential = $this->getAuthCookie()) {
		self::$credentialStore[$username] = $credential;
	}
}

public function getCredentials ($username) {
	return array_key_exists($username, self::$credentialStore) ? self::$credentialStore[$username] : null;
}

public function getAuthCookie () {
	// Return key=>value pair for session authentication token
}

public function setAuthCookie ($authCookie) {
	$this->getSession()->setCookie($authCookie['name'], $authCookie['value']);
}

public function fetchPassword ($username) {
	// Return user's password, looked up from configuration
}

The only slight snafu was that WordPress doesn't use a consistent name for its authentication cookies. (It's usually "wordpress_logged_in_<big hash value>".) "No problem", I thought. "I'll just grab all of the cookies and iterate over them until I find one starting with 'wordpress_logged_in_'", except that the Mink API only allows getting a single cookie by name. There's no way to fetch all of the cookies.

Further complicating matters, each driver has its own way of storing cookies and its own data model to represent them. Some are objects, some are associative arrays, sometimes the values are URL-encoded, sometimes not, etc. (FYI: The value should not be URL-encoded when setting the cookie using the Mink interface's setCookie() method.)

I ended up having to figure out how each driver I wanted to use stores its cookies and implement custom code to retrieve all the cookies for each driver, normalizing the cookie data representation:

public function getAllCookies ($driver) {
	$cookies = array();		

	if ($driver instanceof Behat\Mink\Driver\BrowserKitDriver) {
		$cookies = $this->getBrowserKitCookies($driver);
	}
	else if ($driver instanceof Behat\Mink\Driver\Selenium2Driver) {
		$cookies = $this->getSeleniumCookies($driver);
	}
	else if ($driver instanceof Behat\Mink\Driver\ZombieDriver) {
		$cookies = $this->getZombieCookies($driver);
	}
	else if ($driver instanceof Zumba\Mink\Driver\PhantomJSDriver) {
		$cookies = $this->getPhantomJSCookies($driver);
	}

	return $cookies;
}

private function getBrowserKitCookies ($driver) {
	$cookies = array();

	$cookieJar = $driver->getClient()->getCookieJar();

	foreach ($cookieJar->all() as $cookie) {
		$cookies[] = array(
			"name"=>$cookie->getName(),
			"value"=>$cookie->getValue()
		);		
	}

	return $cookies;
}

private function getSeleniumCookies ($driver) {
	$cookies = array();

	$wdSession = $driver->getWebDriverSession();

	return $this->urlDecodeCookies($wdSession->getAllCookies());
}

private function urlDecodeCookies ($cookiesIn) {
	$cookies = array();
	foreach ($cookiesIn as $cookie) {
		$cookie['value'] = urldecode($cookie['value']);
		$cookies[] = $cookie;
	}

	return $cookies;
}

private function getZombieCookies ($driver) {	

	$cookies = json_decode($driver->getServer()->evalJS("JSON.stringify(browser.cookies);"), true);			
	return $this->urlDecodeCookies($cookies);
}

private function getPhantomJSCookies ($driver) {
	$cookies = array();

	foreach ($driver->getBrowser()->cookies() as $cookie) {
		$cookies[] = array(
			"name"=>$cookie->getName(),
			"value"=>$cookie->getValue()
		);		
	}

	return $cookies;
}

Another complication was that, when setting a cookie value, the Phantom JS driver internally uses the page's current URL to determine the cookie's domain. Ordinarily this wouldn't pose a problem, but at the start of the session, when I'm trying to set the authentication cookie, the page's current URL is "about:blank", which causes the Mink API's setCookie() method to fail. I had to implement custom code to get the domain from the test suite's base_url parameter and pass it to the Phantom JS driver's internal cookie setting method directly:

public function setWPAuthCookie ($authCookie) {
	$session = $this->getSession();
	$driver = $session->getDriver();

	if ($driver instanceof Zumba\Mink\Driver\PhantomJSDriver) {
		$url = parse_url($this->getMinkParameter("base_url"));
		$authCookie['domain'] = $url['host'];
		$driver->getBrowser()->setCookie($authCookie);
	}
	else {		
		$session->setCookie($authCookie['name'], $authCookie['value']);
	}
}

(The other drivers don't explicitly set a cookie domain and the Mink API doesn't provide any way of setting it, as far as I can tell.)

AJAX

The site we were testing features reports with html "select" controls that are sometimes dynamically populated by AJAX requests. For example, in a report allowing results to be filtered by school, the specific schools you can select might be constrained by the currently selected district. Each time the district selection changes, the school options are populated via AJAX with that district's schools. The problem this poses for automated browser testing is ensuring the AJAX request is complete before attempting to select a dynamically populated option.

The Mink API provides a wait() method, which pauses execution until some JavaScript expression you provide becomes "truthy" or a timeout you specify is reached. (Naturally, this assumes you're using a JavaScript-capable driver.) With this method, I was able to wait until the desired option appeared in the select element, which I determined using jQuery (which the site uses):

public function waitForElement ($ancestor, $descendant, $seconds=5) {		
	$js = "jQuery($ancestor).find($descendant).length;";
	if (!$this->getSession()->wait(1000 * intval($seconds), $js)) {
		throw new Exception("$descendant failed to appear in $ancestor after $seconds seconds.");		
	}
}

This worked well enough for schools, which all have distinctive names, but most of the schools in our test data use the same names for classrooms (e.g. "101", "102", etc.). If I was selecting a classroom by name (which I preferred to do, since this is what a human user would do), waiting for classroom "101" to appear in the classroom select wouldn't necessarily work, since the previously selected school would most likely have a classroom with the same name. What I really needed was some general way of determining if any AJAX requests were still in progress so I could wait for them to finish.

Tip: jQuery.active to the rescue: As it turns out, jQuery exposes a property called "active" (or jQuery.ajax.active in some versions of jQuery), which tracks the number of currently active AJAX requests. When all AJAX requests are complete, it's zero. It's not officially documented, but it's there, and it works:

public function waitForAjax ($seconds=5) {
	if(!$this->getSession()->wait((1000 * $seconds), "jQuery.active == 0")) {
		throw new Exception("Ajax calls still pending after $seconds seconds.");			
	}
}	

If the site you're testing doesn't use jQuery, you'd have to find some equivalent in whatever library you're using. I wouldn't introduce jQuery just for this and I don't think there's any browser-native way of doing this.

Of course, the Right Thing™ would be to have some visual indicator that an AJAX request is in progress and check for the visibility of that indicator. Lesson learned!

Conclusion

Automated testing can certainly improve the reliability of your code, but it does require some work to set up and use. Hopefully, our experiences using Behat with WordPress will help you to get started and overcome some of the pitfalls we encountered.

Leave a Reply

Your email address will not be published. Required fields are marked *