Friday, October 7, 2011

Selenium Tests:: How to Make them better? Step1: Make them more Readable

I like COBOL from old days, because its very easy to read through, if written well. Its very much like reading a news paper. People love to have more sensible, readable code, isn't it. It should definitely not like some 8085/86 programming? :-(

So anyway Selenium can be written in quite a few programming languages, anyway I picked Java to write this. I am an expert programmer but still I like the way it can be formatted or written so everyone can understand. And I will try to explain this in this post, how to make a Selenium test more readable.


Why do we need to be much constructive and follow certain processes to make it much better?
Well, quite a familiar situation to some of the teams who are practising Agile newly, where a project is very agile, architecture is evolving, requirements are quite evolving, very unclear process, Agile is not followed by books etc there could be so many reasons for this to happen.

In this situation, development or business is still expecting the QA to automate all tests and integrate them in to a build process as a part of Continuous Integration environment etc. QA complains that the build is not stable and without the build being in a good stable condition there's no point in automating tests. WOW.. sounds familiar isn't it?

What're the basic things one should know about UI mapping technique?
Well, as you can imagine UI mapping technique is nothing but an OR, if you are using some traditional automation tools. Where you can store your objects and constantly refer them in your tests.

I would say, its not quite friendly. You have to do a bit of work to actually find properties you want selenium to use to recognize the objects, by using some open source tools/ add-ons (like, Firebug) or looking at the code or whatever.

so, how to Automate and still make the Maintainability a less effort?

1st Step, I would to make it more readable is by creating a very basic UI map. UI mapping is technique used to make the Web elements (Objects for QTP folks) properties all stored in a single place. It has the following advantages:

1.  Any property change for a given web element (ID, Name, Link, CSS, XPATH etc) in the future is quite easy to make. Because there's only 1 place to go to make such a change, your UI map. 

For traditional automation tool users, its like an Object repository (OR), where you will update or add Mandatory/ Assisted properties etc

2. Because we're using a single place to store and retrieve all object properties from, you will make a change to an Web element properties once for all.

3. You don't need to be an expert in some special technology or something. You can do it in your comfortable language. It's just a technique and there's no strict rules on how to create it, there's a very good guidance in Selenium documentation section

Okay, enough details. How do we actually do it?
Well, again it depends on few things like the size of the application, architecture of the application, number of modules, size of the each module and how much automation effort is planned for testing this application etc. So far I could see Selenium is used either for smoke testing or for some functional testing, where as it can do lot more.

Anyway, I used to java and I have used a Java file to store my web element mappings and I refer to these mapped elements throughout my test suite..

Well, a Java file where I stored my mappings will look something like below
public class gmailMap {
public String emailID = "id=Email";
public String passWord = "id=Passwd";
public String signInBtn = "id=signIn";
public String waitTime = "30000";
public String InboxCnt = "css=div.XQRo3.UKr6le";
public String emailTable = "//*[@id=\":pd\"]";
public String firstemailFrom = "id=:p9";
public String firstemailSubj = "id=:p6";
public String accName = "id=gbi4t";
public String signOut = "//*[@id=\"gb_71\"]";
}

and tests might look something like below when they use the UI mapping technique followed.
@Test
public void testGmailLogIn() throws Exception {
gmailMap UImap = new gmailMap();
selenium.open("/");
selenium.selectWindow(null);
selenium.type(UImap.emailID, "<<username>>");
selenium.type(UImap.passWord, "<<password>>");
selenium.click(UImap.signInBtn);
selenium.waitForPageToLoad(UImap.waitTime);
String newEmails = selenium.getText(UImap.InboxCnt);
System.out.println(newEmails);
verifyTrue(selenium.isElementPresent(UImap.emailTable));
String firstEmail = selenium.getText(UImap.firstemailFrom);
String fullSubjectText = selenium.getText(UImap.firstemailSubj);
String accName = selenium.getText(UImap.accName);
System.out.println(UImap.firstemailFrom);
System.out.println(UImap.firstemailSubj);
System.out.println(UImap.accName);
selenium.click(UImap.accName);
assertTrue(selenium.isElementPresent(UImap.signOut));
selenium.click(UImap.accName);
selenium.click(UImap.signOut);
selenium.waitForPageToLoad(UImap.waitTime);
selenium.selectWindow("null");
//selenium.click("id=PersistentCookie");
}

