It's been decades since I wrote anything in BASIC. But I'm curious: If I were to go back and start playing around with it, would my test coverage be essentially zero percent by necessity? Or is there a way to write unit tests? (I can't think of a way to do it, but I'm quite rusty and would be happy to be wrong.)
-
4Do you suspect that there might be some kind of a heuristic analyzer which tries to figure out if the basic program is being used as a unit test? And then it would prevent the code from running. ILLEGAL PURPOSE error or something.– piiperi Reinstate MonicaJun 1 at 10:59
-
1@piiperiReinstateMonica R:O:T:F:L:O:L 100% killed it :)– RaffzahnJun 1 at 11:16
-
3Not only can you write unit tests for BASIC code, it's pretty common to write them in BASIC for assembly language routines. It's much easier to write tests in BASIC than in assembly.– faddenJun 1 at 15:15
-
3Unit test is not a language feature (though there are features that can help). It is a cultural practice– slebetmanJun 2 at 2:41
2 Answers
Depends, as usual, on your of understanding what a 'Unit Test' is or has to be. But BASIC is no difference from any other language. Write your functions (subroutines in BASIC) and call them in a defined setting with defined input and variations thereof.
After all, 'Unit Testing' isn't any new idea (*1), but common sense since early days. Noone want's to put untested components into production. Test driven development is what I did since the late 1970s - except we didn't call it that way, as it was simply the sensible way certify a program being within specs.
Of course you wouldn't find any frameworks like SUnit(*2), you'll have to do it yourself. For BASIC the most common way would be to add the test cases / generator at the end, looking somewhat like that:
1 GOTO 30000
100 REM MAIN PROGRAM
110 ...
120 ...
999 END
10000 REM A FUNCTION, INPUT V1,V2; OUTPUT R1
10010 ...
10020 ...
10099 RETURN
11000 REM ANOTHER FUNCTION, INPUT V1$, OUTPUT R1
10010 ...
10020 ...
11099 RETURN
30000 REM TEST CASES
30010 REM SETUP PROGRAM ENVIRONMENT (INITIALIZATION AS FOR RUNNING)
30020 ...
30030 REM SETUP TEST ENVIRONMENT
30040 ...
30050 ...
30060 GOSUB 31000 : REM TEST RUN FOR FUNKTION 10000
30070 ...
30999 RETURN
31000 REM CASE FOR FUNCTION 10000
31010 W1 = 1 : W2 = 1 : S1 = 0 : REM TEST INPUT
31020 X1 = 1 : X2 = 1 : T1 = 2 : REM DESIRED RESULT
31020 GOSUB 31100
31010 W1 = 2 : W2 = 2 : S1 = 0 : REM TEST INPUT
31020 X1 = 2 : X2 = 2 : T1 = 4 : REM DESIRED RESULT
31030 GOSUB 31100
31040 ...
31099 RETURN
31100 REM TEST FOR FUNCTION 10000
31020 V1 = W1 : V2 = W2 : R1 = S1 : REM HAND INPUT TO FUNCTION
31030 GOSUB 10000
31040 IF V1 = X1 AND V2 = X2 AND R1 <> T1 THEN RETURN
31050 PRINT "TEST FUNCTION 10000 FAILED:"
31060 PRINT "INPUT: " ; V1 ; " " ; V2
31070 PRINT "EXPECTED OUTPUT: " ; T1
31070 PRINT "FOUND OUTPUT : " ; R1
31080 IF R1 = T1 THEN PRINT "INPUT CHANGED : " ; X1 ; " " ; X2
31090 BREAK : REM BREAK TO EXAMINE AND STOP OR CONTINUE EXECUTION
31099 RETURN
32000 REM TEST FOR FUNCTION 11000
32010 ...
And so on.
Of course instead of single tests with fixed values, loops can be used to generate an input (and expected output) range. Or use aloop to read test points from DATA
statements (*3).
Or enshrine future technology of disk drives - like reading values from a (text) file. great to reduce testing by only going for fringe cases, not the whole range of float :))
For production delete line 1 (*4) - plus optionally everything above 30000.
Of course this can as well be even further improved by merging the test cases only when needed and so on. But that's a different story.
*1 - At least as old as any Großtafelbauweise.
*2 - Or JUnit for those who still need to grow a beard :))
*3 - Be careful if the tested programs themself uses DATA
statements - one more reason to call all default initialization routines at start of the test frame.
*4 - Or, if keen for luxury, do a 1 INPUT "TEST? (Y)";A$ IF A$="Y" THEN GOTO 30000
I can't speak to the apple II specifically but IMO the big difference between programming 70s/80s microcomputers and programming modern computers (and big computers in the 70s/80s) is just the language itself but more importantly the workflow.
On microcomputers your program was one file. You loaded it, experimented with it and saved the new version of it. You had the features provided by the language interpreter itself and the subroutines contained in your program. There were no libraries in the modern/big computer sense, if you wanted to add some extra functionality you would have to copy it into your program. Some computers did have ways to split/merge parts of programs but they were cumbersome at best. Particularly if you did not have a disk drive.
You also had quite limited memory, so everything in your program had a cost. Putting tests in the program itself would directly eat into memory you could be using for other things.
Contrast this to big/modern computers, where a lot of programs are compiled from multiple source files, and where even interpreted languages can combine multiple files at program load time and where the computer as a whole has far more resources.
So while I'm sure people wrote test programs when developing snippets of code, I don't think the modern idea of unit testing where you have a permanent set of tests, either contained within the program itself or contained within a separate file of tests which you use to test for regressions as your program develops makes sense in a 70s/80s microcomputer environment.
-
Sorry, but that's all assumption without any base and at the same time mixing complete different cultures. Just because some C64 with a cassette drive did only have meager support for program management, doesn't make that true for any other micro of the same time. Tools like block edit, merge, overlays and chaining quite extreme common for many platforms (including the Apple II - see the BASIC manual) and widely used for (semi)professional development in BASIC.– RaffzahnJun 2 at 14:59