Edition 96

Working With Cookies

If you've ever used Selenium, and if you're currently using Appium for web testing, you might have wondered if Appium supports interacting with cookies stored in the browser. It does! And to explore how and why we might want to work with cookies, we're going to travel back in time to around 2006, when I was working as a freelance web developer. I had a side project called Expenseus, a useful little app designed to help keep track of shared expenses among a group of friends. Here's what it still looks like today:

Expenseus

Don't laugh! It wasn't pretty then and it certainly hasn't aged well, but it is still running after 13 years without any updates (please do not try and hack it). Above is an image of the login page. To build this app, for some reason I decided to write a more or less complete clone of Ruby on Rails in PHP (don't ask me why--I loved PHP that much, I guess!) As part of this whole framework, I built a cookie-based login system. The way this worked was that, on a visit to the site, the app would generate a unique session id for a visitor, and set that id in the PHPSESSID cookie. From then on, requests to the app would contain this unique id in the header. The server could then associate various bits of state with the user across requests, for example, whether a user is logged in, or whether the user just took an action which should result in a message being shown on the next page.

This is a very common pattern for cookie use. Even though cookies can store arbitrary data, they're usually just used to store a session token or id, and that is associated with state kept on the server in something like Memcache, or a database.

Using Cookies for Testing

What does any of this have to do with testing websites? Well, we can leverage the use of cookies to save ourselves the task of setting up state that is already represented by a cookie. For example, if I have logged into my app, and I have access to the session id, I can take that session id, set it as a cookie in some other browser, and, voila, I'm logged in in the other browser now, too! As we all know, UI tests are slow, so anything we can do to avoid needlessly typing in fields and clicking buttons is worth a shot. So here's what we're going to do: build a test suite where the login steps are actually taken only once, and after that, cookies are used to log in more or less instantly for the other test cases. The logic goes like this:

  1. Navigate to the app's URL
  2. If we don't have a login cookie stored in our test class:
    1. Navigate to the login page and log in by filling out fields and clicking buttons
    2. Retrieve the session id from the login cookie and save it on our test class
    3. Go to main step 4
  3. If we have a login cookie stored from a previous test:
    1. Delete the current cookie
    2. Set a new login cookie with the saved cookie information
  4. Proceed with the rest of the test, in a logged-in state.

In the flow above, we need to interact with the cookies in three ways: getting cookie data, deleting a cookie, and adding a new cookie. The Appium / Selenium API has methods for all of these. Here's how we would get a cookie with the name PHPSESSID:

Cookie loginCookie = driver.manage().getCookieNamed("PHPSESSID");

The Cookie class gives us methods that allow us to retrieve the name, value, expiry, domain, and other metadata of the cookie. Here's how we would delete a cookie with the same name:

driver.manage().deleteCookieNamed("PHPSESSID");

And finally, here's how we set a new cookie, assuming we have a Cookie instance called loginCookie (maybe the same object we got back as the result of the call above?) all ready to go:

driver.manage().addCookie(loginCookie);

Putting it all together, we can define a test helper method that will log a user in, either by automating the fields and buttons, or by setting a cookie if one exists in the static loginCookie class field:

private void loginHelper() {
    WebDriverWait wait = new WebDriverWait(driver, 10);
    driver.get("http://expenseus.com/user/login");
    WebElement username = wait.until(ExpectedConditions.presenceOfElementLocated(
        By.xpath("//input[@name='user[email]']")));
    if (loginCookie != null) {
        System.out.println("Using cookie");
        driver.manage().deleteCookieNamed(COOKIE_NAME);
        driver.manage().addCookie(loginCookie);
        driver.get("http://expenseus.com");
    } else {
        System.out.println("No cookie, logging in via form");
        username.sendKeys(USERNAME);
        driver.findElement(By.xpath("//input[@name='user[password]']")).sendKeys(PASSWORD);
        driver.findElement(By.xpath("//input[@value='Log in']")).click();
        loginCookie = driver.manage().getCookieNamed(COOKIE_NAME);
        System.out.println(loginCookie.getName());
        System.out.println(loginCookie.getValue());
    }
    wait.until(ExpectedConditions.urlContains("dashboard"));
}

(In the code above, I've assumed some things about my app, for example the login url, the relevant locators, or the fact that the site takes me to the dashboard upon login.)

That's basically it! There are probably lots of other interesting ways to use cookies with automation (please let me know what you've come up with!), but it will of course depend partially on how your app is set up and how it uses cookies. Even the trick described here may not work in all cases, if for example the app's backend server ties login sessions not just to a cookie-based session id but also to a request IP.

You can check out a full working example of the technique above, where a test is run first on Safari on iOS, and then on Chrome on Android, where the Chrome test just uses the cookie retrieved from the Safari test, rather than having to go through all the login steps itself. It feels pretty magical to see it in action, until you remember that HTTP is totally stateless, so there's really no difference between two requests from one browser or two requests from two different browsers!