> For the complete documentation index, see [llms.txt](https://docs.contextual.io/documentation-and-resources/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.contextual.io/documentation-and-resources/components-and-data/flows/node-reference/testing/test.md).

# Test

Use a Test node in your Flow to define one or more test cases directly on the Workspace canvas. Each case specifies an input message, an expected result, and the downstream node whose incoming message the expected result is asserted against.

A Test node acts as a stand-in for whatever normally starts the flow, such as an HTTP In or Inject node. Wire it into the flow at the point where that real entry point would sit. When a case runs, the node sends its message down that wire and the rest of the flow up to the designated Output node executes for real.

## Case Fields

A single Test node can hold a list of cases. Every case is made up of five fields.

| Field        | Description                                                                                                                    |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| **Label**    | Display name for the case. Shown in the Testing panel and in test results.                                                     |
| **Message**  | The input msg object, entered as JSON. This is sent down the wire in place of a real trigger payload.                          |
| **Expected** | A JSON object asserted against whatever arrives at **Output**. The match is partial — see [Match Semantics](#match-semantics). |
| **Output**   | The ID of the downstream node to watch. The test runner listens for a message arriving at this specific node.                  |
| **Timeout**  | Milliseconds to wait for a message at **Output** before the case is marked as failed. Defaults to 5000.                        |

## How a Test Case Runs

* Enable testing mode from the **Testing** tab in the Flow Editor sidebar.
* Select the play button next to a test case to run it, or select the play button next to the name of the Test node to run all the test cases for that node. The Test node emits the case's **Message** from its single output port.
* The message travels through the flow's normal wiring. Every node in between executes for real.
* The runner watches the node named in **Output**. As soon as that node finishes processing the message,the results are compared against **Expected**.
* If a match is found before **Timeout** elapses, the case passes and displays a green checkmark in the Testing panel. If the timeout elapses first, or if no matching message arrives, the case fails and displays a red X in the Testing panel with a diff comparison showing the expected vs. received output.

## Assertion vs. Broken Flow

The runner matches any message that reaches **Output**, whichever branch of the flow produced it. A message emitted by a Catch node or any other error path is compared against **Expected** exactly like one from the success path, so error handling can make a case pass or fail. It only changes the outcome when it routes nothing to **Output** at all, in which case the case times out.

This makes error paths testable in their own right: give the failure branch a distinct **Expected** value and assert on it directly.

Return a message on **both** the success and failure branches (not just on success). This way:

* **Mismatch** = the assertion ran and disagreed with your expected value
* **Timeout** = nothing arrived at all (flow is broken or wiring is wrong)

## Match Semantics

Matching between **Expected** and the message arriving at **Output** is partial. The actual message only needs to *contain* the keys and values listed in **Expected**; it does not need be an exact match.

Because matching is partial, **Expected** only needs to name the fields that matter to the assertion. You do not have to reconstruct the whole message shape.

**What to include in Expected:**

* Only the fields your test actually validates
* Extra fields in the actual message are ignored (they won't cause test failure)

**What NOT to include in Expected:**

* `_msgid` — This is generated at runtime and will not match between test runs
* `_contextual` — This is auto-populated and should not be asserted against
* Any other auto-generated or dynamic fields specific to your flow environment

**Type Matching (Important):**

* String `"1"` does **not** match number `1`
* String `"true"` does **not** match boolean `true`
* `null` does **not** match `undefined`
* No type coercion is performed — types must match exactly
* For substring assertions, word boundaries are respected (e.g., `"hello"` does not match inside `"hello world"`)

## Why Not Use an Inject Node

For HTTP routes, an Inject node does not create the real HTTP response object that an HTTP Response node needs, so Inject nodes cannot reach an HTTP Response terminal. Depending on the runtime path, the flow may warn that there is no response object to send or otherwise fail when the HTTP Response node tries to use an incomplete response object.

A Test node avoids this. Point its **Output** at the last meaningful node before the HTTP Response node — for example, the Function node that shapes the response body — and assert against that node instead.

## Example: Asserting on an AI Generate Node

This example checks how [AI Generate node](https://docs.contextual.io/documentation-and-resources/components-and-data/flows/node-reference/ai-gateway/ai-generate) behaves when its **Tools** option is set to *None*, so the model is given no callable tools and must answer from its own knowledge.

### Flow Structure

The main chain runs left to right, ending at Log 3 — the node the Test node watches:

Test node → AI Generate → Log 2 → Function → Log 3

A separate error-handling branch runs only if Function throws. It is not connected by a wire; a group-scoped Catch node listens for errors raised anywhere inside the group:

Catch: group → Log 5 → HTTP Response

| Node          | Type            | Role                                                       |
| ------------- | --------------- | ---------------------------------------------------------- |
| Test node     | contextual-test | Injects the case message and watches Log 3                 |
| AI Generate   | ai-generate     | Sends the prompt to the model with no tools available      |
| Log 2         | log-tap         | Taps the raw model response for the debug sidebar          |
| Function      | function        | Converts the pass/fail decision into a plain payload value |
| Log 3         | log-tap         | The case's **Output** node                                 |
| Catch: group  | catch           | Catches errors thrown inside the group                     |
| Log 5         | log-tap         | Captures the full msg, including error details             |
| HTTP Response | http response   | Terminal node for the error branch                         |

### Input

The case's **Message** is emitted from the Test node's output and enters the flow at the AI Generate node:

```json
{
  "payload": {
    "prompt": "What is the capital of France?"
  }
}
```

### AI Generate Configuration

The AI Generate node reads its prompt from msg.payload and calls the open-ai route. Its **Tools** selector is set to *None*, so the model cannot call out to any tool and can only respond from its own training.

| Setting          | Value         |
| ---------------- | ------------- |
| route            | open-ai       |
| tools            | None selected |
| outputProperty   | payload       |
| responseProperty | \_response    |
| retries          | 3             |

### Assertion Logic

Log 2 taps the raw model response for the debug sidebar, then hands it to Function, which turns the pass/fail decision for this case into a plain payload value:

```javascript
// Safely convert AI output to string
const aiOutput = JSON.stringify(msg.payload);

// Match paris / paris. in any case
const regex = /\bparis\.?\b/i;

if (regex.test(aiOutput)) {
  msg.payload = "success";
  return msg;
} else {
  // Set failure payload
  msg.payload = "failure";

  // Report error so Catch node triggers
  node.error("Expected word 'paris' not found in AI output", msg);

  // Still return msg so flow can continue with failure
  return msg;
}
```

Regardless of outcome, msg is still returned, so Log 3 always receives something to check **Expected** against — including on the failure branch that also throws to the Catch node.

### Output

Log 3 is the node named as this case's **Output**. The runner waits up to the case timeout for a message to arrive there and checks it against **Expected**:

| Field        | Value                    |
| ------------ | ------------------------ |
| **Output**   | Log 3                    |
| **Expected** | { "payload": "success" } |
| **Timeout**  | 50000 ms                 |

The case passes only when Function's regex finds the word "paris" somewhere in the model's answer within 50 seconds. It fails on timeout or on any other payload value.

### Error Path

This branch is separate from the Test node's pass/fail check. It is the group's real error-handling path — what a live HTTP caller would receive if Function's assertion throws, independent of whatever the Test node itself reports.

| Node          | Role                                                                                                                                                                                                                    |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Catch: group  | Configured with scope-select set to *group* and uncaught set to false. Listens for any node.error() thrown by a node inside the same canvas group — in practice, only Function's failure branch.                        |
| Log 5         | A Log Tap set to capture the full msg rather than only msg.payload (unlike Log 2 and Log 3), at *debug* level, sent to the sidebar only. Logging the whole message preserves the error details the Catch node attached. |
| HTTP Response | The terminal node for this branch. Its **Status Code** field and **Headers** list are both left blank.                                                                                                                  |

> **Ignore errors handled by other Catch nodes** is unchecked on the Catch node, so it fires even if a broader, tab-level Catch node already handled the same error. With only one Catch node in this group that is not currently a problem, but it is worth keeping in mind if a tab-level Catch node is added later.

### How the Blank Status Code Behaves

The platform resolves the HTTP response status in this order:

1. If the node's own **Status Code** field is non-empty, that value wins and any msg.statusCode is ignored.
2. If the node's **Status Code** field is blank, the platform falls back to msg.statusCode from the incoming message.
3. If neither is set, the response defaults to 200.

In this example the node's field is blank, and nothing upstream (Function, Catch, or Log 5) sets msg.statusCode, so the error branch returns a plain 200 OK.

> **Warning:** A caller hitting this route cannot tell from the HTTP status alone that the assertion failed; they would have to inspect the response body. Setting msg.statusCode = 500 in Function's failure branch, or configuring an explicit status code on the HTTP Response node, makes the failure visible over HTTP.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.contextual.io/documentation-and-resources/components-and-data/flows/node-reference/testing/test.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