Here as you can see, there's only element names are used, suggesting the reader that I want to use "selenium" to "type", some text "username" in the control called "UImap.emailID".

Please note, the webElement name given in the UImap should be very understandable. Whatever it is, but naming convention is important too so the user can easily understand it by looking at.

There's a few different ways to do it, one of the other popular way of doing it, especially for people from Java world, is using the properties file. In this approach, you will use a simple .properties file where a key=value pairs of representation is used. So a simple gmailUImap.properties might look something like below
UI_Email="id=Email"
UI_passWord = "id=Passwd";

and your test might look something like below.. I am just writing it here, I didn't ran this myself, so if it doesn't run well, you can correct it or pass on your comments on this post as well.

public class gmailClass extends SeleneseTestCase{
public String PROP_FILE="gmailUI.properties";
public String UIemail;
public String UIpassWord;

@BeforeClass
public void setUp() throws Exception {
SeleniumServer selServ = new SeleniumServer();
selServ.boot();
setUp("https://accounts.google.com/", "*firefox");
selenium.windowMaximize();
selenium.windowFocus();
}


public gmailClass() throws Exception{
//create and load properties
    try{  
        InputStream is = gmailClass.class.getResourceAsStream(PROP_FILE);  
        Properties prop = new Properties();  
        prop.load(is);  
        UIemail = prop.getProperty("UI_Email");
        UIpassWord = prop.getProperty("UI_passWord");
        is.close();  
        System.out.println(emailID);
        /* code to use values read from the file*/  
      }catch(Exception e){  
        System.out.println("Failed to read from " + PROP_FILE + " file.");  
      }
}

@Test
public void testGmailLogIn() throws Exception {
gmailMap UImap = new gmailMap();
selenium.open("/");
selenium.selectWindow(null);
selenium.type(UIemail, "<<username>>");
selenium.type(UIpassWord, "<<password>>");
.
..
...
....
}


Whatever way you chose to write your tests, this process makes it a bit better readable test, also making it more maintainable. But, this is missing something, we're giving the precise data to run the test. So its obviously not so complete to data drive it, especially when we want to test it against positive, negative, boundary conditions and most importantly if you want to load test it.. this is our next step

If you have any comments or suggestions, feel free to pass them on!

Selenium Tests: How to make them Better

May be you should be thinking about writing selenium tests in a better manner considering various things about the application, areas of testing that are in-scope for automation, time constraints you are running under etc etc. However below are some examples of tests that can be written in different ways to make it more maintainable, readable, scalable..

If you're using a sample application to record a simple Selenium test. Of course, you can use selenium IDE to record your first test and then export the same to your development IDE (like Eclipse, Visual Studio etc) and try to run it. When you have actually exported your recorded test, it might look something like below (Note: I have used Java code throughout this)

package com.example.tests;
import com.thoughtworks.selenium.*;
import java.util.regex.Pattern;


public class Untitled extends SeleneseTestCase {
public void setUp() throws Exception {
setUp("http://www.mortgagecalculator.org/", "*chrome");
}
public void testUntitled() throws Exception {
selenium.open("/");
selenium.type("name=param[homevalue]", "150000");
selenium.select("name=param[credit]", "label=Excellent");
selenium.type("name=param[principal]", "125000");
selenium.select("name=param[rp]", "label=New Purchase");
selenium.type("name=param[interest_rate]", "2.5");
selenium.type("name=param[term]", "25");
selenium.select("name=param[start_month]", "label=Jan");
selenium.select("name=param[start_year]", "label=2010");
selenium.type("name=param[property_tax]", "1.1");
selenium.type("name=param[pmi]", "0.35");
selenium.click("css=input[type=\"submit\"]");
selenium.waitForPageToLoad("30000");
String Monthly_Pay = selenium.getText("css=td > h3");
}
}

Manual test for the above class is simple;
1. Open MortgageCalculator.org web page
2. Enter homevalue as 150,000
3. Enter Credit Profile as Excellent
4. Enter Loan Amount as 125,000
5. Enter Loan Purpose
6. Enter Loan Term
7. Enter Start Date
8. Enter Property Tax %
9. Enter PMI %
10. Click Calculate button
11. Finally, verify the Monthly Payment

Now the first thing that one would want to do is to data drive it. May be from an excel file or from a database or whatever.. Here we will try to do the same using Excel which is most widely used for data driven frameworks. If you're using TestNG (as I do) you can actually look at one good article about using TestNG capabilities to data drive a selenium test at Mahesh's blog. I am also using the same data driver here, anyway as a programmer or a tester with common understanding of programming as you could imagine the basic code will change something like below...

public void testMortgages(String homeValue, String creditRate, String principalAmt,String typePurchase, String intRate, String termYrs, String startMonth, String startYr, String propTax, String pmiPercent, String expMthlyPay, String actMthlyPay) throws Exception {
selenium.type("name=param[homevalue]", homeValue);
selenium.select("name=param[credit]", "label=" +creditRate);
selenium.type("name=param[principal]", principalAmt);
selenium.select("name=param[rp]", "label="+typePurchase);
selenium.type("name=param[interest_rate]", intRate);
selenium.type("name=param[term]", termYrs);
selenium.select("name=param[start_month]", "label=" +startMonth);
selenium.select("name=param[start_year]", "label=" +startYr);
selenium.type("name=param[property_tax]", propTax);
selenium.type("name=param[pmi]", pmiPercent);
selenium.click("css=input[type=\"submit\"]");
selenium.waitForPageToLoad("30000");
String monthlyPay = selenium.getText("css=td > h3");
Assert.assertEquals(monthlyPay, expMthlyPay);
System.out.println("Actual Monthly Pay is: " + monthlyPay);
}

Basically we have replaced all the static data with variables. How these variables can be picked up from an excel file? well, we use jxl to do the job of retrieving and/or reading excel file etc and TestNG to do the rest of the job, to use the data returned by a method that uses jxl capabilities to read an excel file and then run a test...

Here as an example, I used Mahesh's code for reading an excel file using jxl

public String[][] getTableArray(String xlFilePath, String sheetName, String tableName) throws Exception{
        String[][] tabArray=null;
       
            Workbook workbook = Workbook.getWorkbook(new File(xlFilePath));
            Sheet sheet = workbook.getSheet(sheetName);
            int startRow,startCol, endRow, endCol,ci,cj;
            Cell tableStart=sheet.findCell(tableName);
            startRow=tableStart.getRow();
            startCol=tableStart.getColumn();


            Cell tableEnd= sheet.findCell(tableName, startCol+1,startRow+1, 100, 64000,  false);              


            endRow=tableEnd.getRow();
            endCol=tableEnd.getColumn();
            System.out.println("startRow="+startRow+", endRow="+endRow+", " +
                    "startCol="+startCol+", endCol="+endCol);
            tabArray=new String[endRow-startRow-1][endCol-startCol-1];
            ci=0;


            for (int i=startRow+1;i<endRow;i++,ci++){
                cj=0;
                for (int j=startCol+1;j<endCol;j++,cj++){
                    tabArray[ci][cj]=sheet.getCell(j,i).getContents();
                }
            }
        return(tabArray);
    }

and created a @dataprovider annotation to read from a given file using the above method..


