Sunday, September 18, 2011

More bug-fixing with Selenium WebDriver and PageObjects

A problem, I'm definitely connected to the internet.
Hi everyone. I spent the past few days tracking and trying to reproduce another problem we've been having on the server. The problem is with the "recommend box" -- basically a way you can vote on your favorite songs or concerts. The problem happened seemingly randomly when you tried to recommend a song or a concert:

Oops. This bug had a few annoying characteristics:
  • It was hard to reproduce and had to obvious order to causing the problem
  • I couldn't reproduce it locally 
  • The server stack trace was useless -- because we use an open session in view filter the root cause was a database problem but only showed up at the end of the filter when the transaction is committed 
Not a good combination. After spending a couple days just experimenting and try to reproduce it, I finally sat down and decided to squash it for real. It was clear that without being able to reproduce it consistently, it was going to be a hard fix. It was also clear that it was a database related problem, so the first steps were:
  1. Restore the database with a set of test data
  2. Try to reproduce the problem locally 
  3. Record each action taken until the problem occurs
  4. Repeat until its consistent
Luckily, that actually didn't take too long. I was able to isolate it to about 4 or 5 manual steps. The next part was to automate this process. Since it had no obvious starting point in the code, it was a good candidate for a UI test with Selenium WebDriver.

As with the previous post, we make heavy use of PageObjects to abstract the details of the markup and what-not from the test case:

package http.voting;

import static org.junit.Assert.assertEquals;
import http.SeleniumTest;

import org.junit.Test;

import pages.IndividualRecording.IndividualRecordingPage;
import pages.song.SongPage;
import pages.voting.RecommendPanel;

public class TestUpvote extends SeleniumTest
{
    protected boolean needsDatabaseReset()
    {
        return true; 
    }
    
    @Test
    public void reproduceErrorForUserActivityAndDuplicateKeys()
    {
        // go to recording 8 
        IndividualRecordingPage recording8 = new IndividualRecordingPage(driver, 8);        
        driver.get(recording8.getURL());
        
        // upvote it
        recording8.focus(); 
        RecommendPanel votingPanel = new RecommendPanel(driver);         
        assertEquals(RecommendPanel.VotingResult.SUCCESS,  votingPanel.upvote());
        
        // go to song page 
        SongPage song257 = recording8.gotoSong(257);
        // upvote song twice
        assertEquals(RecommendPanel.VotingResult.SUCCESS,  votingPanel.upvote());
        assertEquals(RecommendPanel.VotingResult.ALREADY_VOTED, votingPanel.upvote()); 

        // go to "July 8th, 1978 - Roslyn etc.." 
        song257.gotoAssociatedRecording(26);
        // upvote (didn't work in bug)
        assertEquals(RecommendPanel.VotingResult.SUCCESS, votingPanel.upvote());         
    }
}

Minor note: the needsDatabaseReset() method tells the SeleniumTest class to restore the database to a fresh state each time a test is run.

You can see the test case deals with specific actions such as "upvote" and "go to song" instead of searching the website for different elements to click. This is all handled by the different page objects. For example, here is the relevant section of the RecommendPanel class:

     public VotingResult upvote()
    {
        final int currentVoteCount = getCurrentVoteScore(); 
        getUpvoteButton().click(); 
        
        try
        {
            // success if the votes increase or the "error box" pops up -- although we still need to check it 
            (new WebDriverWait(source, 5)).until(new ExpectedCondition<Boolean>() {
                public Boolean apply(WebDriver d) {
                    return getCurrentVoteScore() > currentVoteCount || getVisibleErrorBox() != null ; 
                }
            });
            
            int newVoteScore = getCurrentVoteScore(); 
            
            if(newVoteScore > currentVoteCount) 
                return VotingResult.SUCCESS;             
            else if(getVisibleErrorBox() != null && getVisibleErrorBox().getText().contains("You already voted"))
                return VotingResult.ALREADY_VOTED; 
            else
                return VotingResult.ERROR; 
        }
        catch(Exception exc)
        {
            exc.printStackTrace();
            return VotingResult.ERROR; 
        }
        
    }

Here, we're simply clicking the "recommend" button and ensuring that the result is either an increase in the current vote score, a popup saying "you already voted," or an error message.

With this test, the problem is now easily reproducible. The actual problem is very unexciting. We log user actions by IP address to decrease the chance of duplicate voting. The table we use is mapped to multiple classes with the id "generator" set to increment. This caused duplicate primary keys to be generated when new entries were added. Changing the generator to "identity" fixed the problem.

In contrast to the previous bug, this one was hard to reproduce but an easy fix. Stay tuned for more bugs that are both irreproducible and hard to fix!*

And for reference, the PageObjects used for this test:

IndividualRecordingPage
SongPage
RecommendPanel

* hopefully not

No comments:

Post a Comment