npm package: @gmjs/test-util
Description: Test utilities.
Author: Goran Mržljak
Latest version: 0.0.15

# Test Utilities

This project contains utilities for writing file comparison tests.

## Installation

```bash
npm install --save-dev @gmjs/test-util
```

## API

### File Test Functions

High-level functions to do the heavy lifting in file comparison tests.

#### `createFsTestCases`

Creates test cases for file system tests.

These are essentially tests for code that generates a file hierarchy. We can use it to specify expected file hierarchy that should result from the code under test, and compare it to the actual file hierarchy generated by the code under test.

So how do we do that?

First of all, this function expects to find a directory with test cases as described in [Test Case File Structure Setup](#test-case-file-structure-setup).

##### Parameters

The first parameter to the function, `testCasesRoot` specifies the path to the above mentioned directory (`<test-cases-root>`).

The second parameter, `actualFunction` is the actual code under test. You define this function yourself, and pass it to `createFsTestCases()`. Actual function will receive the full path to the test case directory, and it should return a promise that resolves to a [FilesContainer](#filescontainer) object.

##### Return Value

The result of `createFsTestCases()` is an array of `TestCaseRun` objects:

```ts
interface TestCaseRun {
  readonly name: string;
  readonly run: () => Promise<TestComparisonStrings>;
}

interface TestComparisonStrings {
  readonly expected: string;
  readonly actual: string;
}
```

`name` is simply the name of the test case directory. For example, if the test case directory is `<test-cases-root>/cases/test-case-01`, then `name` will be `test-case-01`.

##### Running Tests

The actual test is executed by calling the `run()` function returned from `createFsTestCases()`. This function returns a promise, which resolves to a `TestComparisonStrings` object, which can be used for a simple string comparison test.

`expected` and `actual` strings are generated internally from the `expected` and `actual` [FilesContainer](#filescontainer) objects. The string will contain a list of files, each with its path and content. The list of files is sorted by path, so that the order of files is deterministic. For binary files, only the hash is printed, not the actual content.

##### Example

```ts
describe('test', () => {
  const testCaseRuns = createFsTestCases('path/to/test-cases-root', getActualFiles);

  for (const testCaseRun of testCaseRuns) {
    it(testCaseRun.name, async () => {
      const { expected, actual } = await testCaseRun.run();
      expect(actual).toBe(expected);
    });
  }
});

async function getActualFiles(testCaseDirectory: string): Promise<FilesContainer> {
  // code under test, returns a FilesContainer object
}
```

## Test Case File Structure Setup

This is the test case file structure used by [createFsTestCases](#createfstestcases) function.

This function expects to find a directory with test case files which will produce `actual` and `expected` results for a final test comparison.

Below is an example with a single test case, and a single shared directory.

```
<test-cases-root>
  cases
    test-case-01
      expected
        files
          subdir
            expected-11.png.bin
          expected-1.js.txt
          expected-2.json.txt
        path-mapping.json
      input
        input.json
  shared
    shared-dir
      shared-1.js.txt
      shared-2.ts.txt
```

### Test Case

Test cases can have arbitrary names, not just `test-case-*`.

There can be any number of test cases in `<test-cases-root>/cases` directory.

`<test-case>/expected/files` directory contains files that will be used to create the `expected` result, this is the input for the mapping defined in `path-mapping.json`.

Each file under `<test-case>/expected/files` must have a `.txt` or `.bin` extension, which determines whether the file is read as text or binary.

`<test-case>/expected/path-mapping.json` file contains a list of `fr`-`to` entries, which are used to map files from `<test-case>/expected/files` to the final `expected` file system structure.

Besides `<test-case>/expected` directory, you can put any other directories and files inside `<test-case>` directory. You can for example use these as inputs for creating the `actual` result in case you need any inputs. Then you can read those inputs inside `actualFunction`. However, since you provide `actualFunction` yourself, you have full control of this process and you can generate the `actual` result in any way you want, using any inputs you want.

### Shared Directories

`<test-cases-root>/shared` directory contains a list of shared directories, each with shared files that can be referenced from any test case. These are essentially shared `expected` files, ones which would otherwise be inside `<test-case>/expected/files`, but are moved to a shared directory to avoid duplication. Like files under `<test-case>/expected/files`, files under `<test-cases-root>/shared` must also have a `.txt` or `.bin` extension.

Directories under `<test-cases-root>/shared` can have arbitrary names. Let's say we have the following directory structure:

```
<test-cases-root>
  cases
    ...
  shared
    a-shared-dir
      shared-1.js.txt
      shared-2.ts.txt
    another-shared-dir
      shared-3.json.txt
      shared-4.svg.txt
```

Then, in `path-mapping.json` files, you can reference files under `<test-cases-root>/shared` like this (`<a-shared-dir>` etc. are literal, verbatim values in the mapping json file, not documentation placeholders like `<test-cases-root>`):

```json
[
  {
    "group": "some-group",
    "files": [
      {
        "fr": "<a-shared-dir>/shared-1.js.txt",
        "to": "target-dir/some-file.js"
      },
      {
        "fr": "<another-shared-dir>/shared-3.json.txt",
        "to": "target-dir/some-other-file.json"
      }
    ]
  }
]
```

Note that for example `<a-shared-dir>` refers to `<test-cases-root>/shared/a-shared-dir`.

`<test-cases-root>/shared` is optional, and you don't have to create it if you don't use it in `path-mapping.json` files.

### Test Case Path Mapping

`<test-case>/expected/path-mapping.json` file contains a list of `fr`-`to` entries, which are used to map files from `<test-case>/expected/files` to the final `expected` file system structure.

More precisely, `path-mapping.json` will contain groups of `fr`-`to` entries, but these groups are arbitrary, used just for visual organization of the file, and the lists of `fr`-`to` entries are simply flattened into a single list.

Using the first directory structure example, we can have the following file mapping:

```json
[
  {
    "group": "assets",
    "files": [
      {
        "fr": "subdir/expected-11.png.bin",
        "to": "target-dir/assets/asset-11.png"
      }
    ]
  },
  {
    "group": "src",
    "files": [
      {
        "fr": "expected-1.js.txt",
        "to": "target-dir/src/file-1.js"
      },
      {
        "fr": "expected-2.json.txt",
        "to": "target-dir/src/file-2.json"
      },
      {
        "fr": "<shared-dir>/shared-1.js.txt",
        "to": "target-dir/src/shared-file-1.js"
      },
      {
        "fr": "<shared-dir>/shared-2.ts.txt",
        "to": "target-dir/src/shared-file-2.ts"
      }
    ]
  }
]
```

You can think of the above as a flat list of 5 file mappings, which will produce the following file structure in the `expected` result:

```
target-dir
  assets
    asset-11.png
  src
    file-1.js
    file-2.json
    shared-file-1.js
    shared-file-2.ts
```

## Types

### `FilesContainer`

Container with file path-content pairs.

Has the following structure:

```ts
interface FilesContainer {
  readonly textFiles: readonly FilePathTextContent[];
  readonly binaryFiles: readonly FilePathBinaryContent[];
}
```

Items in the above array have essentially the following structure:

```ts
interface FilePathTextContent {
  readonly path: string;
  readonly content: string;
}

interface FilePathBinaryContent {
  readonly path: string;
  readonly content: Buffer;
}
```