    @DataProvider(name = "DP2")
    public Object[][] createData2() throws Exception{
        Object[][] retObjArr=getTableArray("lib\\data1.xls","Sheet1","mrtgCalcData1");
        return(retObjArr);
    }

But for my Test method to use this data provider, I have to clearly specify the same who's the data provider for that test, so I will add  @Test (dataProvider = <<name>>) annotation for my test, that would make it look like below...

@Test (dataProvider = "DP2")
<<<<your selenium test method>>>

And that's it. You just need to add a Setup(), Teardown() methods, if you're using Junit framework or some test annotations if you're using TestNG framework to start the selenium server and stop it when the job done..!

package com.tests;


import com.thoughtworks.selenium.*;
//import com.tests.publicLibrary.*;
import org.junit.AfterClass;
import org.openqa.selenium.server.SeleniumServer;
import org.testng.Assert;
import org.testng.annotations.*;
import java.io.File;
import jxl.*;


@SuppressWarnings("deprecation")
public class mortgageCalc extends SeleneseTestCase{
   
    @BeforeClass
    public void beforeJobStarted() throws Exception {
        SeleniumServer seleniumserver=new SeleniumServer();
        seleniumserver.boot();
        seleniumserver.start();
        setUp("http://www.mortgagecalculator.org/", "*firefox");
        selenium.open("/");
        selenium.windowMaximize();
        selenium.windowFocus();
    }


