Wednesday, December 30, 2009

Creating A Fortran Unit Testing Framework (part 3): The First Routine

The Form of Our Assertions


We’ll begin by implementing ASRTT. However, it will take us a bit of time to get there. One point that wasn’t made clear in the previous post is whether our assertions are subroutines or subroutine functions. The main difference is that subroutine functions return a value, while subroutines do not. Functions are used to return a single value, while subroutines are return no result. Subroutines are also used when we want to return multiple values. Fortran uses pass by reference semantics.

In our case, the chief difference is one of usage:

  1. LOGICAL OK
    OK = ASRTT(.TRUE.)
  2. CALL ASRTT(.TRUE.)
  3. LOGICAL OK
    CALL ASRTT(.TRUE., OK)

In the first and third versions, the logical variable OK holds the result of the test. In the second version, we ignore the result of the test; the subroutine doesn’t return any value.

The first form seems more natural in terms of the intent of the assertion-—returning .TRUE. or .FALSE. according to the assertion. However, Fortran requires that we always assign the result of the function call to some variable. In most other languages, we can ignore the returned value and write

ASRTT(.TRUE.).

This won’t compile in Fortran; we have to assign the result:

OK = ASSTT(.TRUE.)

The third format removes the need to assign the result. However, it makes the signature of the subroutine more complicated, requiring the user to add a LOGICAL variable to the list of arguments.

The second form is closest to the usage in xUnit frameworks. We generally process the test outside of the main flow (either by updating a data structure or by throwing an exception). Handling the outcome of the test is the work of the framework, and shouldn’t intrude into the user’s code by requiring a dummy variable to accept the return value from a function.

I went back and forth several times, but eventually selected the second form. It is closest to other xUnit frameworks. It has the simplest semantics in using the framework, and so is the least intrusive to the user. Finally, it reflects a proper division of labor by pushing the complication of handling the result of the assertion onto the testing framework where it belongs.

Test-Driven Development

I’m going to take my own advice and write unit tests as we develop the framework. Each aspect of the framework will have one or more tests associated with it, verifying that it works. These tests will be written before each routine is added, a practice called “Test-driven development” (TDD). The idea behind TDD is that writing the tests before the code gives two advantages:

  1. We make sure that the code implements only the functionality that is needed. If it isn’t required to make one of the tests pass it doesn’t get written.
  2. By forcing us to write code that exercises the routines that we are developing, we create an application programming interface (API) that makes sense from the outside, and therefore is simpler to use. An API is the collection of subroutines and functions that can be called by outside code.
  3. We have a suite of tests that allow us to refactor with confidence. Our test suite will need maintenance, just like any other code, and so we are kind to ourselves by writing those tests now.
  4. Writing the test at the same time serves as a form of documentation. The tests show how the framework’s API is intended to be used.
  5. Writing the tests at the same time as the code, the tests are in sync with the code; we don’t have to recall what we were thinking days (or weeks, or months) later.

The philosophy of TDD goes beyond what we will explore here. There are many who feel that TDD is an inappropriate name. They have rechristened it “Behavior-driven development,” to shift focus from the “tests” to the specification of the “behavior” of the code being written. This difference in focus leads to some interesting differences in outlook and focus. I encourage you to take a look at Dan North’s blog introducing BDD, and the behavior-driven development site (note the spelling "behaviour", rather than the American "behavior"). There are several open source projects dedicated to BDD. The oldest/best known are JBehave and easyB for Java, and RSpec for Ruby are a few of the most active. There is also a book coming out on RSpec (currently available in "beta") that is well-worth reading.


Implementing RESCHR Using TDD

The simplest, most basic piece of functionality that we want is to print out “.” if the test has passed, and “F” if the test has failed. So how do we test that? We break it into 2 pieces:

  • A routine that chooses “.” If it receives a .TRUE. argument and “F” if the argument is .FALSE. This second routine will then call the first routine with the correct character.
  • A routine that prints out the results of the test

We begin by assuming that we have a function that does what we want: RESCHR (for result character). It takes a LOGICAL and returns the correct character. The final twist is that FORTRAN 66 doesn’t have characters. Instead, we have to encode the character as a Hollerith constant in an integer, using something like



J = 1HF


We begin by writing a test to verify that expectation.




      PROGRAM TCASES

      CALL TSTCHR

      STOP

      END

C

C TEST CASE FOR THE ROUTINE THAT RETURNS THE CORRECT

C CHARACTER TO PRINT OUT

      SUBROUTINE TSTCHR

      INTEGER RESCHR

      INTEGER ACTUAL

      INTEGER EXPECT

      EXPECT = 1H.

      ACTUAL = RESCHR(.TRUE.)

      IF (EXPECT .EQ. ACTUAL) GO TO 100

      WRITE(6, 1000)

      GO TO 300

  100 CONTINUE

      WRITE(6, 2000)

  300 CONTINUE

      EXPECT = 1HF

      ACTUAL = RESCHR(.FALSE.)

      IF (EXPECT .EQ. ACTUAL) GO TO 400

      WRITE(6, 1000)

      GO TO 500

  400 CONTINUE

      WRITE(6, 2000)

  500 CONTINUE

 1000 FORMAT(1HF, $)

 2000 FORMAT(1H., $)

      RETURN

      END




Basically, we check that the integer returned matches our expectation. The return value ACTUAL is compared to EXPECT. If they agree, a "." is printed; if they don’t agree "F" is printed. The WRITE statement uses a hardcoded reference to unit 6 (the default output), which is the screen on my laptop. [1].

