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.)
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.