    @DataProvider(name = "DP2")
    public Object[][] createData2() throws Exception{
        Object[][] retObjArr=getTableArray("lib\\data1.xls","Sheet1","mrtgCalcData1");
        return(retObjArr);
    }  
   
@Test (dataProvider = "DP2")
public void testMortgages(String homeValue, String creditRate, String principalAmt,String typePurchase, String intRate, String termYrs, String startMonth, String startYr, String propTax, String pmiPercent, String expMthlyPay, String actMthlyPay) throws Exception {
selenium.type("name=param[homevalue]", homeValue);
selenium.select("name=param[credit]", "label=" +creditRate);
selenium.type("name=param[principal]", principalAmt);
selenium.select("name=param[rp]", "label="+typePurchase);
selenium.type("name=param[interest_rate]", intRate);
selenium.type("name=param[term]", termYrs);
selenium.select("name=param[start_month]", "label=" +startMonth);
selenium.select("name=param[start_year]", "label=" +startYr);
selenium.type("name=param[property_tax]", propTax);
selenium.type("name=param[pmi]", pmiPercent);
selenium.click("css=input[type=\"submit\"]");
selenium.waitForPageToLoad("30000");
String monthlyPay = selenium.getText("css=td > h3");
Assert.assertEquals(monthlyPay, expMthlyPay);
System.out.println("Actual Monthly Pay is: " + monthlyPay);
}  
   
   
    @AfterClass
    public void afterJobDone(){
        selenium.close();
        selenium.stop();
    }
   
