From f0bfec2634e3f2ee9e966d3438b5e09956fdea68 Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Thu, 24 Sep 2026 05:22:00 -0700 Subject: [PATCH] Adds Project.md: team reference doc on the JTest C++ Jasmine-style testing framework --- Project.md | 348 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 Project.md diff --git a/Project.md b/Project.md new file mode 100644 index 0000000..fb937bd --- /dev/null +++ b/Project.md @@ -0,0 +1,348 @@ +# JTest — A C++ Test Framework That Feels Like Jasmine + +This document is the single point of reference for anyone new to the **JTest** +project. It explains what we are building, why, how it maps to the JavaScript +testing framework **Jasmine**, and how the pieces fit together today. If you +only read one file, read this one first. + +--- + +## 1. What is JTest, and who is it for? + +**One-sentence summary:** JTest is a C++ unit-testing framework whose API is +deliberately modelled on JavaScript's Jasmine, so that anyone who already knows +Jasmine can write C++ tests without relearning the vocabulary. + +**The job-to-be-done:** a developer testing C++ code wants the same comfort, +readability, and rhythm they get from Jasmine — `describe`/`it`/`expect`/`toBe` — +but in a native, header-and-source C++ form, compiled locally with `clang++`. + +**Who the users are:** +- **Test authors** — C++ developers who write test files using `describe`, + `it`, `expect(...).toBe(...)` etc. +- **Framework maintainers** — the small team (you) that extends JTest itself. + +**What it is NOT (yet):** it is not a drop-in for GoogleTest, not a CI runner, +and not complete. It is an evolving design. See [§8 Current State & Roadmap](#8-current-state--roadmap). + +--- + +## 2. Why the name "Jasmine"? (the vocabulary mapping) + +Jasmine is one of the most widely recognised behaviour-driven testing (BDD) +frameworks in JavaScript. JTest reuses its three pillars one-for-one, plus the +Jasmine "pending" and "failure" idioms. The mapping below is the core mental +model of the whole project. + +| Concept in JTest | Jasmine equivalent | What it does | +|--------------------|--------------------|----------------------------------------------------------| +| `describe(label, makeTests)` | `describe` | Names a group of related tests; can nest. | +| `xdescribe(...)` | `xdescribe` | Same as `describe`, but the whole group is **disabled**. | +| `it(label, fn)` | `it` / `spec` | Declares a single test case that runs `fn`. | +| `xit(label, fn)` | `xit` | Declares a test that is **disabled** (skipped). | +| `expect(x)` | `expect` | Returns an `Expectable` you assert on. | +| `.toBe(matcher)` | `toBe` | Assert equality / truth via a custom matcher function. | +| `.toBeTrue/False/Null/Equal(...)` | `toBe` family | Type-specific boolean/null/equality matchers. | +| `.toThrow(...)` | `toThrow` | Assert a callable throws (optionally a matching exception).| +| `.nevermore()` | `.not` | **Inverts** the next assertion (negation). See §4.4. | +| `expect(x).nevermore().toEqual(y)` | `expect(x).not.toBe(y)` | Negative assertion. | +| `beforeAll/afterAll/beforeEach/afterEach` | same names | Lifecycle hooks, supplied via `DescribeOptions`. | +| `fail(reason)` | `fail` | Force a test to fail with a message. | +| `pending(reason)` | `pending`/`xit` | Mark a test as pending (skipped) with a reason. | + +> **Naming note for maintainers:** JTest can't use Jasmine's `.not()` because +> `not` is awkward in C++. The current chosen name is **`nevermore()`**. A shortlist +> of alternatives was tracked in `include/JTest/Expectable.h` — revisit before +> anything depends on it. + +--- + +## 3. Project layout + +``` +JTest/ +├── Project.md <-- you are here: the team reference doc +├── README.md <-- high-level, plus a TODO list (see §8) +├── LICENSE <-- MIT +├── Makefile <-- builds the framework + the example test binary +├── include/JTest/ <-- public headers (the API surface) +├── src/JTest/ <-- framework implementation (.cpp) +├── examples/ <-- example test sources that exercise the API +└── build/ <-- generated objects/binaries (gitignored target dir) +``` + +### 3.1 Headers (`include/JTest/`) — the public API + +| Header | Responsibility | +|--------|----------------| +| `JTest.h` | Umbrella header. Declares the top-level functions: `execute`, `describe`, `xdescribe`, `it`, `xit`, `fail`, `pending`, and the `expect(T)` template. **Start here.** | +| `Expectable.h` | The `Expectable` matcher class — all the `toBe/toBeEqual/toBeTrue/.../toThrow` matchers plus negation. Header-only, templated. | +| `TestBundle.h` | A node in the test tree: holds `tests_` (leaf tests), `children_` (nested bundles), the four lifecycle hooks, and `disabled_`. | +| `Test.h` | A single leaf test: `label_`, `test_method_` (a `test_fn`), `disabled_`. | +| `TestResults.h` | Accumulated counters: `total / passed / failed / skipped`, with `operator+`, `+=`, and `operator<<` for reporting. | +| `TestOptions.h` | Placeholder for per-test options — currently an empty class. | +| `DescribeOptions.h` | Per-group options: `beforeAll/afterAll/beforeEach/afterEach` setters (fluent, return `*this`). | +| `ConfigureFunction.h` | `configure_fn = std::function` and a `combine(...)` helper that chains two optional hooks. | +| `TestFunction.h` | `test_fn = std::function` — the signature every `it(...)` body is wrapped in. | +| `TestStatus.h` | `enum TestStatus { Unknown, Passed, Failed, Skipped }`. | +| `TestException.h` | Base exception (`std::runtime_error`) carrying label/path/function/file/line; the foundation of test failures. | +| `TestFailedException.h` | `TestException` subclass = a reported test **failure** (`reason()`). | +| `TestPendingException.h` | `TestException` subclass = a **skipped/pending** test. | +| `FailedExpectation.h` | `TestFailedException` subclass thrown by a matcher when an assertion fails. | + +### 3.2 Sources (`src/JTest/`) + +`.cpp` implementations for: `ConfigureFunction`, `DescribeOptions`, +`FailedExpectation`, `JTest` (the runtime: `describe/it/execute`), `Test`, +`TestBundle`, `TestException`, `TestFailedException`, `TestOptions`, +`TestPendingException`, `TestResults`, `TestStatus`. + +### 3.3 Examples (`examples/`) + +- `example.cpp` — the **example test driver**. Contains several `describe` blocks + and a `main()` that runs them, collects a `TestResults`, and prints it. This is + the file you read to learn the API by example. +- `ClassToTest.h` / `ClassToTest.cpp` — currently **empty stubs** (a `MyNS` + namespace placeholder). Intended to be the first "class under test". + +--- + +## 4. How the API works (concepts) + +### 4.1 Describing and nesting + +A test file builds a tree of `describe` blocks. Each `describe` takes a label and a +**factory function** (`make_testbundle_fn`) that returns the inner +`TestBundle`. Nesting is done by returning `TestBundle`s (or other `describe` +results) from those factories. + +```cpp +describe("ClassToTest", [](){ + return TestBundle( + { + it("should do the thing", [](){ + // body + }), + it("should do the other thing", [](){ + pending("we haven't made the other thing yet"); + }), + }, + DescribeOptions() + .beforeEach([](){ /* setup */ }) + .afterEach([](){ /* teardown */ }) + .beforeAll([](){ /* once before all */ }) + .afterAll([](){ /* once after all */ })); +}); +``` + +`xdescribe(...)` / `xit(...)` are the "disabled" variants. + +### 4.2 Expect and matchers + +`expect(actual)` returns an `Expectable`. You chain a matcher onto it. + +```cpp +expect(MyAddFunction(2, 2)).toEqual(4); // true → pass; false → throw +expect(x).toBeTrue(); // boolean-ish +expect(ptr).toBeNull(); // pointer +expect(fn).toThrow(std::runtime_error("...")); // callable throws +``` + +Matchers implemented today: +- `toBe(matcher)` — matcher is `std::optional(const T&)`; returns a + value on failure → throws `FailedExpectation`. +- `toBeFalse()`, `toBeTrue()`, `toBeNull()`, `toEqual(value)`. +- `toThrow()` / `toThrow(exception)` / `toThrow(string)` / `toThrow(matcher)` — + several overloads for "assert this callable throws". + +### 4.3 Negation (the `not` problem) + +`Expectable::nevermore()` flips the internal `is_inverted_` flag so the next +matcher asserts the negative — JTest's stand-in for Jasmine's `.not()`. + +```cpp +expect(2 + 2).nevermore().toEqual(5); // passes: they are not equal +``` + +### 4.4 Running tests + +The top-level entry point is `execute(...)`. There are two overloads: + +- `execute(TestBundle bundle, path="")` — runs a group; recursively handles + `beforeAll → children → tests (each wrapped in beforeEach/afterEach) → afterAll`. +- `execute(Test test, bundle_label)` — runs one test; increments the right + counter and prints a line. + +A typical `main()` (see `examples/example.cpp`) accumulates results and prints +them: + +```cpp +TestResults results; +results += run_group_a(args); +// ... +std::cout << results << std::endl; +``` + +### 4.5 Failure model (exceptions carry the signal) + +JTest uses **exceptions to flow test outcomes**, then catches them at the +`execute(Test...)` level to turn them into counters + output: +- A matcher fails → throws `FailedExpectation` (a `TestFailedException`). +- `fail(reason)` → throws `TestFailedException`. +- `pending(reason)` → throws `TestPendingException`. +- Any uncaught `std::exception` / `...` → counted as a failure ("Unhandled + exception running test"). + +Result line formats (seen in real output — see §7): +- `🚧 Pending Test: ::