About TestLang++
TestLang++ is a custom Domain-Specific Language (DSL) designed to make backend testing fast, readable, and highly maintainable. Currently, the compiler parses .test files and translates them into runnable JUnit 5 Java test classes.
Built for speed and flexibility, the compiler core relies on Flex (Lex) and Bison (Yacc), with intermediate code generation handled in C.
🚀 Vision & Roadmap
TestLang++ is actively expanding beyond its Java/API roots. We are currently looking for contributors to help build out the following features:
- Multi-Language Support: Expanding the C code generator to compile
.testfiles into Python (e.g., PyTest) and other testing frameworks. - Unit Testing Capabilities: Extending the DSL grammar beyond API endpoints to support modular unit test generation.
- Enhanced Assertions: Broadening the
expectsyntax for more complex payload validations.
Features
- Declarative Syntax: Define
configblocks for global settings, setupletvariables, and isolate tests intestblocks. - Native HTTP Methods: First-class support for
GET,POST,PUT, andDELETE. - Multiline JSON: Native support for
"""..."""multiline JSON payloads.
Features Implemented
This compiler successfully implements all core requirements of the assignment, as well as the optional bonus features.
-
Compiler: Built with Flex and Bison, generating C code that produces a Java output file.
-
DSL Parsing: Reliably parses:
-
config { ... }blocks for global settings likebase_urland defaultheaders. -
let ... = ...;statements for global variable declaration (both strings and numbers). -
test { ... }blocks, which are mapped to individual@Testmethods in JUnit.
-
-
Variable Substitution: Correctly substitutes variables (e.g.,
$id,$role) in request URLs, headers, and bodies. -
HTTP Methods: Supports all required HTTP methods:
-
GET -
POST(with optional body and headers) -
PUT(with optional body and headers) -
DELETE
-
-
Assertions: Generates JUnit assertions for:
-
expect status = 200 -
expect header "key" = "value" -
expect header "key" contains "value" -
expect body contains "substring"
-
-
✨ Optional Feature: Multiline JSON Bodies: Supports
"""..."""syntax for multiline JSON strings inPOSTandPUTrequest bodies, making them much easier to read and write. -
✨ Optional Feature: Status Code Ranges: Supports
expect status in 200..299for asserting a range of successful status codes.
TestLang++ Grammar (EBNF)
This is the formal EBNF grammar that the compiler implements.
Program = [ConfigSection], {LetStatement}, {TestBlock};
ConfigSection = "config", "{", {ConfigStatement}, "}";
ConfigStatement = BaseUrl | HeaderStatement;
BaseUrl = "base_url", "=", String, ";";
HeaderStatement = "header", String, "=", String, ";";
LetStatement = "let", Identifier, "=", (String | Number), ";";
TestBlock = "test", Identifier, "{", {Step}, "}";
Step = Request | Assertion;
Request = GetRequest | DeleteRequest | PostRequest | PutRequest;
GetRequest = "GET", String, ";";
DeleteRequest = "DELETE", String, ";";
PostRequest = "POST", String, [RequestBlock];
PutRequest = "PUT", String, [RequestBlock];
RequestBlock = "{", {RequestStatement}, "}";
RequestStatement = BodyStatement | HeaderStatementInBlock;
BodyStatement = "body", "=", (String | JsonBody), ";";
HeaderStatementInBlock = "header", String, "=", String, ";";
Assertion = ExpectStatement;
ExpectStatement = "expect", ExpectCondition, ";";
ExpectCondition = StatusAssertion | HeaderAssertion | BodyAssertion;
StatusAssertion = "status", ("=", Number | "in", Number, "..", Number);
HeaderAssertion = "header", String, ("=" | "contains"), String;
BodyAssertion = "body", "contains", String;
String = '"', {Character | Escape}, '"';
JsonBody = '"""', {AnyCharacter}, '"""';
Escape = '\\', ('"' | '\\');
Number = Digit, {Digit};
Identifier = Letter, {Letter | Digit};
Letter = "A" | "B" | ... | "Z" | "a" | "b" | ... | "z";
Digit = "0" | "1" | ... | "9";
Project Structure
.
├── bin/ # Compiled executables
│ └── testlang # <- The compiled compiler
│
├── compiler/ # Compiler source code
│ ├── Makefile # Build configuration
│ ├── include/ # Header files (e.g., codegen.h)
│ └── src/ # Core implementation
│ ├── lexer.l # Flex lexer rules
│ ├── parser.y # Bison grammar rules
│ └── codegen/ # C code generation logic
│
├── tests/ # Tests cases
│ └── api/ #api test cases
│ └── unit/ #unit test cases
(Note: A build/ directory will be automatically generated inside compiler/ during the make process to hold intermediate object files and generated C code.) (Note: out/ directory will be automatically generated inside compiler during the make test process to hold the generated test cases.)
How to Run
Follow these steps to build the compiler, run the backend, generate the tests, and execute them.
Prerequisites
gcc,make,flex,bison(for building the compiler)java(JDK 11+),maven(for running the Spring Boot backend)java(JDK 11+) (for running the JUnit tests)
Step 1: Build the Compiler (testlang)
We use make to compile the lexer, parser, and code generators.
# Navigate to the compiler directory
cd compiler
# Build the project
make
Step 2: Generate the Java Tests
Once the compiler is built, you can run the test cases to see whether compiler pass the test cases.
#run test cases
make test
Currently Handles Erros
A compiler is no good if it doesn’t help you find mistakes! The yyerror function in parser.y is set up to give you clear, color-coded feedback right in your terminal, including the line number and the line that has the problem.
Here are the types of errors it can catch:
1. Lexical Errors (Typo-level)
This happens when you use a character that the language doesn’t recognize at all.
- Example Code:
let x = 10; % - Error Message:
8: error: Unexpected character
8 | let x = 10; %
2. Syntax Errors (Grammar-level)
This is the most common error. It means you wrote something in the wrong order or are missing a token (like a semicolon or a brace).
- Example Code (Missing semicolon):
let x = 10 - Error Message:
8: error: syntax error, unexpected 'test', expecting SEMICOLON
8 | let x = 10
3. Semantic Errors (Logic-level)
These errors happen when your code is syntactically perfect, but it just doesn’t make sense.
- Invalid Variable Name: The language rules say variable names must start with a letter.
-
Example Code:
let 1badVar = "oops"; -
Error Message:
-
8: error: Invalid variable name: 1badVar
8 | let 1badVar = "oops";
- Undefined Variable: You try to use a variable (like $nope) that you never defined with let.
-
Example Code:
GET "/api/users/$nope"; -
Error Message:
-
16: error: Undefined variable 'nope' in '/api/users/$nope'
16 | GET "/api/users/$nope";
Contributing
Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Whether you want to fix a bug, enhance the grammar, or help us build out Python test generation, your contributions are greatly appreciated.
To get started, please read our Contributing Guidelines. It covers everything you need to know about setting up the compiler locally, understanding the project architecture, and submitting your first Pull Request.