Tuesday, December 27, 2011

31 Days of Testing—Day 20: Refactoring a Monster Test, Part 2

Index to all posts in this series is here!

First off, I hope everyone’s had a great holiday break (or at least readers in areas where you got a holiday break!). I’ve been mostly offline since the 23rd and have greatly enjoyed the respite.

I left off the last post having pulled out the logon functionality into a separate method, plus gave it a bit of  robustness check to make it less brittle—the logon now checks to see if it actually needs to log on.

The logon test looks like this:

Test: Logon If Needed
   1: Navigate to : '/welcome'
   2: IF (Verify Exists 'LoginLinkLink') THEN
   3:    Click 'LoginLinkLink'
   4:    Enter text 'testuser' in 'UsernameText'
   5:    Enter text 'abc123' in 'PasswordPassword'
   6:    Click 'LoginButtonSubmit'
   7: ELSE

Our “main” test now looks like this:

Test: Retrieve a newly created user and validate user's data is correct
   1: Execute test 'Log In If Needed'
   2: Click 'NewContactLink'
   3: Connect to pop-up window : 'http://localhost:3000/contacts/new'
   4: Set 'ContactFirstNameText' text to 'New'
   5: Set 'ContactLastNameText' text to 'User'
   6: Set 'ContactEmailEmail' text to 'new.user@foo.com'
   7: Set 'ContactLinkedinProfileText' text to 'http://linkedin.com/newuser'
   8: Check 'ContactGovtContractCheckBox' to be 'True'
   9: Check 'ContactDodCheckBox' to be 'True'
  10: Check 'ContactOtherCheckBox' to be 'True'
  11: Desktop command: Drag & Drop Neutral Lead Image to Lead Type Drop Target
  12: Click 'CommitSubmit'
  13: Wait for 'TextContent' 'Contains' 'New' on 'NewTableCell'
  14: Verify 'TextContent' 'Contains' 'User' on 'UserTableCell'
  15: Verify 'TextContent' 'Contains' 'new.user@foo.com' on 'NewUserFooTableCell'
  16: Verify 'TextContent' 'Contains' 'http://linkedin.com/newuser' on 'HttpLinkedinTableCell'
  17: Verify attribute 'alt' has 'Contains' value of 'Neutral' on 'New User Lead Type'
  18: Extract attribute 'href' on 'ViewContactLink' into DataBindVariable $(ContactLinkUrl)
  19: Coded Step: [Retrieve_a_newly_created_user_and_validate_users_data_is_correct_CodedStep1]
  20: Click 'ViewContactLink'
  21: Coded Step: [Retrieve_a_newly_created_user_and_validate_users_data_is_correct_CodedStep]
  22:             Connect to pop-up window : 'http://localhost:3000/contacts/7', ConnectToPopup=True
  23: Verify input 'ContactFirstNameText' value 'Exact' 'New'.
  24: Verify input 'ContactLastNameText' value 'Exact' 'User'.
  25: Verify attribute 'value' has 'Same' value of 'new.user@foo.com' on 'ContactEmailEmail'
  26: Verify input 'ContactLinkedinProfileText' value 'Exact' 'http://linkedin.com/newuser'.
  27: Verify attribute 'alt' has 'Same' value of 'Neutral' on 'LeadTypeImage'
  28: Close pop-up window : 'http://localhost:3000/contacts'

In this post I want to concentrate on getting the steps for creating the test contact out of this test and in to its own test/method. In these posts I’m using Test Studio for my examples, but the concept here is completely and absolutely the same regardless if you’re using Selenium, Watir, Visual Studio’s web test, or some other tool.

The eventual goal would be to have the actual creation of the user done via some form of backing API or service call. Getting the browser out of the business of creating your test data is a Good Thing, but it will likely be a multi-step process, especially if you’re new to automation and are just building up your team and toolset.

I’ve worked through this process a number of times, and sometimes the easiest thing is to first go ahead and get the browser doing those actions for you, but centralize the functionality so it’s only happening in one place. While this is slower and more brittle for test execution, it may be your only option at the start – in prior projects/jobs my automation team and I didn’t have skills and/or knowledge right away to start making system calls for doing this setup work. Leaving the browser to do the setup enabled us continue getting automation built as we learned more about the system, what we needed, and worked with other developers to gradually replace those browser-based setup pieces with real service calls.

For now, I’ll chop steps 2-17 in the above “main” test out to a new test. That test now looks like this:

Test: Create Test Contact
   1: Execute test 'Log in if needed'
   2: Click 'NewContactLink'
   3: Connect to pop-up window : 'http://localhost:3000/contacts/new'
   4: Enter text 'New' in 'ContactFirstNameText'
   5: Enter text 'User' in 'ContactLastNameText'
   6: Enter text 'new.user@foo.com' in 'ContactEmailEmail'
   7: Enter text 'http://linkedin.com/newuser' in 'ContactLinkedinProfileText'
   8: Check 'ContactGovtContractCheckBox' to be 'True'
   9: Check 'ContactDodCheckBox' to be 'True'
  10: Check 'ContactOtherCheckBox' to be 'True'
  11: Desktop command: Drag & Drop Neutral Lead Image to Lead Type Drop Target
  12: Click 'CommitSubmit'
  13: Wait for 'TextContent' 'Contains' 'New' on 'NewTableCell'
  14: Verify 'TextContent' 'Contains' 'User' on 'UserTableCell'
  15: Verify 'TextContent' 'Contains' 'new.user@foo.com' on 'NewUserFooTableCell'
  16: Verify 'TextContent' 'Contains' 'http://linkedin.com/newuser' on 'HttpLinkedinTableCell'
  17: Verify attribute 'alt' has 'Contains' value of 'Neutral' on 'New User Lead Type'

