Skip to content

Commit

Permalink
feat: add getUpdateAndExecCmdFn function for unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
atheck committed Nov 16, 2022
1 parent 54fedb6 commit 699b90b
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 17 deletions.
35 changes: 20 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ This library brings the elmish pattern to react.
- [Call back parent components](#call-back-parent-components)
- [Testing](#testing)
- [Testing the update handler](#testing-the-update-handler)
- [Testing all (async) messages](#testing-all-async-messages)
- [Combine update and execCmd](#combine-update-and-execcmd)
- [UI Tests](#ui-tests)
- [Migrations](#migrations)
- [From v1.x to v2.x](#from-v1x-to-v2x)
Expand Down Expand Up @@ -852,10 +852,11 @@ To test your **update** handler you can use some helper functions in `react-elmi

| Function | Description |
| --- | --- |
| `getOfMsgParams` | Extracts the messages out of a command |
| `execCmd` | Executes the provided command and returns an array of all messages. |
| `getUpdateFn` | Returns an `update` function for your update map object. |
| `getUpdateAndExecCmdFn` | Returns an `update` function for your update map object, which immediately executes the command. |
| `createUpdateArgsFactory` | Creates a factory function to create a message, a model, and props in a test. |
| `getOfMsgParams` | Extracts the messages out of a command. You should not use this function, because its likely to be deprecated in near future. Use `execCmd` instead. |

### Testing the update handler

Expand All @@ -864,58 +865,62 @@ To test your **update** handler you can use some helper functions in `react-elmi
```ts
import { getUpdateFn } from "react-elmish/dist/Testing";

const update = getUpdateFn(updateMap);
const updateFn = getUpdateFn(updateMap);

// Call the update function in the test
const [model, cmd] = update(msg, model, props);
const [model, cmd] = updateFn(msg, model, props);
```

A simple test:

```ts
import * as Testing from "react-elmish/dist/Testing";
import { createUpdateArgsFactory, execCmd } from "react-elmish/dist/Testing";

const createUpdateArgs = Testing.createUpdateArgsFactory(() => ({ /* initial model */ }), () => ({ /* initial props */ }));
const createUpdateArgs = createUpdateArgsFactory(() => ({ /* initial model */ }), () => ({ /* initial props */ }));

...
it("returns the correct model and cmd", () => {
it("returns the correct model and cmd", async () => {
// arrange
const args = createUpdateArgs(Msg.test(), { /* optionally override model here */ }, { /* optionally override props here */ });

// act
// Call the update handler
const [newModel, cmd] = update(..args);
const [newModel, cmd] = updateFn(...args);
const messages = await execCmd(cmd);

// assert
expect(newModel).toStrictEqual({ /* what you expect in the model */ });
expect(Testing.getOfMsgParams(cmd)).toEqual([
expect(messages).toEqual([
Msg.expectedMsg1("arg"),
Msg.expectedMsg2(),
]);
});
...
```

### Testing all (async) messages

With `execCmd` you can execute all commands in a test scenario. All functions are called and awaited. The function returns all new messages (success or error messages).

It also resolves for `attempt` functions if the called functions succeed. And it rejects for `perform` functions if the called functions fail.

### Combine update and execCmd

There is an alternative function `getUpdateAndExecCmdFn` to get the `update` function for an update map, which immediately invokes the command and returns the messages.

```ts
import * as Testing from "react-elmish/dist/Testing";
import { createUpdateArgs, getUpdateAndExecCmdFn } from "react-elmish/dist/Testing";

const updateAndExecCmdFn = getUpdateAndExecCmdFn(updateMap);

...
it("returns the correct cmd", () => {
it("returns the correct cmd", async () => {
// arrange
const args = createUpdateArgs(Msg.asyncTest());

// mock function which is called when the "AsyncTest" message is handled
const functionMock = jest.fn();

// act
const [, cmd] = update(...args);
const messages = await Testing.execCmd(cmd);
const [, messages] = await updateAndExecCmdFn(...args);

// assert
expect(functionMock).toBeCalled();
Expand Down
29 changes: 28 additions & 1 deletion src/Testing/getUpdateFn.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@
import { Message, UpdateMap, UpdateReturnType } from "../Types";
import { Message, Nullable, UpdateMap, UpdateReturnType } from "../Types";
import { callUpdateMap } from "../useElmish";
import { execCmd } from "./execCmd";

/**
* Creates an update function out of an UpdateMap.
* @param {UpdateMap<TProps, TModel, TMessage>} updateMap The UpdateMap.
* @returns {(msg: TMessage, model: TModel, props: TProps) => UpdateReturnType<TModel, TMessage>} The created update function which can be used in tests.
* @example
* const updateFn = getUpdateFn(update);
*
* // in your test:
* const [model, cmd] = updateFn(...args);
*/
function getUpdateFn<TProps, TModel, TMessage extends Message> (updateMap: UpdateMap<TProps, TModel, TMessage>): (msg: TMessage, model: TModel, props: TProps) => UpdateReturnType<TModel, TMessage> {
return function (msg: TMessage, model: TModel, props: TProps): UpdateReturnType<TModel, TMessage> {
return callUpdateMap(updateMap, msg, model, props);
};
}

/**
* Creates an update function out of an UpdateMap which immediately executes the command.
* @param {UpdateMap<TProps, TModel, TMessage>} updateMap The UpdateMap.
* @returns {(msg: TMessage, model: TModel, props: TProps) => Promise<[Partial<TModel>, Nullable<TMessage> []]>} The created update function which can be used in tests.
* @example
* const updateAndExecCmd = getUpdateFnWithExecCmd(update);
*
* // in your test:
* const [model, messages] = await updateAndExecCmd(...args);
*/
function getUpdateAndExecCmdFn<TProps, TModel, TMessage extends Message> (updateMap: UpdateMap<TProps, TModel, TMessage>): (msg: TMessage, model: TModel, props: TProps) => Promise<[Partial<TModel>, Nullable<TMessage> []]> {
return async function (msg: TMessage, model: TModel, props: TProps): Promise<[Partial<TModel>, Nullable<TMessage> []]> {
const [updatedModel, cmd] = callUpdateMap(updateMap, msg, model, props);

const messages = await execCmd(cmd);

return [updatedModel, messages];
};
}

export {
getUpdateFn,
getUpdateAndExecCmdFn,
};
3 changes: 2 additions & 1 deletion src/Testing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createUpdateArgsFactory, UpdateArgsFactory } from "./createUpdateArgsFa
import { execCmd } from "./execCmd";
import { RenderWithModelOptions } from "./fakeOptions";
import { getOfMsgParams } from "./getOfMsgParams";
import { getUpdateFn } from "./getUpdateFn";
import { getUpdateAndExecCmdFn, getUpdateFn } from "./getUpdateFn";
import { renderWithModel } from "./renderWithModel";

export type {
Expand All @@ -14,6 +14,7 @@ export {
getOfMsgParams,
execCmd,
getUpdateFn,
getUpdateAndExecCmdFn,
createUpdateArgsFactory,
renderWithModel,
};

0 comments on commit 699b90b

Please sign in to comment.