Friday, January 1, 2010

Creating A Fortran Unit Testing Framework (part 4): Outputting the Test Result

In this installment, we are going to implement the next set of functionality, wiring out the results of the tests. Recall that last time we wrote RESCHR, the routine that determined what to print out ("." or "F") depending upon the result of the test. Our next piece of functionality will be to write that character out, a routine we call RESOUT.
As with RESCHR, we will write RESOUT using test-driven development TDD). The first problem is how to figure out whether something is printing correctly. The simple answer is to just print it to the screen and look at it (and frankly is the very first approach I use). The problem with this approach is that it requires us to look at the output for every test every time we run them. This gets old in a hurry. To be really effective, unit tests must be quick and easy to run. The more effort required to run and evaluate them, the less often they will be used. So, we want the testing framework to automatically figure out whether or not the right thing is being printed out.
To make this automatic, we need to print to a place other than the default output and then compare what was written to our expectation. Most modern languages have some sort of internal representation of a string or file. In FORTRAN 66, we could use Hollerith fields encoded into integer arrays to achieve that effect, but it seems way too much effort of this particular job (we'll tackle that another day). Instead, I'm going to take the easy way out. I'll create a scratch file, write out to it, read the results back in, and compare them to my expectation. After a couple of false starts, here is the test that I came up with:



C
C TEST CASE FOR THE ROUTINE THAT PRINTS OUT THE
C CHARACTER BASED ON THE VALUE OF THE TEST
      SUBROUTINE TSTOUT()
      INTEGER I
      INTEGER PASS
      INTEGER FAIL
      INTEGER RESULT
      DIMENSION RESULT(2)
      DATA PASS/1H./, FAIL/1HF/
      OPEN(UNIT=10, STATUS='SCRATCH')
      CALL RESOUT(.TRUE.)
      CALL RESOUT(.FALSE.)
      REWIND(10)
      READ(10, 3000) (RESULT(I), I = 1, 2)
      IF (RESULT(1) .EQ. PASS) GO TO 100
          WRITE(6, 1000)
          GO TO 200
  100 CONTINUE
          WRITE(6, 2000)
  200 CONTINUE
      IF (RESULT(2) .EQ. FAIL) GO TO 300
          WRITE(6, 1000)
          GO TO 400
  300 CONTINUE
          WRITE(6, 2000)
  400 CONTINUE
 1000 FORMAT(1HF, $)
 2000 FORMAT(1H., $)
 3000 FORMAT(2A1)
      RETURN
      END



The next TDD step is to write our minimal code that will compile, and cause the test to fail:



C
C WRITES OUT '.' IF THE ARGUMENT IS .TRUE.
C 'F' OTHERWISE
      SUBROUTINE RESOUT(A)
      LOGICAL A
      WRITE(10, 100)
  100 FORMAT(2HXX)
      RETURN
      END



We run it, and see the expected output:


FF



Next, we remove the hard-coded unit (10), and take it an argument to the routine, and revise the code to make the tests pass (I actually do these in separate steps, but from the last post you get the idea of TDD, so I'm picking up the pace a bit):



C
C WRITES OUT '.' IF THE ARGUMENT IS .TRUE.
C 'F' OTHERWISE
      SUBROUTINE RESOUT(A, OUTPUT)
      LOGICAL A
      INTEGER OUTPUT
      IF (A) GO TO 200
          WRITE(OUTPUT, 100)
  100     FORMAT(1HF, $)
          GO TO 400
  200 CONTINUE
          WRITE(OUTPUT, 300)
  300     FORMAT(1H., $)
  400 CONTINUE
      RETURN
      END


And the output becomes:


..


indicating that both of the tests now pass.

Now, we can leave this version as it is, and some purists would say we should. However, those purists would be wrong. As it stands, RESOUT replicates the logic of RESCHR (choosing which character to print, based on the value of the LOGICAL argument A. The replication is a violation of the "Don't repeat yourself" rule (know as DRY, from The Pragmatic Programmer). There is no reason to have that code twice, so we modify RESOUT to make use of RESCHR routine that we developed last time. So, our final version of RESOUT becomes:



C
C WRITES OUT '.' IF THE ARGUMENT IS .TRUE.
C 'F' OTHERWISE
C
C LOGICAL A: WHETHER OR NOT THE TEST PASSED
C INTEGER OUTPUT: THE UNIT TO WRITE THE RESULT TO
C
      SUBROUTINE RESOUT(A, OUTPUT)
      LOGICAL A
      INTEGER OUTPUT
      INTEGER RESCHR
      INTEGER OUTCHR
      OUTCHR = RESCHR(A)
      WRITE(OUTPUT, 100) OUTCHR
  100 FORMAT(A1, $)
      RETURN
      END


We run this, and again the tests pass.
Finally, we tighten up the test code:



C
C TEST CASE FOR THE ROUTINE THAT PRINTS OUT THE
C CHARACTER BASED ON THE VALUE OF THE TEST
      SUBROUTINE TSTOUT()
      INTEGER I
      INTEGER OUTPUT
      INTEGER EXPECT
      INTEGER RESULT
      DIMENSION RESULT(2)
      DATA (EXPECT(I = 1 ,2))/1H., 1HF/, OUTPUT/10/
      OPEN(UNIT=OUTPUT, STATUS='SCRATCH')
      CALL RESOUT(.TRUE., OUTPUT)
      CALL RESOUT(.FALSE., OUTPUT)
      REWIND(10)
      READ(10, 100) (RESULT(I), I = 1, 2)
  100 FORMAT(2A1)
      DO 600 I = 1, 2
          IF (EXPECT(I) .EQ. RESULT(I)) go to 300
              WRITE(6, 200)
  200         FORMAT(1HF, $)
              GO TO 500
  300     CONTINUE
              WRITE(6, 400)
  400         FORMAT(1H., $)
  500     CONTINUE
  600 CONTINUE
      RETURN
      END


No comments:

Post a Comment