Here’s what the main test now looks like after refactoring. I've also renamed a couple steps to make things clearer--something I hadn't paid enough attention to despite looking at this a number of times already!

Test: Retrieve a newly created user and validate user's data is correct
   1: Execute test 'Log in if needed'
   2: Execute test 'Create Test Contact'
   3: Extract attribute 'href' on 'ViewContactLink' into DataBindVariable $(ContactLinkUrl)
   4: [LogExtractedContactLinkUrl] : @"New Coded Step
   5: Open a pop up window so we can connect to it with correct URL
   6: [Retrieve_a_newly_created_user_and_validate_users_data_is_correct_CodedStep] : 
   7:     @"Connect to pop-up window for newly created contact", ConnectToPopup=true
   8: Verify input 'ContactFirstNameText' value 'Exact' 'New'.
   9: Verify input 'ContactLastNameText' value 'Exact' 'User'.
  10: Verify attribute 'value' has 'Same' value of 'new.user@foo.com' on 'ContactEmailEmail'
  11: Verify input 'ContactLinkedinProfileText' value 'Exact' 'http://linkedin.com/newuser'.
  12: Verify attribute 'alt' has 'Same' value of 'Neutral' on 'LeadTypeImage'
  13: Close pop-up window : 'http://localhost:3000/contacts'

OK, now we’ve got things nicely separated out. This test does its setup by logging on and creating a new contact. If I was in straight Selenium or Watir, I’d have those two steps consolidated in a fixture setup. If I was driving tests with Cucumber, Fitnesse, or something similar I’d again have those setup steps consolidated in their equivalent.

Now the overall main tests passes, but I’m still using the browser for setting up my test prerequisites. It works, but as I’ve repeatedly said, it’s slow and it’s brittle. Because I’ve moved that setup out to its own test, it’s a simple matter to replace the guts of that test with a call to some sort of a service or system call.

Don’t get all fancy and complex at the start. Look to the simplest thing that will work for you, even if it seems a bit clunky at first. Because my demo app is so trivial, I can go seriously low-tech for my solution here and simply call out to the command line to invoke SQLITE3.EXE to create my test user for me. In the Create Test User test above, I can completely replace steps 2-13 with one coded step:

   1: [CodedStep(@"Create Test Contact")]
   2: public void Create_Test_Contact_CodedStep()
   3: {
   4:     System.Diagnostics.Process proc = new System.Diagnostics.Process();
   5:     proc.StartInfo.FileName = @"d:\temp\sqlite3.exe";
   6:     proc.StartInfo.Arguments = "development.sqlite3 \"delete from contacts"+
   7:         "where email like '%foo.com';insert into contacts "+
   8:         "(first_name, last_name,email, linkedin_profile, lead_type) "+
   9:         "values "+
  10:         "('New', 'User', 'new.user@foo.com', 'http://linkedin.com/newuser','NEUTRAL');"+
  11:         "\"";
  12:     proc.StartInfo.WorkingDirectory = @"D:\projects\Telerik-Demo\db";
  13:     proc.Start();
  14:     proc.WaitForExit();
  15: }

Yes, there are a number of ugly things here: I’ve hard wired in paths to the executable, working directory, and database name. Certainly I could move these off to configuration settings for a more dynamic, flexible approach—and it would be a waste of time for this project. Take care with how far you go on these sorts of things. Be very Lean in your approach and only solve problems you need to.

If my system were more complex, the logical next step would be to move this from a command line call to a service or stored procedure. That would save me spawning up a separate Process, and would simplify the 15 lines above down to something like:

   1: [CodedStep(@"Create Test User")]
   2: public int Create_Test_Contact_CodedStep()
   3: {
   4:     return My_backing_framework.Create_test_contact("New", "User",
   5:         "new.user@foo.com", "http://linkedin.com/newuser", "NEUTRAL");
   6: }

(The returned value could very well be the database’s ID value of the newly created user. Get creative and use these as opportunities to do stuff that makes your testing easier.)

At my last job, my pals Dan, Jayme, and Jose were responsible for our web services extensibility endpoints. They spent a lot of time building up a framework to create their prerequisites. It was fairly tailored to their particular environment, but I knew it was something my testing group could leverage for our Selenium work. It took several months of part-time hacking around on all sides, but eventually we got their “test pack” setup framework playing nicely with our automation framework. It was a long, slow process, but well worth the effort—however, we didn’t start down that road until we were very certain we needed such an extensive supporting framework for us.

So there you have it. Step two in refactoring this test: getting setup steps out from your test, then moved off to a backing API or simple system calls. Do these moves gradually and always take the simplest steps possible.

In my next post I’ll wrap up this effort with some other thoughts about getting rid of “monster” tests.

No comments:

Post a Comment