{"id":13442265,"url":"https://github.com/nubank/state-flow","last_synced_at":"2025-05-14T14:07:28.197Z","repository":{"id":38984867,"uuid":"137817093","full_name":"nubank/state-flow","owner":"nubank","description":"Integration testing framework using a state monad in the backend for building and composing flows","archived":false,"fork":false,"pushed_at":"2025-03-20T21:55:36.000Z","size":496,"stargazers_count":350,"open_issues_count":6,"forks_count":15,"subscribers_count":827,"default_branch":"master","last_synced_at":"2025-05-07T23:57:47.819Z","etag":null,"topics":["nubank-stable"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nubank.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-06-18T23:27:41.000Z","updated_at":"2025-05-07T14:29:20.000Z","dependencies_parsed_at":"2024-05-01T13:20:42.336Z","dependency_job_id":"394b9e61-d3ad-4921-8cde-d21ab901dc8a","html_url":"https://github.com/nubank/state-flow","commit_stats":{"total_commits":283,"total_committers":27,"mean_commits":"10.481481481481481","dds":0.431095406360424,"last_synced_commit":"4f2f888ce70d2f635ab30ef341c296279367fc53"},"previous_names":[],"tags_count":68,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nubank%2Fstate-flow","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nubank%2Fstate-flow/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nubank%2Fstate-flow/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nubank%2Fstate-flow/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nubank","download_url":"https://codeload.github.com/nubank/state-flow/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254159194,"owners_count":22024558,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["nubank-stable"],"created_at":"2024-07-31T03:01:43.685Z","updated_at":"2025-05-14T14:07:28.148Z","avatar_url":"https://github.com/nubank.png","language":"Clojure","funding_links":[],"categories":["Clojure","Uncategorized","Testing"],"sub_categories":["Uncategorized"],"readme":"# StateFlow\n\n[![Clojars Project](https://img.shields.io/clojars/v/nubank/state-flow.svg)](https://clojars.org/nubank/state-flow)\n\nStateFlow is a testing framework designed to support the composition and reuse of individual test steps.\n\n## Learning Materials\n- [Tutorial](./samples/tutorial.clj)\n- [Walkthrough](./doc/walkthrough.repl)\n\n## Definitions\n\n* A [*flow*](#flows) is a sequence of steps or bindings.\n* A [*step*](#primitive-steps) is a primitive step or flow.\n* A [*binding*](#bindings) is a vector of pairs of symbols and steps (or a :let with a vector of regular let-bindings)\n\n## Flows\n\nA flow is a sequence of steps or bindings to be executed with some state as a\nreference. Use the `flow` macro to define a flow:\n\n```clojure\n(flow \u003cdescription\u003e \u003cstep/bindings\u003e*)\n```\n\nOnce defined, you can run it with `(state-flow.api/run* \u003coptions\u003e (flow ...))`.\n\nYou can think flows and the steps within them as functions of the state, e.g.\n\n```clojure\n(fn [\u003cstate\u003e] [\u003creturn-value\u003e, \u003cpossibly-updated-state\u003e])\n```\n\nEach step is executed in sequence, passing the state to the next step. The return value from running the flow is the return value of the last step that was run.\n\n### Primitive steps\n\nPrimitive steps are the fundamental building blocks of flows.\n\n* Return the application of a function f to the state.\n\n```clojure\n(state-flow.api/get-state f)\n```\n\n* Store the application of a function f to the state.\n\n```clojure\n(state-flow.api/swap-state f)\n```\n\n* Transform a value returned by a step\n\n``` clojure\n(state-flow.api/fmap xform \u003cstep-or-flow\u003e)\n```\n\n* Return a value\n\n```clojure\n(state-flow.api/return v)\n```\n\n* Invoke a no-arg function and return its result\n\n``` clojure\n(state-flow.api/invoke no-arg-fn)\n```\n\n### Bindings\n\nBindings bind return values of steps to symbols you can use in other steps.\n\n`[(\u003csymbol\u003e \u003cstep\u003e)+]`\n\nThey are like `let` bindings but the symbol on the left binds to the _return value_ of the step on the right.\n\n```clojure\n[\u003csymbol\u003e \u003cstep-or-flow\u003e]\n ```\n\nYou can also bind directly to values using the `:let` keyword:\n\n```clojure\n[:let [\u003csymbol\u003e \u003cnon-step expression\u003e]]\n ```\n\nYou can bind any number of symbols in a single binding vector, e.g.\n\n```clojure\n[a     step-1\n b     step-2\n :let [c expression-1]\n d     step-3]\n ```\n\n### Running Flows\n\nIf you are using StateFlow for integration testing, the initial state is usually a representation of your service components,\na system using [Stuart Sierra's Component](https://github.com/stuartsierra/component) library or other similar facility. You can also run the same flow with different initial states, e.g.\n\n```clojure\n(def a-flow (flow ...))\n\n(defn build-initial-state [] { ... })\n(state-flow.api/run* {:init build-initial-state} flow)\n\n(state-flow.api/run* {:init (constantly {:service-system (atom nil))} flow)\n```\n\n### Composing Flows\n\nFlows follow the Composite Pattern: a single flow has the same\ninterface as a collection of flows.\n\nYou can compose flows by nesting them in other flows:\n\n``` clojure\n(flow \"do many things\"\n  (flow \"do one thing\" ,,,)\n  (flow \"do another thing\" ,,,))\n```\n\nUse `state-flow.api/for` when you have a flow that you'd like to apply\nto different inputs with the same outcome, e.g.\n\n``` clojure\n(flow \"even? returns true for even numbers\"\n  (flow/for [x (filter even? (range 10))]\n    (match? even? x)))\n```\n\n#### Failing Fast\n\nBy default, a flow continues to be evaluated even if an assertion fails. The `:fail-fast?` option to `state-flow.api/run*` can be used if you would like to stop evaluation after the first assertion failure.\n\n```clojure\n(state-flow.api/run* {:fail-fast? true}\n  (flow \"evaluation stops after `failing-flow-b`\"\n    flow-a\n    failing-flow-b\n    flow-c))\n```\n\n### Flow Example\n\nSuppose our system state is made out of a map with `{:value \u003cvalue\u003e}`. We can make a flow that just\nfetches the value bound to `:value`.\n\n```clojure\n(require '[state-flow.api :as flow :refer [flow]])\n(def get-value (flow \"get-value\" (flow/get-state :value)))\n(flow/run* {:init (constantly {:value 4})} get-value)\n; =\u003e [4 {:value 4}]\n```\n\nPrimitive steps have the same underlying structure as flows and can be passed directly to `run*`:\n\n```clojure\n(def get-value (flow/get-state :value))\n(flow/run* {:init (constantly {:value 4})} get-value)\n; =\u003e [4 {:value 4}]\n```\n\nWe can use `state-flow.api/swap-state` to modify the state. Here's a primitive that increments the value:\n\n```clojure\n(def inc-value (flow/swap-state update :value inc))\n(flow/run* {:init (constantly {:value 4})} inc-value)\n; =\u003e [{:value 4} {:value 5}]\n```\n\nBindings enable us to compose simple flows into more complex flows.\nIf, instead of returning the value, we wanted to return the value\nmultiplied by two, we could do it like this:\n\n```clojure\n(def double-value\n  (flow \"get double value\"\n    [value get-value]\n    (flow/return (* value 2))))\n(flow/run* {:init (constantly {:value 4})} double-value)\n; =\u003e [8 {:value 4}]\n```\n\nOr we could increment the value first and then return it doubled:\n\n```clojure\n(def inc-and-double-value\n  (flow \"increment and double value\"\n    inc-value\n    [value get-value]\n    (flow/return (* value 2))))\n(flow/run* {:init (constantly {:value 4})} inc-and-double-value)\n; =\u003e [10 {:value 5}]\n```\n\n## clojure.test and matcher-combinators\n\nWe use the `defflow` and `match?` macros to build `clojure.test` tests\nout of flows.\n\n`state-flow.api/defflow` defines a test (using `deftest`) that\nwill execute the flow with the parameters that we set.\n\n`state-flow.assertions.matcher-combinators/match?` produces a flow that will make an assertion, which\nwill be reported via clojure.test when used within a `defflow`. It\nuses the\n[`nubank/matcher-combinators`](https://github.com/nubank/matcher-combinators/)\nlibrary for the actual check and failure messages. `match?` asks for:\n\n* the expected value, or a matcher-combinators matcher\n  * if you supply a value, matcher-combintators will apply its defaults\n* the actual value, or a step which will produce it\n  * if you supply a value, `match?` will wrap it in `(state-flow.api/return \u003cvalue\u003e)`\n* optional map of options with:\n  * `:times-to-try` (default 1)\n  * `:sleep-time`   (default 200)\n\nHere are some very simple examples of tests defined using `defflow`:\n\n```clojure\n(defflow my-flow\n  (match? 1 1)\n  (match? {:a 1} {:a 1 :b 2}))\n```\n\nWrap them in `flow`s to get descriptions when the expected and actual\nvalues need some explanation:\n\n```clojure\n(deftest fruits-and-veggies\n  (flow \"surprise! Tomatoes are fruits!\"\n    (match? #{:tomato} (fruits #{:tomato :potato}))))\n```\n\nOr with custom parameters:\n\n```clojure\n(defflow my-flow {:init aux.init! :runner (comp run* s/with-fn-validation)}\n  (match? 1 1))\n\n```\n\n```clojure\n(defflow my-flow {:init (constantly {:value 1\n                                     :map {:a 1 :b 2}})}\n  [value (flow/get-state :value)]\n  (match? 1 value)\n  (flow \"uses matcher-combinator embeds\"\n    (match? {:b 2} (flow/get-state :map)))\n```\n\n### `:times-to-try` and `:sleep-time`\n\nBy default, `match?` will evaluate `actual` only once. For tests with\nasynchrony/concurrency concerns, you can direct `match?` to try up to\n`:times-to-try` times, waiting `:sleep-time` between each try. It will\nkeep trying until it produces a value that matches the `expected`\nexpression, up to `:times-to-try`.\n\n``` clojure\n(defflow add-data\n  (flow \"try up to 5 times with 250 ms between each try (total 1000ms)\"\n    (produce-message-that-causes-database-update)\n    (match? expected-data-in-database\n            (fetch-data)\n            {:times-to-try 5\n             :sleep-time 250})))\n```\n\n### NOTE: about upgrading to state-flow-2.2.4\n\nWe introduced `state-flow.api/match?` in state-flow-2.2.4, and\ndeprecated `state-flow.cljtest/match?` in that release. The signature\nfor the old version was `(match? \u003cdescription\u003e \u003cactual\u003e \u003cexpected\u003e)`.\nWe removed the description because it was quite common for the description\nto add no context that wasn't already made clear by the expected and\nactual values.\n\nWe also reversed the order of expected and actual in order to align\nwith the `match?` function in the matcher-combinators library and with\nclojure.test's `(is (= expected actual))`.\n\nWe also added a script to help refactor this for you. Here's how\nyou use it:\n\n``` shell\n# if you don't already have the state-flow repo cloned\ngit clone https://github.com/nubank/state-flow.git\n;; or\ngit clone git@github.com:nubank/state-flow.git\n;; then\ncd state-flow\n\n# if you already have the state-flow repo cloned\ncd state-flow\ngit co master\ngit pull\n\n# the rest is the same either way\nlein pom # needed for tools.deps to recognize this repo as a `:local/root` dependency\n./bin/refactor-match.sh --help\n;; now follow the instructions\n```\n\nNote that if you have a `defflow` defined in a different namespace, and it depends on `state-flow.api/defflow`, you may need to require it in that namespace.\n\n## Midje Support\n\nWe use `verify` to write midje tests with StateFlow. `verify` is a function that of three arguments: a description, a value or step, and another value or midje checker. It\nproduces a step that, when executed, verifies that the second argument matches the third argument. It replicates the functionality of a `fact` from midje.\nIn fact, if a simple value is passed as second argument, what it does is simply call `fact` internally when the flow is executed.\n\n`verify` returns a step that will make the check and return something. If the second argument is a value, it will return this argument. If the second argument is itself a step, it will return the last return value of the step that was passed. This makes it possible to use the result of verify on a later part of the flow execution if that is desired.\n\nSay we have a step for making a POST request that stores data in datomic (`store-data-request`),\nand we also have a step that fetches this data from db (`fetch-data`). We want to check that after we make the POST, the data is persisted:\n\n```clojure\n(:require\n  [state-flow.api :refer [flow]]\n  [state-flow.midje :refer [verify]])\n\n(defn stores-data-in-db\n  [data]\n  (flow \"save data\"\n    (store-data-request data)\n    [saved-data (fetch-data)]\n    (verify \"data is stored in db\"\n      saved-data\n      expected-data)))\n```\n\n## Writing Helpers\n\nTest helpers specific to your domain can make state-flow tests more\nreadable and intention-revealing. When writing them, we recommend that\nyou start with state-flow functions in the `state-flow.api` namespace.\nIf, for example, you're testing a webapp, you might want a `request`\nhelper like this:\n\n``` clojure\n(defflow users\n  (flow \"fetch registered users\"\n    (http-helpers/request {:method :post\n                           :uri \"/users\"\n                           :body {:user/first-name \"David\"}})\n    [users (http-helpers/request {:method :get\n                                  :uri \"/users\"})]\n    (match? [\"David\"]\n            (map :user/first-name users)))\n```\n\nPresuming that you have an `:http-component` key in the initial state,\nthe `http-helpers/request` helper could be implemented something like this:\n\n``` clojure\n(ns http-helpers\n  (:require [my-app.http :as http]\n            [state-flow.api :as flow :refer [flow]]))\n\n(defn request [req]\n  (flow \"make request\"\n    [http (flow/get-state :http-component)]\n    (flow/return (http/request http req)))\n```\n\nThis produces a step that can be used in a flow, as above.\n\n### funcool.cats\n\n`state-flow` is built on the `funcool.cats` library, which supports\nmonads in Clojure. `state-flow` exposes some, but not all, `cats`\nfunctions as its own API. As mentioned above, we recommend that you\nstick with `state-flow` functions as much as possible, however, if the\navailable functions do not suit your need for a helper, you can always\ndrop down to functions directly in the `cats` library.\n\n## Tooling\n\n### Emacs + cider\n\nAdd `\"defflow\"` to the list defined by `cider-test-defining-forms` to\nenable commands like `cider-test-run-test` for flows defined with `defflow`.\n\nSee [https://docs.cider.mx/cider/testing/running_tests.html#_configuration](https://docs.cider.mx/cider/testing/running_tests.html#_configuration)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnubank%2Fstate-flow","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnubank%2Fstate-flow","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnubank%2Fstate-flow/lists"}