The next step in TDD is to do the minimum possible to get this to compile and see the test fail. This step seems silly. I know the test is going to fail. However, when we are testing more complex code, I have had tests that should fail pass. It is rare, but always provides extremely valuable feedback; there is something important that I don’t understand, and I need to figure that ASAP. Anyway, the minimal code is:




C

C RETURNS THE CHARACTER '.' IF THE ARGUMENT IS .TRUE.

C 'F' OTHERWISE

      INTEGER FUNCTION RESCHR(A)

      RESCHR = 1HX

      RETURN

      END


So, I compile and run, and the test does indeed fail; I get



FF



as output. The next step is to get the test to pass. We change RESCHR:




C

C RETURNS THE CHARACTER '.' IF THE ARGUMENT IS .TRUE.

C 'F' OTHERWISE

      INTEGER FUNCTION RESCHR(A)

      LOGICAL A

      IF (.NOT. A) GO TO 100

        RESCHR = 1H.

        GO TO 200

  100 CONTINUE

        RESCHR = 1HF

  200 CONTINUE

      RETURN

      END


And we are rewarded with passing tests:


..



(trust me, it is much more exciting in person…)

The final step is to tighten up the code—refactor. I pull the constants 1HF and 1H. into variables, to make the code more communicative:




C

C RETURNS THE CHARACTER '.' IF THE ARGUMENT IS .TRUE.

C 'F' OTHERWISE

      INTEGER FUNCTION RESCHR(A)

      INTEGER PASS, FAIL

      DATA PASS/1H./, FAIL/1HF/

      LOGICAL A

      IF (.NOT. A) GO TO 100

        RESCHR = PASS

        GO TO 200

  100 CONTINUE

        RESCHR = FAIL

  200 CONTINUE

      RETURN

      END




I run the tests, and both still pass. The other piece is to tighten up the test code as well. We are going to be running this a lot, so we might as well be nice to ourselves.




      PROGRAM TCASES

      CALL TSTCHR()

      STOP

      END

C

C TEST CASE FOR THE ROUTINE THAT RETURNS THE CORRECT

C CHARACTER BASED ON THE VALUE OF THE TEST

      SUBROUTINE TSTCHR()

      INTEGER RESCHR

      INTEGER I

      INTEGER PASS

      INTEGER FAIL

      INTEGER ACTUAL

      INTEGER EXPECT

      LOGICAL VALUES

      DIMENSION EXPECT(2)

      DIMENSION VALUES(2)

      DATA (VALUES(I), I=1,2)/.TRUE., .FALSE./,

     &    (EXPECT(I), I=1, 2)/1H., 1HF/,

     &    PASS/1H./, FAIL/1HF/

  DO 300 I = 1, 2

      ACTUAL = RESCHR(VALUES(I))

      IF (EXPECT(I) .NE. ACTUAL) GO TO 100

        WRITE(6, 200) PASS

        GO TO 300

  100 CONTINUE

        WRITE(6, 200) FAIL

  200 FORMAT(A1, $)

  300 CONTINUE

      RETURN

      END




There are three things about this code that may seem unusual. One is the empty parentheses after the call to TSCHR. This is not idiomatic Fortran. However, I like to do it because it is consistent with other languages, and makes the calls stand out more in the code. Secondly, I have put each variable’s definition on a separate line. This makes the listings somewhat longer, but also makes it easier for me to find each variable, or to change their type if I need to. Similarly, the DIMENSION statements are on their own line, for the same reason. These are simply my stylistic, idiosyncracies; do whatever feels right for you.

One thing I do suggest is that you explicitly declare every variable, rather than relying on Fortran’s implicit typing. I have been bitten by this repeatedly, and strongly recommend it. Even more useful is the IMPLICIT NONE in later versions of the language. Why? When (not if) I misspell a variable’s name, the compiler blissfully ignores my blunder and creates a new variable. These kinds of typos can be a nightmare to track down, because I tend to see what I intended to type, rather than what is actually there.

We looked at whether to make our assertions functions or subroutines. I somewhat arbitrarily opted for subroutines, largely because Fortran demands that I assign the results of a function to a variable. I find that an annoying intrusion, so I went with subroutines

We took a look at test driven-driven development. Here the tests are written before the actual code. In my humble opinion, TDD has several advantages, but the two that are most important to me are:

  • creating the tests forces me to think through what the routine should do. If I can’t figure out how to test it, then I don’t understand it well enough to code it. The example we looked at was trivial, but as we get into more complicated cases, my experience is that it is well-worth the effort to figure out what the code should do before I write it.
  • TDD also makes us look at the code from the point of view of a user of our routines (from the outside), rather than focusing simply on how they work (from the inside). This seems like a trivial difference, but I have found that it consistently helps me write better designed code.

Finally, we used our knowledge of TDD to create our first tiny piece of the framework, the RESCHR subroutine. We

  1. Wrote the test.
  2. Wrote the minimum code to get the test to run and fail.
  3. Write the minimum code to get the test to pass.
  4. Refactored the framework code to make it cleaner, ensuring all along that the tests still passed.
  5. Refactored the test code, again making sure that all of the tests passed.

Next time we’ll develop a bit more, reinforcing these steps in a slightly more complicated setting.



[1] Unit 6 is typically mapped to the default output device, and I will assume throughout this series that we are using a version of Fortran that allows us to specify file names and descriptors directly. If you are working in a different environment (IBM mainframe for example) you know only too well that you have to map the units in a different way, such as JCL. You have my deepest condolences.

No comments:

Post a Comment