Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: Explain how FlowStages work and their rationale #558

Merged
merged 12 commits into from
Mar 8, 2024
3 changes: 2 additions & 1 deletion packages/openactive-integration-tests/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ These consist of:

- [Chakram](http://dareid.github.io/chakram/): This is a HTTP test framework designed for Mocha (however it works fine on Jest)
- [Request helper](test/helpers/request-helper.js): This makes requests, and records the request + response against the logger. There are methods to directly make requests, along with methods for each API endpoint.
- [Flow Stages](test/helpers/flow-stages/flow-stage.js): A part of the booking flow (e.g. the C1 request). Use it to call the relevant API endpoint. When called, it stores the results, which can then be applied to successive Flow Stages (e.g. C1 output can be used for the C2 request).
- [Flow Stages](test/helpers/flow-stages/README.md): A part of the booking flow (e.g. the C1 request). Use it to call the relevant API endpoint. When called, it stores the results, which can then be applied to successive Flow Stages (e.g. C1 output can be used for the C2 request).

# Test flow

- [Feature helper](test/helpers/feature-helper.js): This wraps up the initialisation of the test, implementing the describe blocks and initialising the logger.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Flow Stages

A Flow is a sequence of API calls that are made by Test Suite, for a given test, against either the Booking System or the Broker Microservice. A Flow Stage encapsulates the configuration and logic for one of those API calls. e.g. there is a FlowStage for C1 (which is in the Booking System), a FlowStage for Fetch Opportunities (which uses the Broker Microservice), etc.
lukehesluke marked this conversation as resolved.
Show resolved Hide resolved

As an example, for [`opportunity-free-test`](packages/openactive-integration-tests/test/features/payment/free-opportunities/implemented/opportunity-free-test.js), when running in [Simple Booking Flow](https://openactive.io/open-booking-api/EditorsDraft/#simple-booking-flow), the following Flow Stages are utilised:

1. Fetch Opportunities
2. C1
3. Assert Opportunity Capacity (after C1)
4. C2
5. Assert Opportunity Capacity (after C2)
6. B
7. Assert Opportunity Capacity (after B)

Each of these FlowStages does the following, when run in a test:

1. Receives some input from previous FlowStages (if applicable)
- This input can be provided to each FlowStage via `getInput(..)`, which is provided in the constructor. e.g. to have a C1 FlowStage receive the output of a Fetch Opportunities FlowStage, use the following in the C1 FlowStage's constructor:
```js
const fetchOpportunities = new FetchOpportunitiesFlowStage({ /* ... */ });
const c1 = new C1FlowStage({
getInput: () => ({
orderItems: fetchOpportunities.getOutput().orderItems,
}),
// ... other args
});
```
2. Performs an API call
- Using `runFn(..)`, which is provided in the constructor
3. Transforms the output
- This is also performed in `runFn(..)`, which is provided in the constructor
4. Sends (some of) this output to subsequent FlowStages
- This output is automatically saved by the FlowStage as the output from `runFn(..)`, which is provided in the constructor, and can be retrieved by calling `getOutput(..)` on the FlowStage instance.

FlowStages can then be queried after they've run in order to:

1. Check that the FlowStage was successful
- This is overridable per-FlowStage. For example, B's Flow Stage considers the run successful if the HTTP response has status 201
Copy link
Contributor

@civsiv civsiv Aug 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there default success checks? I can't remember if it does a default 200 check? If so something like:

Suggested change
- This is overridable per-FlowStage. For example, B's Flow Stage considers the run successful if the HTTP response has status 201
- This is overridable per-FlowStage. For example, B's Flow Stage considers the run successful if the HTTP response has status 201 as opposed to the default FlowStage behaviour that checks for status 200

The only reason I'm wondering whether there's default behaiour is due to the word "overridable". If there's no default behaviour, what's being overridden?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are not. So I'll change the word overridable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DONE

- FlowStage method: `itSuccessChecks()`
2. Perform validation checks on the output
- This is overridable per-FlowStage. In all cases, this is a case of calling [Validator](https://github.com/openactive/data-model-validator) on the HTTP output.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here re overridable

- FlowStage method: `itValidationTests()`

## FlowStageRunnable

An abstraction that can be either a **Flow Stage**, [**Book Recipe**](./book-recipe.js) or a [**Flow Stage Run**](./flow-stage-run.js). This represents a single FlowStage or sequence of FlowStages that can be run.
lukehesluke marked this conversation as resolved.
Show resolved Hide resolved

This encapsulation allows us to, for example, more easily reason about tests in which the "book" stage could use either [Simple Booking Flow](https://openactive.io/open-booking-api/EditorsDraft/#simple-booking-flow) or [Booking Flow with Approval](https://openactive.io/open-booking-api/EditorsDraft/#booking-flow-with-approval), which involve different sets of API calls.

## Jest Tests

Flows, consisting of Flow Stages, run the underlying API calls which are being tested, via Jest in the various Test Suite tests.

Jest tests involve a custom course of execution, in which setup occurs in `beforeEach`/`beforeAll` hooks, tests are run in `it` hooks, etc. Flow Stages are designed to work with this.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Jest tests involve a custom course of execution, in which setup occurs in `beforeEach`/`beforeAll` hooks, tests are run in `it` hooks, etc. Flow Stages are designed to work with this.
Jest tests involve a custom course of execution, in which setup occurs in `beforeEach`/`beforeAll` hooks and tests are run in `it` hooks, etc. Flow Stages are designed to work with this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i've done it slightly differently involving semicolons


Here is how Flow Stages slot into Jest's test execution lifecycle:

- (Outside of hooks): A `FlowStage` class instance is created. The configuration of this FlowStage defines what will happen when it is run.
- `describe` hook: Contains the execution and checking logic for a given test.
- `beforeAll` hook: Runs the FlowStage.
- `it` hooks: Run success, validation, and any additional tests on the output of the FlowStage.

## FlowStageUtils

[FlowStageUtils](./flow-stage-utils.js) is a collection of utility functions which simplify and standardise the process of using FlowStages to write Test Suite tests.

Of particular importance is the `describeRunAndRunChecks(..)` function, which creates a Jest `describe(..)` hook for a given **FlowStageRunnable** which performs the running and checking of the runnable as described in the **Jest Tests** section.

## FlowStageRecipes

Setting up a flow, consisting of multiple FlowStages where each feeds input into the next, can require a lot of boilerplate and repeated logic. In most cases, parts of these flows can be packaged up as they will be the same in lots of different tests.
lukehesluke marked this conversation as resolved.
Show resolved Hide resolved

These packaged up flows are stored in [FlowStageRecipes](./flow-stage-recipes.js).

An example is `initialiseSimpleC1C2BookFlow(..)`, which sets up a FetchOpportunities -> C1 -> C2 -> Book flow, which works with either [Simple Booking Flow](https://openactive.io/open-booking-api/EditorsDraft/#simple-booking-flow) or [Booking Flow with Approval](https://openactive.io/open-booking-api/EditorsDraft/#booking-flow-with-approval). This is used in many tests which simply make a booking with a certain configuration and simply check that it was successful or failed as expected.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const { assertIsNotNullish } = require('../asserts');
*/

/**
* A set of FlowStages and/or FlowStageRuns, which can be treated as a single sequence to be run.
*
* @template {{ [stageName: string]: UnknownFlowStageType | FlowStageRun<any> }} TStages
*/
class FlowStageRun {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ class FlowStage {
* This input goes into `getInput`. It's a function as it will be called when
* the FlowStage is run (rather than when the FlowStage is set up). Therefore,
* it will have acccess to the output of any prerequisite stages.
*
* Note that this is usually used to get the output of a prerequisite stage, but
* is flexible to other use cases.
* @param {string} args.testName Labels the jest `describe(..)` block
* @param {(input: TInput) => Promise<TOutput>} args.runFn
* @param {(flowStage: FlowStage<unknown, TOutput>) => void} args.itSuccessChecksFn
Expand Down Expand Up @@ -198,7 +201,10 @@ class FlowStage {
*
* If there is a prerequisite stage, it will be run first.
*
* The result is cached.
* The result is cached. This means that FlowStages will only be run once, while
* allowing for some flexibility on how they are run. For example, a test could only
* run the `run()` method on the last FlowStage in a flow, and this would automatically
* run all pre-requisite stages.
*/
run = pMemoize(async () => {
// ## 1. Run prerequisite stage
Expand Down