|
| 1 | +<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> |
| 2 | +[](#contributors-) |
| 3 | +<!-- ALL-CONTRIBUTORS-BADGE:END --> |
| 4 | +[](https://github.com/stackbuilders/assertive-ts/actions/workflows/ci.yml) |
| 5 | +[](https://github.com/stackbuilders/assertive-ts/actions/workflows/release.yml) |
| 6 | +[](https://github.com/stackbuilders/assertive-ts/actions/workflows/pages.yml) |
| 7 | +[](https://www.npmjs.com/package/@stackbuilders/assertive-ts) |
| 8 | +[](https://www.npmjs.com/package/@stackbuilders/assertive-ts) |
| 9 | +[](https://www.npmjs.com/package/@stackbuilders/assertive-ts) |
| 10 | +[](https://github.com/stackbuilders/assertive-ts/blob/main/LICENSE) |
| 11 | +[](https://github.com/stackbuilders/assertive-ts/releases) |
| 12 | +[](https://snyk.io/test/github/stackbuilders/assertive-ts) |
| 13 | + |
| 14 | +# AssertiveTS |
| 15 | + |
| 16 | +A type-safe fluent assertion library written in TypeScript and inspired by [Jest](https://jestjs.io/docs/expect) assertions and the popular [AssertJ](https://assertj.github.io/doc/). |
| 17 | + |
| 18 | +This library is designed to work in the browser and in Node.js. It ships with a rich set of expressive and flexible matchers that allows chaining multiple assertions. AssertiveTS is framework agnostic and should be used with a test framework such as [Jest](https://github.com/stackbuilders/assertive-ts/blob/main/docs/jest-tutorial.md), [Mocha](https://github.com/stackbuilders/assertive-ts/blob/main/docs/mocha-tutorial.md), or Ava. |
| 19 | + |
| 20 | +## Type-safe library |
| 21 | + |
| 22 | +A distinctive feature of AssertiveTS with other assertion libraries is that it leverages the TypeScript compiler to avoid type coercions and mismatches. It also infers the static type of the value you want to assert and provides you with intelligent matcher completion and signature help so that you can write code more quickly and correctly. |
| 23 | + |
| 24 | +### Features |
| 25 | + |
| 26 | +- Type safety and intelligent matcher completion |
| 27 | +- Rich set of expressive and flexible matchers |
| 28 | +- Concise, chainable interface inspired by AssertJ |
| 29 | +- Works with any test runner and framework such as [Jest](https://github.com/stackbuilders/assertive-ts/blob/main/docs/jest-tutorial.md), [Mocha](https://github.com/stackbuilders/assertive-ts/blob/main/docs/mocha-tutorial.md), or Ava |
| 30 | +- Well tested: more than 300 tests! |
| 31 | + |
| 32 | +## Install |
| 33 | + |
| 34 | +```sh |
| 35 | +npm install --save-dev @stackbuilders/assertive-ts |
| 36 | +``` |
| 37 | + |
| 38 | +Or: |
| 39 | + |
| 40 | +```sh |
| 41 | +yarn add --dev @stackbuilders/assertive-ts |
| 42 | +``` |
| 43 | + |
| 44 | +## Usage |
| 45 | + |
| 46 | +Import the library in your test script: |
| 47 | + |
| 48 | +```ts |
| 49 | +import { expect } from "@stackbuilders/assertive-ts" |
| 50 | +``` |
| 51 | + |
| 52 | +Use the `expect` function along with a "matcher" function on the value you want to assert: |
| 53 | + |
| 54 | +```ts |
| 55 | +expect(sum(1, 2)).toBeEqual(3); |
| 56 | +``` |
| 57 | + |
| 58 | +To assert the opposite, just add `.not` before a matcher: |
| 59 | + |
| 60 | +```ts |
| 61 | +expect(sum(1, 2)).not.toBeNull(); |
| 62 | +``` |
| 63 | + |
| 64 | +With `assertive-ts` you can use **fluent assertions**, which means you can chain multiple matcher functions to the same value under test: |
| 65 | + |
| 66 | +```ts |
| 67 | +expect("assertive-ts is awesome!") |
| 68 | + .toStartWith("assertive-ts") |
| 69 | + .not.toContain("unsafe") |
| 70 | + .toEndWith("awesome!"); |
| 71 | +``` |
| 72 | + |
| 73 | +The matcher functions depend on the type of the value on the `expect`. If you're using TypeScript, the compiler will let you know if something is not available for that assertion: |
| 74 | + |
| 75 | +```ts |
| 76 | +// Boolean assertion |
| 77 | +expect(isEven(2)).toBeTrue(); |
| 78 | + |
| 79 | +// String assertion |
| 80 | +expect("foobar").toStartWith("foo"); |
| 81 | + |
| 82 | +// Number assertion |
| 83 | +expect(sum(1, 2)).toBePositive(); |
| 84 | + |
| 85 | +expect(14).toEndWith("4"); |
| 86 | + ^ ? type error: `toEndWith` does not exist in `NumberAssertion` |
| 87 | +``` |
| 88 | + |
| 89 | +For a list of all matchers and extended documentation, please refer to the [API documentation](https://stackbuilders.github.io/assertive-ts/docs/build/). |
| 90 | + |
| 91 | +### Type Factory 🏭 |
| 92 | + |
| 93 | +A great feature of AssertiveTS is the type safety across the API. But, what should you do if you want to check the value under test is of some specific type during runtime? The answer is simple, AssertiveTS provides a `.asType(TypeFactory)` method, where the [TypeFactory](https://stackbuilders.github.io/assertive-ts/docs/build/interfaces/TypeFactory.html) parameter lets you check for the specific type and narrow the assertion instance to a more specific one. To make things simpler, AssertiveTS provides [TypeFactories](https://stackbuilders.github.io/assertive-ts/docs/build/interfaces/StaticTypeFactories.html) for the basic types: |
| 94 | + |
| 95 | +```ts |
| 96 | +import { expect, TypeFactories } from "@stackbuilders/assertive-ts"; |
| 97 | + |
| 98 | +expect(value) |
| 99 | + .asType(TypeFactories.String) |
| 100 | + .toBeEmpty(); |
| 101 | + |
| 102 | +expect(list) |
| 103 | + .asType(TypeFactories.array(TypeFactories.Number)) |
| 104 | + .toHaveSameMembers([1, 2, 3, 4, 5]); |
| 105 | +``` |
| 106 | + |
| 107 | +If the built-in type factories are not enough to assert your specific type, you can always create your own factory. A `TypeFactory<S, A>` is nothing more than an object with 3 properties: |
| 108 | + |
| 109 | +- `Factory: new(actual: S) => A` - The specific assertion constructor to return if the predicate is true. Where `S` is the actual value type, and `A` is the type of the assertion to return (`A` should extend from `Assertion<S>`). |
| 110 | +- `predicate(value: unknown): value is S` - A predicate function that checks if the value is of the expected type. |
| 111 | +- `typeName: string` - The name of the checked type. Used to make the assertion error message clearer. |
| 112 | + |
| 113 | +So, using a custom `TypeFactory` can look like the following: |
| 114 | + |
| 115 | +```ts |
| 116 | +interface Point3D { |
| 117 | + x: number; |
| 118 | + y: number; |
| 119 | + z: number; |
| 120 | +} |
| 121 | + |
| 122 | +expect(maybePoint).asType({ |
| 123 | + Factory: ObjectAssertion<Point3D>, |
| 124 | + predicate: (value): value is Point3D => { |
| 125 | + return typeof value === "object" |
| 126 | + && value !== null |
| 127 | + && "x" in value |
| 128 | + && "y" in value |
| 129 | + && "z" in value |
| 130 | + && Object.values(value).every(v => typeof v === "number"); |
| 131 | + }, |
| 132 | + typeName: "Point3D" |
| 133 | +}); |
| 134 | +``` |
| 135 | + |
| 136 | +### Handling TypeScript Unions |
| 137 | + |
| 138 | +[Union types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types) are a TypeScript concept that is only applicable at type level. During runtime, the value can only be one of the types. For instance, if we say `const foo: number | string = ...`, at runtime `foo` will be either a `number` or a `string`. If you want to use a more specific assertion on a union type, you can use `.asType(..)` to first assert the expected type, and then move forward with more assertions: |
| 139 | + |
| 140 | +```ts |
| 141 | +const foo: number | string = 5; |
| 142 | + |
| 143 | +expect(foo) |
| 144 | + .asType(TypeFactories.Number) |
| 145 | + .toBePositive(); |
| 146 | +``` |
| 147 | + |
| 148 | +### Help! The value can also be `null` or `undefined` |
| 149 | + |
| 150 | +When a value can be also `null` or `undefined`, we're going over the same concept as [Union types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types). So if you want to make more specific assertions over a value that can be `null | undefined`, just use `.asType(..)` first: |
| 151 | + |
| 152 | +```ts |
| 153 | +const bar: string | null | undefined = " "; |
| 154 | + |
| 155 | +expect(bar) |
| 156 | + .asType(TypeFactories.String) |
| 157 | + .toBeBlank(); |
| 158 | +``` |
| 159 | + |
| 160 | +## Test Runner Integration |
| 161 | + |
| 162 | +- [Jest Integration](https://github.com/stackbuilders/assertive-ts/blob/main/docs/jest-tutorial.md) |
| 163 | +- [Mocha Integration](https://github.com/stackbuilders/assertive-ts/blob/main/docs/mocha-tutorial.md) |
| 164 | + |
| 165 | +## API Reference |
| 166 | + |
| 167 | +You can find the full API reference [here](https://stackbuilders.github.io/assertive-ts/docs/build/) |
| 168 | + |
| 169 | +## Extension mechanism ⚙️ |
| 170 | + |
| 171 | +This feature allows you to extend the `expect(..)` function to return additional `Assertion<T>` instances depending on the value under test. This opens the door to add additional assertion matchers for more specific cases. An `Assertion<T>` can be added in the form of a `Plugin`: |
| 172 | +```ts |
| 173 | +interface Plugin<T, A extends Assertion<T>> { |
| 174 | + Assertion: new(actual: T) => A; |
| 175 | + insertAt: "top" | "bottom"; |
| 176 | + predicate: (actual: unknown) => actual is T; |
| 177 | +} |
| 178 | +``` |
| 179 | + |
| 180 | +Where `Assertion` is the class you want to add, `insertAt` determines if the logic is inserted before or after all the primitives, and `predicate` is the logical code used to determine if value matches the `Assertion` type. |
| 181 | + |
| 182 | +Once you have a plugin object, you can add it to assertive-ts with the `usePlugin(..)` helper function. Calls to this function should go on the setup file of your test runner or in a `beforeAll()` hook, so the extension is applied to all your tests. |
| 183 | +```ts |
| 184 | +// test/setup.ts |
| 185 | +import { usePlugin } from "@stackbuilders/assertive-ts"; |
| 186 | + |
| 187 | +import { FilePlugin, HTMLElementPlugin } from "./plugins"; // your custom (or 3rd-party) plugins |
| 188 | + |
| 189 | +usePlugin(FilePlugin); |
| 190 | +usePlugin(HTMLElementPlugin); |
| 191 | +// ... |
| 192 | +``` |
| 193 | + |
| 194 | +### What about the types? |
| 195 | + |
| 196 | +Each new plugin should add an additional overload to the `expect(..)` function to maintain type safety. To do that, you can extend the `Expect` interface to add the additional overloads. For example: |
| 197 | +```ts |
| 198 | +import { FileAssertion } from "./FileAssertion"; |
| 199 | +import { HTMLElementAssertion } from "./HTMLElementAssertion"; |
| 200 | + |
| 201 | +declare module "@stackbuilders/assertive-ts" { |
| 202 | + |
| 203 | + export interface Expect { |
| 204 | + (actual: File): FileAssertion; |
| 205 | + (actual: HTMLElement): HTMLElementAssertion; |
| 206 | + // ... |
| 207 | + } |
| 208 | +} |
| 209 | +``` |
| 210 | + |
| 211 | +> **Note:** 3rd-party libraries should do this on their types entry point (index.d.ts), this way the interface is automatically extended when their plugin is passed to the `usePlugin(..)` function. |
| 212 | +
|
| 213 | +### How to... |
| 214 | + |
| 215 | +If you're looking to write a plugin, you can find a simple example [here](https://github.com/stackbuilders/assertive-ts/blob/main/examples/symbolPlugin/). The example plugin is used in the [Jest](https://github.com/stackbuilders/assertive-ts/blob/main/examples/jest/test/plugins.test.ts) and [Mocha](https://github.com/stackbuilders/assertive-ts/blob/main/examples/mocha/test/plugins.test.ts) examples too, so you can also take a look at them to see how to apply and use plugins. |
| 216 | + |
| 217 | +## Contributors ✨ |
| 218 | + |
| 219 | +Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): |
| 220 | + |
| 221 | +<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> |
| 222 | +<!-- prettier-ignore-start --> |
| 223 | +<!-- markdownlint-disable --> |
| 224 | +<table> |
| 225 | + <tbody> |
| 226 | + <tr> |
| 227 | + <td align="center" valign="top" width="14.28%"><a href="https://github.com/JoseLion"><img src="https://avatars.githubusercontent.com/u/3087228?v=4?s=100" width="100px;" alt="Jose Luis Leon"/><br /><sub><b>Jose Luis Leon</b></sub></a><br /><a href="https://github.com/stackbuilders/assertive-ts/commits?author=JoseLion" title="Code">💻</a> <a href="#infra-JoseLion" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#maintenance-JoseLion" title="Maintenance">🚧</a> <a href="#platform-JoseLion" title="Packaging/porting to new platform">📦</a> <a href="https://github.com/stackbuilders/assertive-ts/commits?author=JoseLion" title="Tests">⚠️</a></td> |
| 228 | + <td align="center" valign="top" width="14.28%"><a href="https://github.com/byrpatrick"><img src="https://avatars.githubusercontent.com/u/37427699?v=4?s=100" width="100px;" alt="Byron Motoche"/><br /><sub><b>Byron Motoche</b></sub></a><br /><a href="https://github.com/stackbuilders/assertive-ts/commits?author=byrpatrick" title="Code">💻</a> <a href="https://github.com/stackbuilders/assertive-ts/commits?author=byrpatrick" title="Tests">⚠️</a> <a href="https://github.com/stackbuilders/assertive-ts/pulls?q=is%3Apr+reviewed-by%3Abyrpatrick" title="Reviewed Pull Requests">👀</a></td> |
| 229 | + <td align="center" valign="top" width="14.28%"><a href="https://github.com/alejo0o"><img src="https://avatars.githubusercontent.com/u/60680371?v=4?s=100" width="100px;" alt="Alejandro Vivanco"/><br /><sub><b>Alejandro Vivanco</b></sub></a><br /><a href="https://github.com/stackbuilders/assertive-ts/commits?author=alejo0o" title="Code">💻</a> <a href="https://github.com/stackbuilders/assertive-ts/commits?author=alejo0o" title="Tests">⚠️</a> <a href="https://github.com/stackbuilders/assertive-ts/pulls?q=is%3Apr+reviewed-by%3Aalejo0o" title="Reviewed Pull Requests">👀</a></td> |
| 230 | + <td align="center" valign="top" width="14.28%"><a href="https://github.com/dalejo96"><img src="https://avatars.githubusercontent.com/u/77456654?v=4?s=100" width="100px;" alt="David Villamarin"/><br /><sub><b>David Villamarin</b></sub></a><br /><a href="https://github.com/stackbuilders/assertive-ts/commits?author=dalejo96" title="Code">💻</a> <a href="https://github.com/stackbuilders/assertive-ts/commits?author=dalejo96" title="Tests">⚠️</a></td> |
| 231 | + <td align="center" valign="top" width="14.28%"><a href="https://github.com/Alex0jk"><img src="https://avatars.githubusercontent.com/u/22301755?v=4?s=100" width="100px;" alt="Alexander Mejía"/><br /><sub><b>Alexander Mejía</b></sub></a><br /><a href="https://github.com/stackbuilders/assertive-ts/commits?author=Alex0jk" title="Code">💻</a> <a href="https://github.com/stackbuilders/assertive-ts/commits?author=Alex0jk" title="Tests">⚠️</a></td> |
| 232 | + <td align="center" valign="top" width="14.28%"><a href="https://github.com/ChristianSama"><img src="https://avatars.githubusercontent.com/u/43491324?v=4?s=100" width="100px;" alt="Christian Samaniego"/><br /><sub><b>Christian Samaniego</b></sub></a><br /><a href="https://github.com/stackbuilders/assertive-ts/commits?author=ChristianSama" title="Documentation">📖</a> <a href="https://github.com/stackbuilders/assertive-ts/commits?author=ChristianSama" title="Code">💻</a> <a href="https://github.com/stackbuilders/assertive-ts/commits?author=ChristianSama" title="Tests">⚠️</a> <a href="https://github.com/stackbuilders/assertive-ts/pulls?q=is%3Apr+reviewed-by%3AChristianSama" title="Reviewed Pull Requests">👀</a></td> |
| 233 | + <td align="center" valign="top" width="14.28%"><a href="https://github.com/sestrella"><img src="https://avatars.githubusercontent.com/u/2049686?v=4?s=100" width="100px;" alt="Sebastián Estrella"/><br /><sub><b>Sebastián Estrella</b></sub></a><br /><a href="#infra-sestrella" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> |
| 234 | + </tr> |
| 235 | + <tr> |
| 236 | + <td align="center" valign="top" width="14.28%"><a href="https://github.com/nieled"><img src="https://avatars.githubusercontent.com/u/20074796?v=4?s=100" width="100px;" alt="Daniel Calle"/><br /><sub><b>Daniel Calle</b></sub></a><br /><a href="#infra-nieled" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> |
| 237 | + <td align="center" valign="top" width="14.28%"><a href="https://github.com/NeoLight1010"><img src="https://avatars.githubusercontent.com/u/58057324?v=4?s=100" width="100px;" alt="Anthony Suárez"/><br /><sub><b>Anthony Suárez</b></sub></a><br /><a href="https://github.com/stackbuilders/assertive-ts/commits?author=NeoLight1010" title="Documentation">📖</a></td> |
| 238 | + <td align="center" valign="top" width="14.28%"><a href="https://github.com/sebas1208"><img src="https://avatars.githubusercontent.com/u/5571870?v=4?s=100" width="100px;" alt="Sebastian Avalos"/><br /><sub><b>Sebastian Avalos</b></sub></a><br /><a href="https://github.com/stackbuilders/assertive-ts/pulls?q=is%3Apr+reviewed-by%3Asebas1208" title="Reviewed Pull Requests">👀</a></td> |
| 239 | + </tr> |
| 240 | + </tbody> |
| 241 | +</table> |
| 242 | + |
| 243 | +<!-- markdownlint-restore --> |
| 244 | +<!-- prettier-ignore-end --> |
| 245 | + |
| 246 | +<!-- ALL-CONTRIBUTORS-LIST:END --> |
| 247 | + |
| 248 | +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! |
| 249 | + |
| 250 | +## License |
| 251 | + |
| 252 | +MIT, see [the LICENSE file](https://github.com/stackbuilders/assertive-ts/blob/main/LICENSE). |
| 253 | + |
| 254 | +## Contributing |
| 255 | + |
| 256 | +Do you want to contribute to this project? Please take a look at our [contributing guideline](https://github.com/stackbuilders/assertive-ts/blob/main/docs/CONTRIBUTING.md) to know how you can help us build it. |
| 257 | + |
| 258 | +--- |
| 259 | +<img src="https://www.stackbuilders.com/media/images/Sb-supports.original.png" alt="Stack Builders" width="50%" /> |
| 260 | + |
| 261 | +[Check out our libraries](https://github.com/stackbuilders/) | [Join our team](https://www.stackbuilders.com/join-us/) |
0 commit comments