Abstracting a Grid Wrapper in TestComplete 4
"Flexibility" is a word that gets thrown around a lot when describing TestComplete, but how, exactly, is it flexible?
"Hey, I don't care about the definition of 'flexible', I just want to know if my test scripts will break when the developers start changing the UI under my feet!"
Uhm, right. That's what I mean by "flexible". A test tool's not much good if all my scripts break as soon as something changes in the tested app, so today we'll take a look at a technique that will provide a layer of breakage protection for our tests.
"Cynicism and suspicion?"
Hmm, not so much that (although it's worked for me), but we will be taking the attitude that our developers will eventually change something rather fundamental, thus putting our tests to the test. Using TestComplete's Object-Driven Testing (ODT) feature, we'll build a breakage-control grid wrapper. In other words, we'll create some code that will allow us to manipulate a grid control, but will not be so tightly coupled to the control that we can't quickly flex (hence "flexibility") to deal with the change. As a side benefit, our ODT class will allow us to have a single access point in case more than one type of grid control is used in a single application.
Ok, by now you may have noticed that if I've written an article, it will have something to do with ODT, so I'll dispense with the introductory material and assume that you've read the required texts before coming to class (in other words, if you're not familiar with ODT, you might want to go read up on the subject in the TestComplete help).
Object-Driven Testing is one of the more robust and powerful parts of TestComplete owing to its rather dynamic and programmatic nature. As a result, there is generally more than one way to solve any particular problem. The approach we're taking for this article involves creating a single ODT class for wrapping several different grid types. Another approach would be to use the ODT ability to re-map an object's methods to point at various implementations, resulting in a sort of inheritance model. You can research that one on your own.
Our example here will wrap the VCL TListView class, the .NET System.Windows.Forms.ListView class, the Developer Express TcxGrid class and the Developer Express.XtraGrid class. For the tested applications, I'm using the Delphi and C# versions of the TestComplete sample application; Orders.exe; the DevExpress sample program for TcxGrid, UnboundSimpleDemo.exe; and for the XtraGrid, the DexExpress sample app, GridMainDemo.exe. The two versions of Orders.exe can be downloaded from the AutomatedQA website at http://www.automatedqa.com/downloads/testcomplete/tc4_samples.asp, while the DevExpress demos are part of the standard installs for those products.
One last thought; the example I'm providing here is only pedagogical and should not be considered as a pattern or a best practice. This is only sample code to show what might be done.
ODT class set up
In this solution, we start out with an abstracted grid wrapper class, rather ingeniously named "GridWrapper". This class will know how to manipulate a grid control (one of any of four different classes) via a set of idealized, and generalized grid-like methods. Then, behind the scenes, we'll specify how to manage particular grid types.
One last note: I've created this class according to some conventions I've developed for managing ODT classes, I'll explain them as I go, and for this paper, I'll even instruct you to follow them, but of course, don't take my conventions as holy writ.
Note - if you're using DelphiScript, you can use the following code to generate the ODT class.
If you didn't use the code above to create the ODT declaration, then in the ODT panel of TestComplete 4, create a new class called "GridWrapper". Note that since I tend to use the visual part of the ODT panel only for definition of classes, and never create objects visually (rather, in code), I don't really bother with prefixes for my ODT classes, but feel free to use "cls" or "odt" or whatever you like.
For the GridWrapper class, I've added some properties (see figure 1).
figure 1 - GridWrapper properties
|GridControl||This will hold whatever control that we're wrapping as a grid. Note that because we're abstracting our control, this can actually be non-grid controls (like a ListView, as we'll see in our examples).|
|GridType||This is a string indicating what type of grid we're working on.|
A note about visibility: Since TestComplete's ODT classes have no notion of visibility (public, protected, private, etc.), everything is public. Yet, there are times when I really want private properties or methods. As a result, I've taken to "hacking" visibility. If I have a property or method of an ODT class that I want only the objects of that class themselves to call (and NOT the test code developers) I prefix that property or method.
Our grid wrapper doesn't have any private data members, but if it did, I'd prefix the private properties with "F" (for "field - a private data member. I'm falling back on my Delphi upbringing here). For methods, I prefix the method with "pr_" for private. My hope is that the prefix will be annoying enough that I and my fellow coders will not go messing around with those.
I've added a set of methods to the GridWrapper class (note that because there are no read/write access methods for ODT classes, some of these methods might normally be properties in a standard OOP class):
figure 2 - GridWrapper methods
If you have much experience with grids in general, you probably recognize this class as just that - a grid; generalized. The methods are what you'd see on most any grid control implementation, which is just the point. We want this grid wrapper to be usable to test a variety of grid classes. Note that with these methods, we could iterate through the entire grid, row by column, getting and/or setting values, and even emulating user interaction by calling SelectRow() or FocusCell() before beginning text entry.
I normally create a new script code unit for each ODT class I define. One class - one unit. This seems to make things a little more clear and manageable. As such, create a new unit in your TestComplete project called odtGridWrapper.sd (Note: my code in this article is in DelphiScript).
We use the following routines in odtGridWrapper (you can just copy the code from the listing to follow unless you're working in another scripting language):
|Routine Name||Parameter List||Return Value||Note|
|CellValue||aRow, aCol: Integer||OleVariant||Returns values of cells.|
|SetCellValue||aRow, aCol: Integer; aValue: OleVariant||Boolean||Sets a cell value directly|
|CellByValue||aValue: String; var aRow, aCol: Integer||Void||Gets a cell by its value|
|SelectRow||aRowIdx: Integer||Void||Selects whole row|
|FocusCell||aRow, aCol: Integer||Void||Focuses single cell|
|Picture||None||Onscreen object||Gets TC onscreen object for visual validation purposes|
|pr_Assigned||None||Boolean||"private" method to validate assignment|
|pr_Annotate||aMessage: String||Void||Adds messages to log|
This is the approach that will use the GridType property we defined. We'll check the value of GridType to direct our actions.
For this approach, we need some grids to implement and demo apps to work with. See the earlier listing of the sample applications used. If you don't have either of the Developer Express components, it's ok, you can still see the point of the demo using the TestComplete Orders.exe applications.
These apps will actually show some of our flexibility. The Orders applications are actually built using ListView controls rather than grids. The DevExpress demos, of course, use their popular grid components.
If you follow through the code, you'll see that we're simply implementing a case statement in each of the methods, taking the value of Self.GridType (note that "Self" is specific to DelphiScript, for everything else, it's "This") and implementing action based on its value. If we're dealing with a VCL TListView, we take one set of actions; if it's a TcxGrid, we do something else, and so on.
Thus, for the RowCount method, we return a value that represents a count of the "rows" in each type of grid, even if the control in question is not a grid.
As you can see, some of the methods are more complex than others, but each provides uswith an abstraction layer of protection from the internal and specific workings of each control. The benefit is that, if our developers should decide to change their choice of grid controls half-way through application development, our code will not suffer as much as it might have if we were addressing the grid controls directly in our test code.
For an example of use of the abstracted grid, see the Main unit from our test project:
The point of this unit is to use the abstracted grid on each of the sample applications, executing the same code on each. For each sample application you have access to, set the Boolean variables in the Main routine to turn on or off testing of those applications and then run the project.
If you're unable to run the project or just want to cut to the chase, here's a link to an exported log of the whole project run.http://www.automatedqa.com/images/blog/rkl_022806_LogSample.zip
At QuickCARE, we're doing a considerable amount of control wrapping since we're still rather early in development and have already changed some implementing controls twice (three times for our menu). This is not a condemnation of our development team, rather it's indicative of real-world development, and it would be unwise for us to not prepare for further adjustments.
Additionally, by creating control wrappers that are defined as "ideal" controls (like our highly simplified grid wrapper here) it saves us from requiring the whole QA team to become experts on all the tested controls. The grid is straightforward and easy to use in tests, regardless of how obfuscated the underlying control may be.
I've commented the code so it should be mostly clear, but I'm open to comments and questions. Here
's the TC project.