Testing Klyn Applications
The Klyn test framework discovers packaged test classes and executes the complete selection in
one isolated test process and one JIT session. It reports class and method results incrementally
and can persist the complete execution as JSON. The same tests/Start.kn entry point
supports console and graphical runs.
Minimal Test Starter
A project needs only one small entry point. Pass
arguments unchanged to TestLauncher.run(); the launcher
handles package filters, console reporting, JSON output, help, and graphical mode. Its return
value is the process exit code.
# tests/Start.kn
import klyn.unittest.launcher
exit(TestLauncher.run(arguments))
Keeping option handling in the framework guarantees that every project exposes the same command line and that new framework options do not require changes to its starter.
All selected classes share one test process and one JIT session. This avoids repeating runtime, compiler, and standard-library initialization for every class while retaining a distinct JIT namespace and a structured result for each source. A fatal native crash can stop the remaining classes; the runner reports every source that could not execute as an infrastructure failure.
Test Source Layout
The tests directory is the class-loader root. Every test package must therefore
match its directory, exactly as it does for application sources. Discovery is driven by
@TestClass, not by a filename convention; ending test filenames with
Test.kn remains the recommended convention.
tests/
|-- Start.kn
`-- inventory/
|-- InventoryServiceTest.kn
`-- persistence/
`-- InventoryRepositoryTest.kn
# tests/inventory/InventoryServiceTest.kn
package inventory
import klyn.unittest
@TestClass
public class InventoryServiceTest:
@Test
public testAvailableQuantity():
assert 12 - 4 == 8
Declaring package inventory.persistence in
tests/inventory/InventoryServiceTest.kn is an error. Move the file or correct
its package; the runner never hides a package-layout problem.
Test and Fixture Annotations
| Annotation | Execution point |
|---|---|
@TestClass |
Marks the public class containing test methods. |
@BeforeClass |
Runs once before the first test method. |
@Before |
Runs before every test execution, including each data row. |
@Test |
Marks one independently reported test method. |
@Case / @Cases |
Supply positional arguments or rows to a @Test method; one-parameter rows may be single values. |
@After |
Runs after every test execution, including each data row. |
@AfterClass |
Runs once after the last test method. |
package inventory
import klyn.unittest
@TestClass
public class InventoryRepositoryTest:
private _quantity as Int
@BeforeClass
public static prepareDatabase():
print("Preparing test data")
@Before
public resetQuantity():
this._quantity = 10
@Test
public testRemoval():
this._quantity -= 3
assert this._quantity == 7
@After
public verifyInvariant():
assert this._quantity >= 0
@AfterClass
public static closeDatabase():
print("Releasing test data")
Data-Driven Tests
Keep the assertions in one typed method and declare its inputs with repeated
@Case(...) annotations. Pass exactly one value per parameter directly,
in parameter order. Use @Cases([(...), (...)]) for a compact table.
Both annotations belong to klyn.unittest and accompany @Test
inside a @TestClass.
package examples
import klyn.unittest
@TestClass
public class ArithmeticTest:
@Test
@Case("demo", 2, "demo-2")
@Case("klyn", 10, "klyn-10")
public labels(prefix as String, value as Int, expected as String):
assert f"{prefix}-{value}" == expected
@Test
@Cases([(1, 1), (2, 8), (3, 27), (4, 64)])
public cubes(value as Int, expected as Int):
assert value ** 3 == expected
@Test
@Cases(["Klyn", "Compiler", "Runtime"])
public nonEmptyName(value as String):
assert value.size > 0ul
@Test(expected=ValueException)
@Case(-1)
@Case(-10)
public rejectsNegative(value as Int):
if value < 0:
throw ValueException("Negative value")
@Case accepts a variadic number of positional values:
@Case(42) supplies one argument and @Case() supplies none.
A tuple or collection is a single argument: @Case((1, 2)) passes one tuple,
not two integers. With @Cases, each explicit tuple is an argument row.
For a method with exactly one parameter, non-tuple values are accepted directly:
@Cases([42, 43]) is equivalent to
@Cases([(42,), (43,)]), and both forms may be mixed.
To pass a tuple as the single argument, wrap it in a one-element row:
@Cases([((1, 2),), ((3, 4),)]).
Methods with zero or multiple parameters still require tuple rows.
An empty @Cases([]) is rejected,
so a missing dataset cannot silently turn a test green. The compiler rejects incorrect
argument counts and incompatible types at their source locations. Test methods must
return Void, be non-generic and use fixed, by-value parameters, not
ref or out parameters or variadic packs. Even optional parameters require a value
in each row.
Rows run in declaration order, including when @Case and @Cases
are combined. Duplicate rows are preserved. Argument expressions are evaluated afresh
for each execution, after @Before, not once during discovery. The argument
pack and the table wrappers describe the cases; they are not materialized at runtime.
A tuple or collection supplied as an actual method argument is created normally.
Each row is a separately counted result named cubes[1], cubes[2],
and so on, in the console, JSON reports and GUI. A failure does not skip the next row.
@Before and @After surround each execution; the test instance is
shared within the class, as for ordinary tests. @BeforeClass and
@AfterClass still run once. The @Test timeout and expected
exception apply independently to every row. The class timeout covers the whole table.
Expected Errors and Timeouts
Use expected when an exception is the successful outcome of a test. A
@Test(timeout=...) budget limits one test method, while
@TestClass(timeout=...) limits the complete lifecycle of a test class. Both values
are expressed in milliseconds; zero, the default, disables the corresponding limit.
@Test(expected=ValueException)
public testRejectsNegativeQuantity():
throw ValueException("Quantity cannot be negative")
@Test(timeout=500)
public testCompletesQuickly():
value = 40 + 2
assert value == 42
@TestClass(timeout=2000)
public class FastContractTest:
@Test
public completesWithinTheClassBudget():
assert true
A timed-out method is interrupted and reported as failed. It cannot satisfy an
expected declaration. A class budget includes construction,
@BeforeClass, @Before, every @Test,
@After and @AfterClass; source compilation is intentionally excluded.
When method and class budgets are both present, the first deadline reached stops execution.
Asserting Exceptions
Use Assert.assertThrows<ExceptionType> with a function or lambda without
parameters to check one operation without ending the test method. The callable is invoked
once and its return value, if any, is ignored. Assert.assertFails accepts the
same kind of callable and checks for any exception. Passing an evaluated expression such as
Assert.assertThrows(compile(source)) produces a compile-time TypeError: wrap the operation
in lambda(): compile(source) (or lambda: compile(source)).
A string containing source code is just a string,
not an implicit call to the compiler. These helpers are ordinary static Klyn methods;
their names do not alter argument evaluation, type checking, or exception handling.
import klyn.unittest
invalidScalar = -1
Assert.assertThrows<ValueException>(lambda (): Char(invalidScalar))
value as Int = 42
Assert.assertThrows<TypeError>(lambda (): eval("value = 3.14"))
assert value == 42
def failWithoutArguments() as Void:
throw ValueException("Invalid value")
Assert.assertThrows<ValueException>(failWithoutArguments)
The first assertion checks a runtime value error. The second explicitly compiles code with
eval and checks that assigning a Double to the visible Int
variable is rejected. A failed compilation does not replace the variable's value or static type.
compile(source) checks a separate source fragment without executing it.
Imports visible at the call site, such as import klyn.unittest, also apply
inside that fragment. Local variables are not captured, and declarations or imports
introduced in the fragment do not leak into the caller. Use eval when execution
in the caller's visible context is required.
Running Tests
# Run every @TestClass below tests
klyn tests/Start.kn
# Run one package and all of its subpackages
klyn tests/Start.kn klyn.data
# Combine any number of package roots
klyn tests/Start.kn klyn.data klyn.databases
# Display every supported option
klyn tests/Start.kn --help
Selected sources run sequentially in one test process. Compilation failures are reported for
the affected class; an unrecoverable native crash can prevent the remaining classes from running.
Tests must restore shared state and release resources in their fixture cleanup. Successful entries
and the final successful summary are green; failures are red and identify the affected test
methods. Each class occupies one console line; OK or KO is aligned
against the right edge, and long source names are shortened in the middle without hiding the
test filename.
Running /project/tests/klyn/regex/RegExTest.kn... 0.042 s OK
Test summary: 1/1 test class(es) passed; 8/8 test method(s) passed; 0.042 s.
JSON Reports
# Default destination: test-results.json
klyn tests/Start.kn --json
# Explicit destination
klyn tests/Start.kn --json=reports/collections.json klyn.collections
The report records duration, source, package, class and method status, captured standard output and error streams, stack traces, and infrastructure failures. It can be loaded later by the graphical runner.
Graphical Test Runner
klyn tests/Start.kn --gui
klyn tests/Start.kn --gui klyn.io klyn.regex
The GUI uses DockWorkspace. Its tree groups packages, classes, and methods and
displays a red or green status dot at the right of each row. The root stays expanded; passing
branches start collapsed, while every branch containing a failure starts expanded. Selecting
a package or class aggregates its details in the central area. The window opens before test
execution starts; tests run in an isolated worker and completed classes are published to the
tree, output, and summary in real time without blocking GUI painting or input.
- Output preserves the complete nominal and error streams.
- Details focuses on the selected package, class, or method.
- File > Save report persists the current report as JSON.
- File > Load report opens a previous JSON execution.
Save and load actions use the embedded SaveFileLightBox and
OpenFileLightBox from klyn.gui.windows.dialogs. They remain inside the
test-runner window and do not start a nested event loop.
Graphical execution is not a separate program. Always use
klyn tests/Start.kn --gui, so discovery, filtering, isolation, and result
semantics remain identical to console mode.
IDE Integration
The Klyn extension uses the native VS Code Testing API. Starting a test from the editor, gutter, or Explorer reveals the Tests view and result panel immediately. Packages, classes, and methods remain hierarchical; selecting any node displays the output, duration, and errors aggregated for that node.
Internally, --json-stream publishes framed JSON Lines events whenever a class
starts or completes. This lets IDEs update their test tree in real time while the regular
--json report remains the authoritative final snapshot and fallback.
Programmatic Execution
Build tools can use TestRunner and TestReport directly. Package
filters retain the same exact-or-descendant semantics as the command line.
import klyn.io
import klyn.unittest
runner = TestRunner(FolderPath("tests"))
runner.consoleOutput = false
report = runner.run(["klyn.collections"])
report.save(FilePath("collection-results.json"))
if not report.passed:
Application.exit(Application.EXIT_FAILURE)