    public String[][] getTableArray(String xlFilePath, String sheetName, String tableName) throws Exception{
        String[][] tabArray=null;
       
            Workbook workbook = Workbook.getWorkbook(new File(xlFilePath));
            Sheet sheet = workbook.getSheet(sheetName);
            int startRow,startCol, endRow, endCol,ci,cj;
            Cell tableStart=sheet.findCell(tableName);
            startRow=tableStart.getRow();
            startCol=tableStart.getColumn();


            Cell tableEnd= sheet.findCell(tableName, startCol+1,startRow+1, 100, 64000,  false);              


            endRow=tableEnd.getRow();
            endCol=tableEnd.getColumn();
            System.out.println("startRow="+startRow+", endRow="+endRow+", " +
                    "startCol="+startCol+", endCol="+endCol);
            tabArray=new String[endRow-startRow-1][endCol-startCol-1];
            ci=0;


            for (int i=startRow+1;i<endRow;i++,ci++){
                cj=0;
                for (int j=startCol+1;j<endCol;j++,cj++){
                    tabArray[ci][cj]=sheet.getCell(j,i).getContents();
                }
            }
        return(tabArray);
    }
}

If you run this using TestNG, it should work fine. You can use any other framework by making lil updates to the above class, that is out of scope for this.. But what can we do better, here? Is this the best automated test case, I can ever have using Selenium. Obviously not, because one could use his/ her own intelligence to come up with a brilliant solution to make it more maintainable, scalable etc. However there's pretty basic things that one should consider that're already listed in selenium's website. Below are some examples of how this test can be made better readable, maintainable, scalable. I will try to explain them in my next few posts..

Monday, March 7, 2011

QA Automation in Agile

Mindset
First of all one should be aware that in Agile, QA automation takes a completely different approach to traditional approach. Because more then QA its the TEAM who would be automating things, as the project is scaled to meet the project objectives. ROI of any project would increase top-down if we consider a pyramid structure (Illustrated as below).



As you can see from the diagram, If you have more unit Tests, Integration tests/ API tests (that are done under the hood, right below the UI layer) - that would allow you to catch defects sooner then later. GUI tests should be written only for the business critical functionality that can not be automated in Unit/ Integration testing. There could definitely be some kinda 3rd party controls that the application uses that might not be possible to test in Development or there could be a scenario where Dev can't see all the object elements exposed so as to write tests and the list goes on. How to pick those tests, what tests should be automated in QA etc is discussed below

Working Environment
Automation in Agile environment is key player to the success of the project but at the same time if there’s no proper planning, it obviously a failure. On the other hand, the whole project planning plays a key role in successfully delivering automation.

If you have the full project in ones brain it doesn’t work that way. Everything should be put on paper in the form of backlog. Organizing the backlog is a scrum master’s responsibility, though. But it has a greater impact on delivery of the automation suite as well.

In my opinion when the system architecture is designed, automation engineer should do an evaluation of the company standard tool to see if that would work with the given technologies, environment. If not, alert the project manager so that can be negotiated around whether it is okay to go out side the company standard tools (ex: open source tools) or any other best fits etc. Basically its automation engineer’s responsibility to get the right tool sorted before initiating the project. Key responsibilities during this process:

