The migration march to PB 12.NET will have many shops revisiting legacy applications. In my previous articles ("Refactoring Is Not an ‘R' Word, [PBDJ, Vol. 16, issue 12] and "Refactoring Classic PowerBuilder Applications Using TDD and pbUnit" [PBDJ, Vol. 17, issue 1) you read why refactoring code before migration helps ensure smooth migration and enterprise integration. You were introduced to Test Driven Development methodology and saw how you can use it to ensure successful refactoring. I introduced you to pbUnit, an open source tool and framework that you can use for both refactoring and developing new code in PB Classic applications, and guided you through installing pbUnit and mastering the basic algorithm for refactoring PB legacy code. In this article I'll show you a technique to use for rolling multiple test cases into a single test suite and how to approach writing test cases that exercise database-centric code.
Merging Common Code into a Test Suite
At the conclusion of my last article I guided you to write three test cases that characterized the behavior of the calculator application under three different sets of input values: two positives, a positive and a negative, and two zero values. After writing these three tests, a discerning programmer would have noticed that two sections of code were common to and repeated in each of the three tests: the initial setup code and the concluding tear down code. No doubt an astute object-oriented programmer would wonder: Could I have written these code segments one time? Could they have been automatically invoked at the proper time for every test in the series? For those who are just reading along, here's one test case with the common code in bold face:
w_calctester lw_calctester lw_calctester = Create w_calctester
lw_calctester.sle_years.text ='5' lw_calctester.sle_amount.text = '50000' this.assert( 'Positive Value Incorrectly Calculated', &
lw_calctester.sle_interest_paid.text ='$250,000.00' )
Granted this code is trivial, but in most production quality applications you will have many more steps to build, and to set the state for the complex memory structures you need to support the method under test; ditto for the code you need to clean up after a test run. Wouldn't it be useful to write the code once and have it automatically invoked?
For this purpose, the pbUnit
TestCase ancestor provides two "life cycle" events: SetUp and TearDown.
pbUnit invokes these events automatically before/after every test event call in a test case. You write code to build your memory structures in the SetUp event and write code to destroy them in the
TearDown event. pbUnit hands each test event its own isolated and individual memory structure, so there is no chance of having a "dirty" test structure. Figures 1-4 show what the test case code will look like after refactoring the test.
Figure 4: Test Case with SetUp and TearDown
Test Suite: Executing Multiple Test Cases in a Single Run
After a while the number of
TestCase objects in your collection will grow. You will find that you need to run several test cases as a sequence to cover a particular area of code. You might find yourself wishing that you could group multiple tests together and automatically run them as a sequence.
pbUnit provides a way for you to group TestCases together using a
TestSuite class. Internally,
pbUnit gathers all tests into a virtual TestSuite and runs them from there, calling SetUp and
TearDown from each test to guarantee isolation. You can formalize the
TestSuite and use it to your advantage. To construct a
TestSuite, inherit from the
TestSuite class in the constructor event for each test you wish to invoke the Initialize method, passing the testcase class name as a string parameter. Save the test suite class, giving it a name that starts with the word "Suite" so you can recognize it in the GUI. For the sake of clarity, organization and ease of use, you might create a library just for your suites. To run the entire suite as a sequence, select the suite from your test list and run it (see Figures 5, 6 and 7).
Figure 6: TestSuite constructor code
Figure 7: Running a TestSuite
To Mock or Not to Mock: Testing DB-Centric Code
TDD purists claim that tests that exercise database access code are not really unit tests, since they depend on an external entity. A purist would take the approach of building a mock data object that returns the data that would have been returned from a call to the database and in the test replacing the call to the database with a call to the mock object. Pragmatists, however, recognize that testing data access code can be valuable because exercising classes that interact directly with the database can guarantee these classes execute the right SQL statements, assemble the right objects, and so on. Although these tests depend on external entities, they exercise classes that are the basic building blocks of a big application. One important caveat is that you must ensure the database is in a known state before you run your tests. Another limiting factor is that for unit tests to be useful, they must run fast; database access can be relatively slow. In the Java world there is a robust framework called dbUnit that deals with the issues of database access testing. Currently there is no such framework for PowerBuilder. See Chapter 16 of jUnit in Action for a complete discussion of database testing with jUnit.
If you are going to test data access code using pbUnit, you must be aware that since each test case runs in isolation from all others, a connection established globally using
SQLCA will not be available to your test code. One approach would be to establish a connection inside each test case using the
TearDown events. However, you can intuitively predict that this will negatively impact test performance. A better way to configure a connection for multiple tests is establish it in the Constructor event of the
TestSuite class, before the calls to initialize your tests. You can even use the
SQLCA global variable. This will make a single connection available to all the tests in the suite. One important note: you cannot code the Disconnect after the initialize calls in the Constructor event. The constructor is invoked and completed before the tests are actually run. A Disconnect in the constructor will also be invoked, closing the connection before the tests are run. Therefore you must code your Disconnect in the TestSuite Destructor event (see Figure 8).
Figure 8: TestSuite with Database Connection
I hope the articles in this series have exposed you to the advantages and possibilities of improving your code quality via unit testing. I also hope they have whetted your appetite for including pbUnit or some other community or vendor-based framework in your tool arsenal. Tools like pbUnit can only grow and become accepted through community participation in their development. If you like pbUnit and can extend its functionality, please consider sharing your extensions. Of late the PowerBuilder community has been quiet, relying more on vendor-provided solutions than community-evolved ones. It is my hope that as we move more toward .NET integration, this trend will change and the community will once again become more active.
Recommended Additional Reading
These books, although not PowerBuilder centric will provide you with valuable insights into TDD and working with legacy code
- Kent Beck, Test-Driven Development by Example published by Addison-Wesley
- Michael Feathers, Working Effectively with Legacy Code, Prentice Hall
- Martin Fowler, Refactoring: Improving the Design of Existing Code, Addison-Wesley
- Tahchiev, Leme, Massol, and Gregory, JUnit in Action, Manning Publications Company