{"id":49954429,"url":"https://github.com/dlealv/officescripts-unit-test-framework","last_synced_at":"2026-05-17T22:07:30.969Z","repository":{"id":301802765,"uuid":"1009313452","full_name":"dlealv/officescripts-unit-test-framework","owner":"dlealv","description":"Lightweight, extensible unit testing framework for Office Scripts, inspired by libraries like JUnit. Provides basic assertion capabilities and defines the structure for executing test cases. Designed for easy integration and extension within Office Scripts projects","archived":false,"fork":false,"pushed_at":"2025-07-10T04:11:06.000Z","size":5598,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-07-10T10:38:24.904Z","etag":null,"topics":["officescript","officescripts","testing","testing-framework","testing-library","typescript","unit-testing","unit-testing-framework"],"latest_commit_sha":null,"homepage":"https://dlealv.github.io/officescripts-unit-test-framework/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dlealv.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-06-26T23:45:18.000Z","updated_at":"2025-07-10T04:11:09.000Z","dependencies_parsed_at":"2025-06-28T23:36:44.580Z","dependency_job_id":null,"html_url":"https://github.com/dlealv/officescripts-unit-test-framework","commit_stats":null,"previous_names":["dlealv/officescripts-unit-test-framework"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/dlealv/officescripts-unit-test-framework","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dlealv%2Fofficescripts-unit-test-framework","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dlealv%2Fofficescripts-unit-test-framework/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dlealv%2Fofficescripts-unit-test-framework/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dlealv%2Fofficescripts-unit-test-framework/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dlealv","download_url":"https://codeload.github.com/dlealv/officescripts-unit-test-framework/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dlealv%2Fofficescripts-unit-test-framework/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33157234,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-17T09:28:26.183Z","status":"ssl_error","status_checked_at":"2026-05-17T09:27:52.702Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["officescript","officescripts","testing","testing-framework","testing-library","typescript","unit-testing","unit-testing-framework"],"created_at":"2026-05-17T22:07:16.511Z","updated_at":"2026-05-17T22:07:30.963Z","avatar_url":"https://github.com/dlealv.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Office Scripts Unit Testing Framework\n\nA lightweight, extensible unit testing framework for [Office Scripts](https://learn.microsoft.com/en-us/office/dev/scripts/) \u0026 TypeScript, inspired by libraries like JUnit.  \nProvides basic assertion capabilities and a structured test runner for easy test authoring, debugging, and reporting—**usable both in Office Scripts and in local Node/TypeScript environments**.\n\n---\n\n## Features\n\n- **Assert Class**: Rich assertion methods for values, arrays (with type and value checking), exceptions, types, containment, and more.\n- **TestRunner**: Structured, hierarchical output with configurable verbosity levels (`OFF`, `HEADER`, `SECTION`, `SUBSECTION`).\n- **Compatible**: Runs on both Office Scripts and Node/TypeScript (for local or CI testing).\n- **Simple**: No dependencies, no decorators, no runtime imports.\n- **Extensible**: Add your own assertions or test conventions easily.\n\n---\n\n## Getting Started\n\n### 1. Clone or copy this repo\n\nPlace `unit-test-framework.ts` in your project.  \n(Optional: Use `test/main.ts` as a starting point for your test suite.)\n\n### 2. Write Tests\n\nDefine a `TestRunner` and create a test class with static methods, e.g.:\n\n```typescript\nconst runner = new TestRunner(TestRunner.VERBOSITY.SECTION)     // Define the test case runner and verbosity level\nrunner.title(\"Start Testing\", 1)                                // Output title indicating the test started\nrunner.exec(\"Test Case for math\", () =\u003e TestCase.math(), 2)     // Execute math method from TestCase with section indentation level\nrunner.title(\"End Testing\", 1)                                  // Output title indicating the test ended\n\n// Class to organize all test cases\nclass TestCase {\n  public static math(): void {\n    Assert.equals(2 + 2, 4, \"Addition works\")\n    Assert.isTrue(5 \u003e 2, \"Greater comparison\")\n    Assert.throws(() =\u003e { throw new Error(\"fail\") }, Error, \"fail\", \"Throw check\")\n  }\n}\n```\n**Note:** The `TestCase` class is not required, just a way to organize all test cases to be executed via the `TestRunner` class.\n\n### 3. Run Tests\n\nIn Office Scripts, call `main(workbook)` (see `test/main.ts`).\n\nIn Node/TypeScript, run a wrapper (see `main-wrapper.ts`) that invokes `main`.\n\n---\n\n## API Reference\n\n### Assert Class\n\n#### Value Equality \u0026 Arrays\n\n```typescript\nAssert.equals(actual, expected, \"optional message\")\n```\n- **Supports primitives, arrays, and objects.**\n- For arrays, each element is checked for both type and value. For objects/arrays of objects, a deep check (using JSON.stringify) is performed.\n- Example:\n  ```typescript\n  Assert.equals([1, 2, 3], [1, 2, 3], \"Arrays are equal\") // Passes\n  Assert.equals([1, \"2\"], [1, 2])                         // Fails: type mismatch at index 1\n  Assert.equals([{x:1}], [{x:1}])                         // Passes: objects are deeply equal\n  ```\n\n#### Inequality\n\n```typescript\nAssert.notEquals(actual, notExpected, \"optional message\")\n```\n\n#### Instance Checks\n\n```typescript\nAssert.isInstanceOf(obj, ClassConstructor, \"optional message\")\nAssert.isNotInstanceOf(obj, ClassConstructor, \"optional message\")\n```\n\n#### Type Checks\n\n```typescript\nAssert.isType(value, \"string\" | \"number\" | \"boolean\" | \"object\" | \"function\" | \"undefined\" | \"symbol\" | \"bigint\", \"optional message\")\nAssert.isNotType(value, \"string\" | \"number\" | \"boolean\" | \"object\" | \"function\" | \"undefined\" | \"symbol\" | \"bigint\", \"optional message\")\n```\n- Example:\n  ```typescript\n  Assert.isType(\"hello\", \"string\", \"Should be string\")\n  Assert.isType(42, \"number\")\n  Assert.isType({}, \"object\")\n  Assert.isNotType(\"hello\", \"number\", \"String is not number\")\n  Assert.isNotType(42, \"string\", \"Number is not string\")\n  ```\n\n#### Null/Undefined Checks\n\n```typescript\nAssert.isNull(value, \"optional message\")\nAssert.isNotNull(value, \"optional message\")\nAssert.isUndefined(value, \"optional message\")\nAssert.isNotUndefined(value, \"optional message\")\nAssert.isDefined(value, \"optional message\") // alias for isNotUndefined\n```\n\n#### Truthy/Falsy\n\n```typescript\nAssert.isTrue(expression, \"optional message\")\nAssert.isFalse(expression, \"optional message\")\n```\n\n#### Containment\n\n```typescript\nAssert.contains(arrayOrString, value, \"optional message\")\n```\n- Example:\n  ```typescript\n  Assert.contains([1, 2, 3], 2, \"Array contains 2\")\n  Assert.contains(\"hello world\", \"world\", \"Substring found\")\n  ```\n\n#### Exception Assertions\n\nTo test that code throws (or does not throw) as expected, always pass a function reference using `() =\u003e ...`.  \nIf you pass a direct function call (e.g., `Assert.throws(myFunction())`), the code will execute before it reaches the assertion and the assertion won't work as intended.\n\n**Example:**\n\nSuppose you have the following simple class:\n\n```typescript\nclass Divider {\n  static divide(a: number, b: number): number {\n    if (b === 0) throw new Error(\"Cannot divide by zero\")\n    return a / b\n  }\n}\n```\n\nYou can test that `Divider.divide` throws for zero denominator, and does not throw otherwise:\n\n```typescript\n// Correct: Pass a function reference (using an arrow function)\nAssert.throws(\n  () =\u003e Divider.divide(10, 0),\n  Error,\n  \"Cannot divide by zero\",\n  \"Should throw when dividing by zero\"\n)\n\n// Also correct: test that a valid division does NOT throw\nAssert.doesNotThrow(\n  () =\u003e Divider.divide(10, 2),\n  \"Should not throw for valid division\"\n)\n```\n\n**Note:**  \n`Assert.throws` requires **the throwing code to be passed as a function reference** (using `() =\u003e ...` or `function() { ... }`).  \nThis allows the assertion method to execute your function and catch any exceptions inside its own logic.\n\n#### Fail Manually\n\n```typescript\nAssert.fail(\"This should not happen\")\n```\n\n---\n\n### TestRunner Class\n\n#### Creating a Test Runner\n\n```typescript\nconst runner = new TestRunner(TestRunner.VERBOSITY.SECTION) // or HEADER, OFF, SUBSECTION\n```\n\n#### Verbosity Levels\n\n- `OFF` (`0`): No output.\n- `HEADER` (`1`): Only top-level section headers.\n- `SECTION` (`2`): Section and higher.\n- `SUBSECTION` (`3`): All titles, including subsections.\n\n**How verbosity and indent work:**  \n- Each call to `runner.title(\"Title\", indent)` prints the message with `indent` number of `*` as prefix and suffix (e.g., `** title **` for `indent=2`).\n- A title is only printed if its `indent` is **less than or equal to** the current verbosity.\n- This lets you control granularity of test output: higher verbosity shows more detail.\n\n#### Running Tests\n\n```typescript\nrunner.exec(\"My Test Name\", () =\u003e {\n  Assert.equals(1 + 1, 2)\n}, 2) // The '2' is the indent level for this test (prints if verbosity \u003e= 2)\n```\n**Note:**  \nWhen using `TestRunner.exec`, always pass the test code as a function reference (e.g., `() =\u003e ...` or `function() { ... }`). This ensures the test is executed at the correct time within the `exec` method, preserving the intended order of output and test execution. Passing a direct function call (e.g., `runner.exec(\"Test\", myTestFunction())`) will execute the test immediately—before `exec` can manage output or error handling—leading to unexpected results such as out-of-order titles or missed error reporting.\n\n#### Structured Output\n\n```typescript\nrunner.title(\"Title the testing\", 1) // * Title the testing *\nrunner.title(\"Section\", 2)           // ** Section **\nrunner.title(\"Detail\", 3)            // *** Detail ***\n```\n\n#### Getting Verbosity\n\n```typescript\nrunner.getVerbosity()      // returns numeric level\nrunner.getVerbosityLabel() // returns \"HEADER\", etc\n```\n\n---\n\n## Example: Full Test Suite\n\n```typescript\n// main test file for the unit test framework\n\nfunction main(workbook: ExcelScript.Workbook) {\n  const runner = new TestRunner(TestRunner.VERBOSITY.SECTION)\n  let success = false\n  try {\n    runner.title(\"Running All Tests\", 1)\n    runner.exec(\"Math Test\", () =\u003e TestCase.math(), 2)\n    runner.exec(\"Null/Undefined Test\", () =\u003e TestCase.nullUndefined(), 2)\n    runner.exec(\"Instance Test\", () =\u003e TestCase.instance(), 2)\n    runner.exec(\"Throws/DoesNotThrow Test\", () =\u003e TestCase.throwsDoesNotThrow(), 2)\n    runner.exec(\"Type Test\", () =\u003e TestCase.type(), 2)\n    success = true\n  } finally {\n    runner.title(success ? \"All Tests Passed\" : \"Test Failure\", 1)\n  }\n}\n\n// Class to organize all test cases as static methods\nclass TestCase {\n  public static math() {\n    Assert.equals(2 + 3, 5, \"Addition works\")\n    Assert.notEquals(2 * 2, 5, \"Multiplication does not equal 5\")\n    Assert.equals([1, 2], [1, 2], \"Array equality\")\n  }\n\n  public static nullUndefined() {\n    Assert.isNull(null, \"Should be null\")\n    Assert.isNotNull(0, \"Zero is not null\")\n    Assert.isUndefined(undefined, \"Should be undefined\")\n    Assert.isNotUndefined(\"\", \"Empty string is defined\")\n    Assert.isDefined(123, \"Number is defined\")\n  }\n\n  public static instance() {\n    class Animal {}\n    class Dog extends Animal {}\n    const d = new Dog()\n    Assert.isInstanceOf(d, Dog, \"Dog instance of Dog\")\n    Assert.isInstanceOf(d, Animal, \"Dog instance of Animal\")\n    Assert.throws(() =\u003e Assert.isInstanceOf({}, Dog), AssertionError, undefined, \"Throws if not instance\")\n    Assert.isNotInstanceOf({}, Dog, \"Plain object is not instance of Dog\")\n  }\n\n  public static throwsDoesNotThrow() {\n    // --- All throws cases ---\n    // 1. Throws an Error with specific message\n    Assert.throws(() =\u003e { throw new Error(\"fail\") }, Error, \"fail\", \"Should throw Error\")\n\n    // 2. Throws a TypeError\n    Assert.throws(() =\u003e { throw new TypeError(\"bad type\") }, TypeError, \"bad type\", \"Should throw TypeError\")\n\n    // 3. Throws any error (not checking error type or message)\n    Assert.throws(() =\u003e { throw \"custom error string\" }, undefined, undefined, \"Should throw any error (string)\")\n\n    // 4. Throws AssertionError when an assertion fails inside\n    Assert.throws(() =\u003e Assert.isTrue(false, \"Forced fail\"), AssertionError, undefined, \"Should throw AssertionError when assertion fails\")\n\n    // 5. Using a function variable that throws\n    const failFunc = () =\u003e { throw new RangeError(\"range fail\") }\n    Assert.throws(failFunc, RangeError, \"range fail\", \"Should throw RangeError\")\n\n    // --- All doesNotThrow cases ---\n    // 1. Does not throw (simple value)\n    Assert.doesNotThrow(() =\u003e 42, \"Should not throw on returning 42\")\n\n    // 2. Does not throw (returns undefined)\n    Assert.doesNotThrow(() =\u003e undefined, \"Should not throw on returning undefined\")\n\n    // 3. Does not throw (assertion that passes)\n    Assert.doesNotThrow(() =\u003e Assert.isTrue(true, \"Should pass\"), \"Should not throw if assertion passes\")\n\n    // 4. Using a function variable that does not throw\n    const safeFunc = () =\u003e \"hello\"\n    Assert.doesNotThrow(safeFunc, \"Should not throw with safeFunc\")\n  }\n\n  public static type() {\n    Assert.isType(\"abc\", \"string\", \"abc is string\")\n    Assert.isType(123, \"number\", \"123 is number\")\n    Assert.throws(() =\u003e Assert.isType(123, \"string\"), undefined, undefined, \"Throws if type mismatch\")\n    Assert.isNotType(\"hello\", \"number\", \"String is not number\")\n    Assert.isNotType(42, \"string\", \"Number is not string\")\n  }\n}\n\n// Make main available globally for Node/ts-node test environments\nif (typeof globalThis !== \"undefined\" \u0026\u0026 typeof main !== \"undefined\") {\n  // @ts-ignore\n  globalThis.main = main\n}\n```\n\n---\n\n## Output Example (Verbosity: SECTION)\n\n```\n* Running All Tests *\n** START Math Test **\n** END Math Test **\n** START Null/Undefined Test **\n** END Null/Undefined Test **\n** START Instance Test **\n** END Instance Test **\n** START Throws/DoesNotThrow Test **\n** END Throws/DoesNotThrow Test **\n** START Type Test **\n** END Type Test **\n* All Tests Passed *\n```\n\n- Each title uses `*` characters as prefix/suffix, repeated according to the `indent` parameter.\n- A title prints only if its `indent` is less than or equal to the runner's verbosity.\n- Example above shows only indent `1` and `2` titles, because verbosity is set to `SECTION` (`2`).\n\nIf verbosity level is `HEADER` the output will be:\n```\n* Running All Tests *\n* All Tests Passed *\n```\n\n---\n\n## Development \u0026 Customization\n\n- Add your own assertion methods to the `Assert` class.\n- Organize tests as you wish—group by topic, file, or feature.\n- Works directly in the Office Scripts editor (Excel Online), as well as in VSCode/Node with your own mocks.\n\n---\n\n## Additional Information\n\n- TypeDoc documentation: [TYPEDOC](https://dlealv.github.io/officescripts-unit-test-framework/typedoc/)\n\n## License\n\n[MIT](LICENCE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdlealv%2Fofficescripts-unit-test-framework","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdlealv%2Fofficescripts-unit-test-framework","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdlealv%2Fofficescripts-unit-test-framework/lists"}