Verdict
No
Do not test a main method that only builds objects from arguments of different types and calls one entry point; move argument parsing and exit code logic into a function that you unit test.
Why
- Blast radiususers
- Change frequencyrarely
- Detectabilityimmediately
- Reversibilitytrivial
- Test costmoderate
I do not test a main method that only builds objects from arguments of different types and calls one entry point. Detectability is immediately: a missing or swapped argument fails to compile or stops the program before it serves anything, so the next local run shows the error. Reversibility is trivial, because a health-checked deploy keeps the old version running. Blast radius is users and Change frequency is rarely, since main changes when the program gains a dependency. Test cost is moderate, because a test of main starts the whole program or has to intercept System.exit. Rule R8 gives Do not test.
| When | Decision | Why |
|---|---|---|
| The main method parses command-line flags and picks defaults, such as the date a report covers | Test: move the parsing into a function and test each flag and each default | Detectability falls to eventually and Test cost to trivial: a wrong default date produces a plausible report |
| The main method of a scheduled job sets the exit code that the scheduler reads | Test: extract a run function that returns the exit code, and assert that a failure returns non-zero | Detectability falls to eventually, Reversibility rises to with-effort and Test cost falls to trivial: the scheduler records a failed run as a success |
| The deploy replaces the running version without a health check | Test minimally: one CI test that starts the application context, such as an empty @SpringBootTest test | Detectability falls to same-day and Reversibility rises to with-effort: an alert reports the outage, and the service stays down until someone reverts |
| The main method of a nightly billing job picks the day to charge from its arguments | Test mandatory: extract the date choice and test the default, an explicit date and a rerun of one date | Blast radius rises to money, Detectability falls to eventually and Reversibility rises to costly: a repeated date bills customers twice |
| A Python main function takes argv and returns an exit code, so a test takes minutes | Do not test a Python main that only wires objects from arguments of different types, even when the test is cheap | Test cost falls to trivial, but Detectability stays immediately, because a broken main fails on its first run |
| The main method passes several values of one type to one constructor, such as the connect and read timeouts of an HTTP client | Test: move the wiring into a function and assert which value each parameter receives | Detectability moves to eventually and Test cost falls to trivial: a swapped value of the right type starts an application that looks healthy |
What breaks if you don't test
A main method breaks when a dependency is missing or built in the wrong order. The program throws on start: the JVM prints a stack trace, or a container restarts in a loop. The health check refuses the new version before a customer reaches it.
What you lose if you over-test
A test that calls main(new String[0]) starts the real program. It opens database connections, binds ports, and breaks every time main gains a dependency. A System.exit call in main ends the test JVM, and the old workaround, a custom SecurityManager, stopped working when Java 24 disabled the security manager. The test then checks only what the first local run already showed.
What to do instead
Keep main to one call and to wiring whose arguments differ in type, and move every decision into a function that returns a value. In Python, the __main__ documentation recommends sys.exit(main()) under a short __main__ guard. Unit test the extracted function. The compiler, a local run and a health-checked deploy cover the wiring.
When the answer changes
- Main parses flags, picks defaults, sets the exit code a scheduler reads, or passes several values of one type to one constructor, so it can be wrong without a crash.
- Main picks an implementation per environment, so a production-only branch first runs in production; give it the startup test from the configuration answer.
- The deploy has no health check, so a program that fails to start replaces one that worked.
Code example
A nightly export split into main and run
public final class ExportJob {
public static void main(String[] args) {
System.exit(run(args, new DatabaseExporter(Database.fromEnv()), LocalDate.now()));
}
static int run(String[] args, Exporter exporter, LocalDate today) {
LocalDate day = args.length > 0 ? LocalDate.parse(args[0]) : today.minusDays(1);
try {
exporter.export(day);
return 0;
} catch (ExportException e) {
System.err.println("export failed for " + day);
return 1;
}
}
}
@Test
void failedExportReturnsNonZero() {
Exporter failing = day -> { throw new ExportException("disk full"); };
assertEquals(1, ExportJob.run(new String[0], failing, LocalDate.of(2026, 9, 15)));
}
The test pins the one value the scheduler reads: a failed export exits with 1. The line in main stays untested, because a missing environment variable in Database.fromEnv() stops the job on its first run.
Sources
Related questions
FAQ
- Should I unit test a main method?
Do not unit test a main method that only creates objects from arguments of different types and calls one entry point, because a broken main stops the program on its first run. Unit test a function that holds the flag parsing, the exit code logic and any wiring of several values of one type.
- Should I test the main() method of a Spring Boot application?
Do not test the main() method of a Spring Boot application, because it only calls
SpringApplication.run. Spring Initializr's@SpringBootTesttest starts the same context without main; run it in CI when your deploy has no health check.- How do I test a main method that calls System.exit?
Move the logic into a
runmethod that returns the exit code, and let main callSystem.exit(run(args)). Testrunwith plain assertions on the returned code, because aSystem.exitcall inside a test ends the test JVM.- Should I exclude the main method from code coverage?
Exclude a wiring-only main method from the coverage gate instead of testing it for the gate. In Python, add
if __name__ == .__main__.:toexclude_alsoin coverage.py. In Java, keep main to one line.