Intro To The VFPUnit Workbench. Part 2
Open the experiment 'A' from the first example, if it is not currently open.
Press the 'New From Copy' button to create a new experiment, by making a copy of this experiment.
Change the name of the test to "B", or whatever you want.
By making a copy, we automatically are assigning it to the "Tutorial" Suite.
Again, Open the test code editing window by pressing the 'Test Code' button adjacent to the Test code edit box.
Now, we are going to examine using expressions as the first arguments of TEST.
Formally, we can view this as:
LOCAL llResult
llResult = {expression}
TEST(llResult, "Did Not See Expected Result")
Often, it is clearer to state the problem this way. Especially during initial test writing.
i.e. Collect the result of an expression, and then in a separate step, call TEST passing the intermediate result variable.
But usually, we short-hand the notation to be:
TEST({expression}, "Did Not See Expected Result")
* 1 line of code may be better than 3. -depends upon the clarity or simplicity of the expression.
So, delete some sample code lines, add some lines:
Complete so that code is:
| |||
lnX = _screen.formCount LOCAL llResult * _screen.formCount > 0 is an expression that is .T. if any windows are open, .F. if not. llResult = _screen.formCount > 0 * expect to see at least the VFPUnit forms. TEST(llResult, "No Forms Are Open" +KCR +"Should See At Least One!") | |||
Press the 'Save and Run' button. A single result should be added, that simply says Ok. The failure message only shows up on the Details form, and then, it is grayed out. | |||
Note the subtle difference in the context of the expression and the failure message. The expression is worded toward the positive, as in: 'Check that this expression is True'. While the message is worded toward the negative. as in: 'This Went Wrong'. It is the difference between Expectation vs. Explanation. It's kind of corny, but the easiest way of remembering this difference when creating a test is to remember: "Test For Truth, Scream Injustice" You want the failure message to tell you what is wrong, remember, it is an aid for future debugging. Just for kicks, here's some more code to try out. Copy and paste right into Sample window if you wish. I've bypassed the llResult variable, and put the expression directly in the first argument position. | |||
lox = CREATEOBJECT("Form")
TEST( VARTYPE(lox) = 'O' .AND. !ISNULL(lox), 'Unable to create a simple Form')
lox.caption = 'Hello'
lox.visible = .T.
TEST( WVISIBLE( lox.name), lox.caption +' Should Be Visible')
wait window 'You should see a form' time 3
lox.visible = .F.
TEST( !WVISIBLE( lox.name), lox.caption +' Should Not Be Visible')
wait window "And now you don't" time 3
| |||
We think abstractly. It's what we do. Keep it up, good job. But not here. A test is as specific as humanly possible. there should be no ambiguity whatsoever. Your program code is generic, the tests are not. For example: You are asked to write a financial calculation. You are either given a set of specs, or you search out the users that know, and collect as much information as you can. You sit back and think for a while, do the use cases, sketch some pictures.., do what ever you need to get ready to code. Here is where the tests come in. Express the requirements as concretely as possible. Conceptually, we could start with something like this: *- Topic: Interest paid on an imaginary account : $ 5.25 should be paid on a $1000 account at 5.25% interest, for one year. $10.50 should be paid on a $2000 account at 5.25% interest, for one year. $0 should be paid on a $0 account at 5.25% interest, for one year. which, when translated into a format that the workbench can use, become: | |||
*- Interest amount is rate * account amount * duration (in months) TEST( 5.25 = InterestCalc( 5.25, 1000, 12) ) TEST(10.50 = InterestCalc( 5.25, 2000, 12) ) TEST( 0 = InterestCalc( 5.25, 0, 12) ) | |||
| |||
go ahead and run the experiment. You know the test will fail. we want it to. | |||
This brings up 2 major points. 1) All tests should fail when first created. If we are truly adding or changing something, we would expect that the behavior does not already exist. Wouldn't you be suspicious if the test passed? -that would mean that you already have an interestcalc() function, loaded into memory, magically accepting 3 parameters, and returning the correct values. It is much like establishing the baseline to test that we are really effecting a change. 2) The failures will direct future development. What the results window is telling you is that you need an interestcalc() function, and that it takes 3 arguments. In a way, we are making (what are often thought of as trivial) decisions about names. function/method names, object names, variable names.., at the place where it is most appropriate to determine their behavior, and not during coding, where we tend to name things based on mechanism. This feature of directing development, is one of the most powerful aspects of this method. It means that, if you can write a test, the framework will tell you how to code it! | |||
But this feels unnatural. It's too simplistic, almost obvious. But on the other hand, it is the one place where you can find concrete evidence of behavior, rather than abstract descriptions of what it -should- do. I still find it hard to adjust to that way of thinking, after all, we've all spent years putting values into variables, and then operating at that level of abstraction. We now want to take the values out of the variables. -at least for here. | |||
for now, let's assume that there is more behavior, and write a few more tests that expose some imaginary needs. In addition, we'll add the failure messages and a few more boundary conditions. | |||
*- Interest amount is rate * account amount * duration (in months)
TEST( 5.25 = InterestCalc( 5.25, 1000, 12), 'Interest not rate * amount * duration')
TEST(10.50 = InterestCalc( 5.25, 2000, 12), 'Interest not rate * amount * duration')
TEST( 0 = InterestCalc( 5.25, 0, 12), 'Interest not zero for zero accounts')
*- these fall into the 'it will never happen' category.
*- (but they always seem to).
TEST( 0 = InterestCalc(-5.25, 1000, 12), 'Interest not zero for negative rate')
TEST( 0 = InterestCalc( 5.25,-1000, 12), 'Interest not zero for negative account')
TEST( 0 = InterestCalc( 5.25, 1000, -12), 'Interest not zero for negative duration')
TEST( 0 = InterestCalc(-5.25,-1000, 12), 'Interest not zero for negative rate, account')
TEST( 0 = InterestCalc(-5.25, 1000, -12), 'Interest not zero for negative rate, duration')
TEST( 0 = InterestCalc(-5.25,-1000, -12), 'Interest not zero for negative rate, account, duration')
TEST( 0 = InterestCalc( 5.25, 1000, 0), 'Interest not zero for new accounts')
** this last test suggests a whole series of questions of what 'duration' is.
** from the current specs, it appears to be based on whole months.
** but, in writing the test, it soon becomes apparent that that it is easier said than done.
** In real life, we track things based on dates.
** so, we need something similar to this format:
TEST( 5.25 = InterestCalc( 5.25, 1000, {1/1/2000} - {1/1/1999} ),'Incorrect Month Calculation' )
*(my apologies to my international friends. date format used is MDY)
| |||
Testing on boundary conditions will often point out areas not considered in the initial design, which may cause a refinement of the specs. Testing unexpected conditions gives a level of predictability, accountability and stability, at the low cost of a few copy and pastes. At minim mum, you will have at least thought about what would happen when a rogue program passes a bad value. | |||
Clearly, the last test is incompatible with the first group. It is passing duration as days, and not months. There are several possible courses of action: 1) require that only months be used in the calculation. 2) change the tests so that all pass duration as days. 3) have the function determine if months or days was passed, and then convert appropriately. All of these are valid possibilities, and all exist is some form in a major system somewhere in the world. (with varying amounts of success) What we've really done, is to have identified another problem, that of duration calculation. In practice, this may be a point where the design was revisited, for there is ambiguity either in the design, as to how to calculate duration, or in my understanding of the design. So, after talking to the users, they have informed me that months are the appropriate unit of duration. Having anticipated that, I also pried out the following information: "We determine this interest the first of every month. We take accounts that have money in them on that day, and count the number of full months backwards, until we hit the account start date. If the start date was not on the first day of that month, they don't get credit for that month. Too bad. Only full calendar months are counted." (note: this is option #1 above). Seeing this as a separate problem lets me write tests on that specific problem. i.e. We introduce a 'BizMonths()' calculation, and change the last test to: | |||
* TEST( 5.25 = InterestCalc( 5.25, 1000, {1/1/2000} - {1/1/1999} ),'Incorrect Month Calculation' )
TEST( 5.25 = InterestCalc( 5.25, 1000, BizMonths({1/1/1999}, {1/1/2000}) ),'Incorrect Month Calculation' )
* note: cannot use just elapsed days in month calculation, for different months have
* different number of days, so we need start and end dates.
* also, I flipped the order of the parameters passed, into the more conventional
* BizMonths(start, end) format
| |||
Now, we can write test that exercise the BizMonths calculation. | |||
TEST(12 = BizMonths({1/1/1999}, {1/1/2000}), '1 calendar year not seen as 12 months')
TEST( 0 = BizMonths({1/1/1999}, {1/1/1999}), 'same day seen as a month or more')
TEST( 0 = BizMonths({1/15/1999}, {2/15/1999}), 'must calculate complete calendar months.')
TEST( 0 = BizMonths({1/1/1999}, {1/15/1999}), 'partial month seen as a month')
TEST( 1 = BizMonths({1/1/1999}, {2/15/1999}), 'partial month rounded up')
TEST(13 = BizMonths({1/1/1999}, {2/15/2000}), 'partial month rounded up')
*- these could be interesting..,
TEST( 0 = BizMonths({2/1/2000}, {2/29/2000}), 'leap year feburary rounded up')
TEST( 0 = BizMonths({1/1/2000}, {1/1/1999}), 'Invalid format [end before start] seen as valid')
* and so on...
| |||
If there is some overlap or redundancy in the tests, -so what-. Better to be thorough. Feel free to comment the tests fully, it may help you or a coworker understand your line of thought at a later date. | |||
By extension, you can see that all of the parameters to InterestCalc() can be treated accordingly, and we could get something like: InterestCalc( BizRate(), BizAccountAmount(), BizMonths(startdate, enddate) ) with corresponding tests for BizRate() and BizAccountAmount(). Construction of those tests are left as an exercise to the reader, but you will have to make up your own specs. | |||
-need a table to perform the tests with? Make one. hardcode the full name. -It's not meant to be portable. Need to share that table? voila- the need for a test data bed. If you saw this coming, make a copy (of a subset) of your production data, and hardcode that name in the tests. The hassle of setting up or changing the location of a test data bed that is hard coded in, is a lot less than repairing the production data and undoing the decisions that where made on that bad data, when it gets modified. If there is the chance that it could be changed by testing, it will be. | |||
So far, there hasn't been any actual working code, and what we do have feels a lot like 'old fashioned' procedural coding. i.e. where are the classes? Aren't we supposed to 'use objects'? You are right. All we have done is explore and express functional requirements. And, we have expressed them in a syntax that requires 'public functions', like those found in procedure files. Before you scoff at the 'old fashioned' notion of procedure files, let me remind you that they are alive and well, and often (unfortunately) the main business component of modern systems. The only difference is that yesterday we called them libraries or procedure files, and today we call them stored procedures (of a database). I'm not advocating implementing BizRate(), BizAccountAmount(), and BizMonths() as stored procs, but then again, I wouldn't be surprised to find their equivalents as SPs in a system. But that brings up the subject of packaging. | |||
| Next | Previous | Main |
Generated 03/11/02 01:49:41 PM |