Why small programs
When a big program gives the wrong answer, the fault could be in the program, in a rarely used instruction, in wrap-around, in input handling, or in several of them at once. A large test tells you something is wrong and little else.
A tiny program does the opposite. If a five-cell program that adds two numbers fails, the cause is almost certainly in a handful of places. Small tests are cheap to write, quick to run and easy to read, and together they become a safety net that lets you change the interpreter with confidence.
Set up a simple harness
Keep each test as a pair of files: the program and its expected output. For instance, add.bf and add.out. A short script runs the interpreter on each program, captures the output and compares it. Report which tests failed and show the difference. If a program needs input, store that in a third file.
Make the interpreter easy to drive from the script: accept a source file, read input from standard input, write output to standard output, and exit with a clear status. A step limit is also valuable, so that a test with a bug that loops forever fails instead of hanging the whole run.
Write the expected output by reasoning about the program, not by copying what your interpreter printed. Otherwise a bug becomes part of the expected result. For anything doubtful, work it out with tracing by hand first.
The ladder
Order tests so that each rung relies only on rungs below it. If a low rung fails, ignore the higher ones until it is fixed.
Rung 1: Stopping and moving. A program that is only the stop instruction should halt and print nothing. Then test each of the four direction arrows: move right, then turn down and stop; and so on for the others. Test a straight path across the whole row to confirm that the pointer starts at the top left heading right.
Rung 2: Wrap-around. A pointer that leaves one edge must reappear on the opposite one. Make four tiny programs, one for each edge, with the stop instruction placed so that it is reached only if the wrap works. Because this is where off-by-one errors live, include a grid with unequal width and height, so mixing up the two dimensions shows up.
Rung 3: Output. Print one character using a known code, then print a number. Check the exact output, including any trailing space the language defines.
Rung 4: Pushing and the stack. Push a digit and print it. Push two and print both, which shows the order. Test duplicate, swap and discard, each with enough values on the stack and then with too few, to cover stack underflow behavior.
Rung 5: Arithmetic. One program per operator, with simple positive numbers first. Then add the awkward cases: division by zero, negative remainders and large values near the integer limits, as listed under arithmetic edge cases. Check operand order with subtraction and division, because reversed operands are a classic mistake.
Rung 6: Branching. Test the horizontal and vertical conditionals with zero and with non-zero values, and confirm that each sends the pointer the expected way. A program that prints different characters on each path makes the outcome visible.
Rung 7: String mode. A quoted string should push each character in order. Printing it back shows whether the order is right. Include the edge case of a quote that is never closed before the pointer wraps.
Rung 8: Reading and writing cells. Write a character to a cell, read it back, and print it. Then write an instruction into a cell the pointer will later reach, which tests self-modification. If you cache decoded cells, this test is essential.
Rung 9: Input. Read a character and echo it. Read a number and print it. Test end of input with an empty input file. Mixed reads, such as a number followed by a character, show whether both use the same buffer.
Rung 10: Small complete programs. Finally, run a few real programs whose output you know: a hello world, a countdown, a factorial for small values. If the earlier rungs pass and one of these fails, you have found an interaction between features, and the trace will help locate it.
Keep what fails
Every time you find a bug by other means, add a test that reproduces it before you fix it. Over time the test folder becomes a record of every mistake the interpreter has ever made, which is the best protection against its repeating.
Tests as documentation
Name each test after the behavior, not the instruction, for example wrap-left-edge or divide-by-zero-pushes-zero. A new reader can then see your decisions about each edge case just by listing the folder. If you document behavior differences from the reference language, the tests are the proof.
Use known programs carefully
Published example programs for the language are good extra tests, but treat them as secondary. They exercise many features at once, and a failure does not point at a cause. Use them after the ladder passes. Also remember that some programs rely on behavior that differs between versions, so the right comparison is with the specification you chose.
Fuzzing and comparison
If you have a trusted second interpreter, you can generate random small programs and compare the outputs of both. Differences reveal edge cases you did not think to write down. Limit steps and avoid input-dependent programs at first. This is an extra, not a replacement for the ladder.
When to run the tests
Run them after every change, however small. A suite of a hundred tiny programs should finish in a few seconds. Before any optimization, as described under interpreter performance, make sure everything is green; afterward, confirm it still is.