Generate and run zunit tests for a java-cli-app project using $ARGUMENTS. Apply all rules below strictly.
What is zunit
zunit is a zero-dependency test runner. It discovers *Test.java source files and runs each directly via java --source 25. No compilation step, no JUnit, no framework.
Test Convention
- Test files: name ends with
Test.java, placed intest/directory - Each test file is a self-contained Java source script with
void main() - Tests run via
java --source 25 --class-path <classpath> - Failure: any thrown exception or non-zero exit = failed
- Success: clean exit with code 0
- All test files run concurrently in separate JVMs
Test File Structure
Every test file follows this pattern:
void main() {
// arrange
var input = ...;
// act
var result = SomeClass.someMethod(input);
// assert
assert expected.equals(result) : "expected %s but got %s".formatted(expected, result);
}
Assertion Style
- No assertion libraries — use the built-in
assertstatement; zunit runs every test with-ea - Always attach a descriptive message including expected and actual values:
assert condition : message - One test file can contain multiple assertions — the first failure stops the file
- For testing exceptions, use try/catch and throw
AssertionErrorexplicitly:
void main() {
try {
SomeClass.methodThatShouldFail(badInput);
throw new AssertionError("expected exception was not thrown");
} catch (IllegalArgumentException expected) {
// success
}
}
Caveat: assert only executes with assertions enabled. Running a test file directly requires the flag: java -ea FooTest.java.
Test Discovery for java-cli-app Projects
Directory Layout
project-root/
├── src/main/java/ # main source (compiled by zb)
├── test/ # zunit test sources
│ ├── SomethingTest.java
│ └── AnotherTest.java
├── zbo/app.jar # zb output (auto-detected as classpath)
└── .zb # zb config
Classpath
zunit auto-detects zbo/app.jar as the classpath. Tests import main classes directly — zb packages everything into app.jar.
How to Generate Tests
- Read the main source code in
src/main/java/to understand what to test - Create test files in
test/directory — one test file per logical concern - Name test files descriptively:
ParserTest.java,ValidationTest.java,OutputTest.java - Test public behavior, not internal implementation
- Include both happy path and error/edge cases
- Keep each test file focused — prefer multiple small test files over one large file (they run concurrently)
Test File Rules
- No package declaration
- No imports from
java.base— it is automatically available - Use module imports for non-base modules (e.g.,
import module java.net.http;) - Use
varfor local variables - Use unnamed class style —
void main()at top level, helper methods as needed - Do not use
IO.println()for assertions — useassert - Printing to stdout is fine for debugging but not required
HTTP Client Timeouts
When tests make network calls using java.net.http.HttpClient, always set explicit timeouts to prevent tests from hanging:
- Set
.connectTimeout(Duration.ofSeconds(2))on theHttpClient - Set
.timeout(Duration.ofSeconds(2))on eachHttpRequest
Running Tests
After generating test files:
- Build first:
zb(compiles main sources intozbo/app.jar) - Run tests:
zunit(discoverstest/*Test.java, runs againstzbo/app.jar) - Alternatively:
zb && zunit
Use zunit -verbose to debug classpath or discovery issues.
Example
Given a main class in src/main/java/Converter.java:
class Converter {
static int toFahrenheit(int celsius) {
return celsius * 9 / 5 + 32;
}
}
Generate test/ConverterTest.java:
void main() {
// freezing point
var freezing = Converter.toFahrenheit(0);
assert freezing == 32 : "expected 32 but got " + freezing;
// boiling point
var boiling = Converter.toFahrenheit(100);
assert boiling == 212 : "expected 212 but got " + boiling;
// negative
var negative = Converter.toFahrenheit(-40);
assert negative == -40 : "expected -40 but got " + negative;
}