  • Evaluate the company standard tool against the given architecture, technologies, environment
  • Make recommendations on alternatives
  • Tool evaluation: Pick the best fit of the available by running POC against each of the available options
  • Build Automation Environment
Once all the above tasks are done, we’re half way thru the successfully delivering automation. Quite frequent issues are shown when you’re running on a baseless environment or every time a change happens you have re-build the environment from scratch or using some one else’s environment and so on. Rather this could be avoided by building your own environment with all the pre-requisites and maintain it throughout the project delivery. That’s the catch here.

When this is all done and you are now running in a Scrum team. Few things that QA manager has to sort out first i.e., Resource Management
Ø      Make sure there’s a min 1:3 ratio of Dev to QA resources
Ø      Make sure there’s a min 1:3 ratio of Automation to QA resources

Note: 1:3 ratio of Automation Specialist to QA is based on the assumption that QA resources are technically capable of maintaining the automated scripts.

There could obviously be problems keeping up-to-speed with development if the right ration of QA team is not maintained. The same way a right ratio of Automation team to QA should also be maintained, otherwise automation would lag behind the schedule, too much pressure as the project goes along etc that is unnecessary when you can sort all them upfront with little bit of planning of resources.

Best practice is to add resources as the project goes along as what’s happening with the project is well determined when it’s rolling on. When, where, and how the new resource can best fit the existing team, can also be determined only when it’s already running. There’s no point having a bunch of people sitting there and doing nothing, either.

Enough talk about the preparation, getting the house in line. Let’s get in to the sprints.

Live Environment Examples
I will try drawing a rough picture of how automation in an agile should be approached. In my opinion, take small steps before walking & walk before you run.

Sprint1: 1st week – Build an automation framework. You might be spending couple of days to a week writing your framework. This is so crucial and I would say basic items. Without a proper framework also you might enter cycles but is not an organized/ professional way of going about it

Sprint1: 2nd week – Automation feasibility study & Business critical functionality identification. You would never do a better thing then this. It’s so important for both business and automation delivery to identify the scenarios that would be worth automating. There’s no point taking every little story in to account and end up in doing nothing. This is identified by the product owner, if not, by Business Analyst

Start automating whatever can be done & what is delivered by Development that is stable enough to automate & that is marked as Business Critical

Sprint2: 1st week – Automation Backlog of Sprint1. There are certainly some stories that would still need to be automated in the first week or so. This week should also be a good time to plan for Sprint#2 automation that includes identifying max ROI functions that needs to be automated etc

Sprint2: 2nd week – Continue Automating whatever can be done & what is delivered by Development that is stable enough to automate & that is marked as Business Critical

Sprint3: Same as Sprint2

Recovery Sprint: Clear off any Technical Debt. This sprint basically to help the whole team NOT JUST Automation team to:

Ø      Get up-to-speed with the Development. Update/ add/ Run any pending tests that QA has to do. Update the quality management tools to reflect any additions/ update and keep it up-to-speed. MANUAL TESTERS
Ø      Update the Unit tests, API/ Integration tests that should be changed to sync with the latest feature developments. For example, you might have written a test that was good for Sprint1 but it is not valid any more. Have you updated all of them? Make sure the answer is YES. Its most useful for clearing out any technical debt that you’re slowly started accumulating. Let’s have NO TECHNICAL DEBT. Clear that off from day-1. That’s the main goal. DEVELOPERS
Ø      Automation testers should go back & re-visit the automated functions/ scripts that they have coded. In most cases, you need to update the object repositories or Mapping files or might have to change the whole function altogether. Because changes are tend to be big enough in 3 sprints. If you do not update them now, you would end up in feeling like re-writing the whole thing againL? Which I am sure you hate to do. AUTOMATION TESTERS

Continue along these lines for the rest of the project completion. Note: This is under assumption that you're running a typical 2 week sprints.

I suggest having 1 week sprints for every 3 sprints, however, it should change depending on the project. Factors that might help you consider recovery sprint are like project complexity/ amount of changes going on etc.

I think if we continue something similar to above guidelines, we should not be far off from successfully delivering the project.


Conclusion
Main Goal of Automation should be:
Ø High ROI
Ø Provide Business Value


Simply forget about 100% Automation. Delivering all functionality, should not be your goal.


Sharing your experiences & any comments are much appreciated..All the best with your project

Sunday, February 27, 2011

How to Create a Test using OTA

Working with TestFactory objects using OTA API

'Get or create test plan subject folder
errmsg = "Subject tree error"
Set root = TreeMgr.TreeRoot("Subject")
On Error Resume Next
Set folder = root.FindChildNode(FolderName)
On Error GoTo LinkDefectsToEntitiesErr

If folder Is Nothing Then _
Set folder = root.AddNode(FolderName)

'Create a design test
errmsg = "Design test error"
'Get the test if it exists
' For the code of GetTest, see the Test object example
' "Get a test object with name and path"
Set NewTest = GetTest(TestName, FolderName)
'If it doesn't exist, create it

If NewTest Is Nothing Then
Set NewTest = TestF.AddItem(Null)
NewTest.Name = TestName
NewTest.Type = "MANUAL"
'Put the test in the new subject folder
NewTest.Field("TS_SUBJECT") = folder.NodeID
NewTest.Post
End If

'Get or create a design step from the factory of the new test
errmsg = "Design step error"
Set StepF = NewTest.DesignStepFactory
Dim aFilter As TDFilter
Set aFilter = StepF.Filter
Dim StepName$
StepName = TestName & "Step_1"
aFilter.Filter("DS_STEP_NAME") = StepName
Set lst = StepF.NewList(aFilter.Text)
If lst.Count = 0 Then
Set desStep = StepF.AddItem(Null)
desStep.StepName = StepName
desStep.StepDescription = "Step to be linked to defect."
desStep.StepExpectedResult = "This step expected to be linked."
desStep.Post
Else
Set desStep = lst.Item(1)
End If

Find a specified requirement in a specified folder: OTA

Work with QC Requirements using OTA API

Public Function GetReqByPath(fullPath$, _
Optional delimChar As String = "\") _
As Req
' This function returns a Req object specified by its
' full path.
' For example:
' Set r = GetReqByPath("SCRATCH\OTA_REQ_DEMO\OTA_S_O_1")
' will return the OTA_S_O_1 object.
' A requirement name is not unique in the project, but it is
' unique as a direct child of another requirement.
' Therefore, these routine works by walking down the
' requirement tree along the fullPath until the requirement
' is found at the end of the path.
' If a backslash is not used as the folder delimiter, any other
' character can be passed in the delimChar argurment.

Dim rFact As reqFactory
Dim theReq As Req, ParentReq As Req
Dim reqList As list
Dim NodeArray() As String, PathArray() As String
Dim WorkingDepth As Integer
On Error GoTo GetReqByPathErr

'Trim the fullPath and strip leading and trailing delimiters

fullPath = Trim(fullPath)
Dim pos%, ln%
pos = InStr(1, fullPath, delimChar)
If pos = 1 Then
fullPath = Mid(fullPath, 2)
End If
ln = Len(fullPath)
pos = InStr(ln - 1, fullPath, delimChar)
If pos > 0 Then
fullPath = Mid(fullPath, 1, ln - 1)
End If

' Get an array of requirements, and the length
' of the path
NodeArray = Split(fullPath, delimChar)
WorkingDepth = LBound(NodeArray)

' Walk down the tree
'tdc is the global TDConnection object.
Set rFact = tdc.reqFactory

For WorkingDepth = LBound(NodeArray) To UBound(NodeArray)
'First time, find under the root (-1)
'After that, under the previous requirement found: ParentReq.ID

If WorkingDepth = LBound(NodeArray) Then
Set reqList = rFact.Find(-1, "RQ_REQ_NAME", _
NodeArray(WorkingDepth), TDREQMODE_FIND_EXACT)
Else
Set reqList = rFact.Find(ParentReq.ID, "RQ_REQ_NAME", _
NodeArray(WorkingDepth), TDREQMODE_FIND_EXACT)
End If
' Delete parent. Each loop has to find it again.
Set ParentReq = Nothing
Dim strItem, reqID&, strID$, thePath$

For Each strItem In reqList
' The List returned from ReqFactory.Find is a List
' of strings of format ID,Name.
' For example "9,Products/Services On Sale"
' Extract the ID from the string by splitting the
' string at the comma.
pos = InStr(strItem, ",")
strID = Mid(strItem, 1, pos - 1)

' Convert the ID to a long, and get the object
reqID = CLng(strID)
Set theReq = rFact.Item(reqID)

'Now check that the object is at the correct depth.
'If so, we've found the requirement. On the next loop,
'we'll look from here down.
thePath = theReq.Path
PathArray = Split(thePath, "\")

' Debug.Print "Number of elements is " & UBound(PathArray)
' Debug.Print theReq.ID, theReq.Name

If UBound(PathArray) = WorkingDepth Then
Set ParentReq = theReq
Exit For
End If
Next strItem
If ParentReq Is Nothing Then Exit For
Next WorkingDepth
Set GetReqByPath = ParentReq
Exit Function

GetReqByPathErr:
ErrHandler err, "GetReqByPath"
Set GetReqByPath = Nothing
End Function