Unit Tests
Testing Philosophy
Section titled “Testing Philosophy”TDD is mandatory. Tests are written before or alongside implementation — never after. Every public function must have tests covering its happy path and error cases.
Test Organization
Section titled “Test Organization”apps/skein_compiler/test/ skein/ lexer_test.exs # lexer unit tests parser_test.exs # parser unit tests (incl. scenario/golden) analyzer_test.exs # analyzer unit tests codegen/ core_erlang_test.exs # codegen integration tests schema_gen_test.exs # JSON Schema generation tests integration/ memory_llm_test.exs # memory + llm pipeline tests tool_test.exs # tool declaration tests test_construct_test.exs # test/scenario/golden constructs req_json_test.exs # request JSON handlingapps/skein_runtime/test/ skein/runtime/ # runtime behaviour tests (agents, store, queue, replay, ...)apps/skein_cli/test/ cli/ # CLI tests (incl. the skein test runner)The suite is covered by unit and property tests across all apps; counts grow with every change, so this page doesn’t track them — run mix test or check CI for the current totals.
Lexer Tests
Section titled “Lexer Tests”The lexer test suite covers:
- Individual token types: Each keyword, operator, and literal type has dedicated tests
- Multi-token sequences:
let x = 42produces the expected token list - String handling: Plain strings, interpolation, empty strings, escaped characters
- Position tracking: Line and column numbers are correct across multi-line input
- Comments: Single-line comments are properly ignored
- Error cases: Unrecognized characters produce structured errors
Example:
test "tokenizes a simple binding" do assert {:ok, tokens} = Skein.Lexer.tokenize("let x = 42") assert tokens == [ {:let, {1, 1}}, {:ident, {1, 5}, "x"}, {:eq, {1, 7}}, {:int, {1, 9}, 42}, {:eof, {1, 11}} ]endParser Tests
Section titled “Parser Tests”The parser test suite covers every AST construct:
- Module parsing: Empty modules, modules with functions
- Function parsing: With/without parameters, various return types
- Let bindings: Simple values, expression values
- Match expressions: Boolean matching, pattern types, multiple arms
- Operator precedence: All operator combinations at different levels
- Operator associativity: Left-associative chaining
- Pipe expressions: Single and chained pipes
- Function calls: With arguments, nested calls
- Field access: Simple and chained field access
- String handling: Plain strings, interpolation
- Literals: Integers, floats, booleans
- Unary operators: Prefix
!, postfix!and? - Type declarations: Record types with typed fields
- Enum declarations: Simple variants and variants with data
- Capability declarations: With parameters
- Parenthesized expressions: Grouping for precedence override
- Function references:
&namesyntax - Complete examples: The full
hello.skeinprogram - Error cases: Missing tokens, unexpected tokens
Example:
test "parses binary operator with correct precedence" do source = "module M { fn f(a: Int, b: Int, c: Int) -> Int { a + b * c } }" {:ok, tokens} = Lexer.tokenize(source) {:ok, %AST.Module{declarations: [f]}} = Parser.parse(tokens)
assert %AST.Block{expressions: [expr]} = f.body assert %AST.BinaryOp{op: :+} = expr assert %AST.BinaryOp{op: :*} = expr.rightendCodeGen Integration Tests
Section titled “CodeGen Integration Tests”These are full pipeline tests — they compile Skein source to BEAM bytecode, load the module, and call its functions:
- Phase 1 acceptance tests:
greet/1,add/2,classify/1fromhello.skein - Arithmetic operations: Addition, subtraction, multiplication
- Let bindings: Binding preservation, sequential bindings
- Boolean operations: Comparison operators, equality
- String operations: Plain strings, string interpolation, multi-segment interpolation
- Match expressions: Boolean matching with true/false arms
- File compilation:
compile_file/1with the example file - Elixir interop:
__info__(:module),__info__(:functions)
Example:
test "Phase 1 acceptance: greet/1 returns interpolated greeting" do source = ~S""" module Greeter { fn greet(name: String) -> String { "Hello, ${name}!" } } """ {:module, mod} = Compiler.compile_string(source) assert mod.greet("World") == "Hello, World!" assert mod.greet("Skein") == "Hello, Skein!"endImportant Test Conventions
Section titled “Important Test Conventions”Async Settings
Section titled “Async Settings”CodeGen tests use async: false because they load BEAM modules into the VM — a global, shared resource:
use ExUnit.Case, async: falseLexer and parser tests use async: true since they are pure functions with no side effects.
String Escaping
Section titled “String Escaping”Use the ~S sigil for Skein source strings in tests to avoid Elixir interpolation conflicts:
# Good: ~S prevents Elixir from interpreting ${name}source = ~S"""module M { fn greet(name: String) -> String { "Hello, ${name}!" }}"""
# Bad: Elixir would try to interpolate ${name}source = """module M { fn greet(name: String) -> String { "Hello, ${name}!" }}"""File Paths
Section titled “File Paths”When using compile_file/1 in tests, remember that tests run from apps/skein_compiler/:
# Correct: relative from apps/skein_compiler/{:module, mod} = Compiler.compile_file("../../examples/hello.skein")Running Tests
Section titled “Running Tests”# All testsmix test
# Specific test filemix test apps/skein_compiler/test/skein/parser_test.exs
# Specific test by line numbermix test apps/skein_compiler/test/skein/parser_test.exs:42
# With trace outputmix test --trace