Saturday, May 20, 2006

Refactoring a legacy web application with Selenium

About two months ago, one of our clients decided, to change the way one java web application that we built for them a few years ago worked. The application is built to gather and process different kinds of measurements in a medical sector, building different charts and analysis reports on these measurements.

The requirements

When the application was first built, it was decided that each client (hospital etc) will have their own instance of the application installed at their location. That meant for instance that when a report is run, it is done on all the data from the database.

Now the client, after a market research decided that it was to hard to deploy and maintain a vast amount of applications in different locations, so they asked me if we could change the way the aplication working, so they could host the application, the application can have one database, but multiple clients can log into it and work. This would mean that the data in the database should be divided for each of their clients so they can only see their own data and now the other's.

The solution was to have a master table, I called it entities, which would now have a foreign key in all measurement tables. Although altering the database wasn't such a problem, I had to maintain the existing functionality as it was, and because of the domain there were some difficulties, especially because some complicated phisical methods of calculations were used troughout the software.

How can I change but keep existing functionality exactly the same?

If the application needs to stay the same, I had the idea of recording some testing scenarios using the great open source Selenium[1] and the Mozilla Firefox extention, called Selenium IDE [2], trough the web interface that worked for the legacy version, that would need to work with the new version also.

But if I record a database test, and want it to work after I change the internals of the project, this means that I need to have the same data into the database, when the test is rerun. For this first I created a servlet which I called CreateTestDatabaseServlet who's mission was to reset the entire data from a database to a known state. Based on that state I could record my tests, because I knew what should appear on the user interface. For instance, my servlet, cleared all FilmType rows from the database and added Film Type A, Film Type B and Film Type C. Now when going into the web application, I knew for sure that the combo on a screen , where I can choose a FilmType has the 3 values all the time:

The code of the servlet is like:

public void doGet(HttpServletRequest request, HttpServletResponse response) throws
ServletException, IOException{

this.session = HibernateUtil.getSession();
response.setContentType(CONTENT_TYPE);
PrintWriter out = response.getWriter();
out.println("");
out.println("

The servlet has received a " + request.getMethod() +
". This is the reply.

");
out.println("

"+this.createTestDatabase()+"

");


try
{
out.println("

" + this.insertTestData() + "

");
}
catch(Exception Ex)
{
Ex.printStackTrace();
}
finally
{
try {
this.session.close();
} catch (HibernateException ex) {
ex.printStackTrace();
}
this.session = null;
}
out.println("");
out.println("");
out.close();
}

where:

private String createTestDatabase() {
new AECBO(this.session).deleteAll();
new TubeRoomBO(session).deleteAll();
new UserBO(session).deleteAll();
new EntityBO(session).deleteAll();
new SUMBO(session).deleteAll();

...

return "";
}

and

private String insertTestData() {

...

createFilmType("Film Type A");
createFilmType("Film Type B");
createFilmType("Film Type C");

...

createProjection("Left Crano-Caudal");
createProjection("Right Crano-Caudal");

....

createUnit("FFD","centimeters (cm)",1);
createUnit("TRC","centimeters (cm)",1);
createUnit("FSD","meters (m)",1);
createUnit("BT","centimeters (cm)",1);
createUnit("CIO","micro-Grays (µGy)",1);

...
}

Now when a Selenium test is run, the first thing it does it reset the database to the known state, then login (I already know I have a user created by the servlet, that has a/a as credentials) and do what is necessary:

|open|/app/CreateTestDatabaseServlet|
|open|/app/login/login.faces?random=734574375|
|type|loginForm:userName|a|
|type|loginForm:password |a|
|clickAndWait|link=Login|
|clickAndWait|link=Image Quality|
|type|_id1:main:dateinput||


With the method to reset the database in place, using Selenium IDE [2] I started recording tests for all parts of the application, following different scenarios, which I then saved and created a suite of selenium tests, and after I made sure all of them work, I started dissecting the code.



Then I took a test, refactored the code and database until I made that selenium test work. Then I took the next test, until it and the one before it worked. I looked if I could make some refectorings, I did them ensuring nothing is broken and moved to the next test, until one month later all tests were working.

Although it doesn't seem like very much, the selenium regression tests helped me a great deal, telling me in 7 minutes if the existing functionality of the application (which took more then a year to develop because there are some very complicated phisics formulas in it) is working exactly as it did before, but with the extentions added. One of the big refactorings I did also was to remove the built in data access layer and business objects, and replace everything with Hibernate [3].

So it proved that my plan of action
1. Reset state of the database
2. Record the tests
3. Refactor - test until the recorded tests works again

worked very well and very fast.

Conclusion

Without Selenium, Selenium IDE and Hibernate, the entire operation could have taken a lot more then a month, because of the constant fear that the existing functionality will be broken and would need tobe rebuilt, after more then a year was already invested in the project.

[1] Selenium - www.openqa.org/selenium
[2] Selenium IDE - www.openqa.org
[3] Hibernate - www.hibernate.org

2 comments:

Patrick Lightbody said...

Dan,
Glad you like Selenium. If you haven't tried it, check out Selenium RC - it lets you do even more. Selenium RC was originally donated as a project that came from a commercial product called HostedQA. HostedQA provides all the standard Selenium features, plus test refactoring, macros, screenshots, scripting support, and more. Definitely check it out if you like Selenium.

Dan Bunea said...

Hi Patrick,

Yes, we are currently using Selenium RC for some .NET web projects and find it very useful. The funny things is that before it actually appeared, we made a class called Selenium where the code was:

open("/app/index.aspx");
type("txtName","Name");

which had a Render method that outputed the test as html:

|open|/app/index.aspx|
|type|txtName|Name|


which was great for functional decomposition of our tests. However we are now movign the tests written that way to Selenium RC as they are integrated with NUnit and it's easier to be run by Cruise Control (.NET).

Thanks for your reply.
Dan