Forky
Forky is a monorepo for kickass rust crates.
Crates in this repo are mature enough for usage but not yet at a stage to warrent individual repos.
Very early stage warning:
- breaking changes on patch versions
- continued development not guaranteed
- outdated docs
- bugs ahoy
Crates
Command Line Interface
Installation
cargo install forky_cli
forky --help
Welcome to the Forky CLI!
Usage: Forky CLI [COMMAND]
Commands:
auto-fs generate mod and css files
watch execute command on file change
serve serve static files
style Generate types for styles
mod generate mod files for your project
sweet build, serve & run tests in-browser
help Print this message or the help of the given subcommand(s)
watch
Analagous to cargo watch
but allows for watch globs.
usage:
forky watch
Usage: forky.exe watch [OPTIONS] <cmd>...
Arguments:
<cmd>... the space seperated command to run, ie forky watch -- echo howdy
Options:
-w, --watch <watch> paths to watch
-i, --ignore <ignore> paths to ignore
--once only run once instead of watching indefinitely
Mod
usage:
forky mod
I like to organize projects by many small files and including them becomes a headache so I use a cli to auto-generate mod files that include all files in a directory.
Its current incarnation is zero config and opinionated so you may want to play around with it on an empty project before integrating with existing codebases.
Sweet
Sweet is a full-stack test framework for Rust. Use a single framework for some or all of the supported test types:
Type | Description |
---|---|
Native - Unit | Results-not-panic workflow |
Native - E2E | Full support for webdriver, fantoccini etc |
In-browser - Component | Test indivudual web components, framework agnostic |
In-browser - E2E | Run e2e tests on actual elements via iframes |
The in-browser tests are architecturally similar to Cypress Component and e2e tests. The native tests may be be compared to the likes of Jest or Playwright.
Features
- 🔥 Parallel
- 🕙 Async
- 🕸️ Native & In-Browser
- 🌍 E2E Tests
- ☮️ Intuitive matchers
- 🌈 Pretty output
Usage
#[sweet_test]
fn true_is_true() -> Result<()> {
expect(true).to_be_true()
}
Very Quick Start
git clone https://github.com/mrchantey/sweet-demo.git
cd sweet-demo
cargo run --example sweet
Getting Started
Check out the quickstart page or have a browse of the tests written for sweet
Overview
Sweet has four main components:
sweet!
defines a test suite#[sweet_test]
defines a testexpect()
returns a matchervisit()
returns an iframe (e2e)
Quickstart - Native
- edit
cargo.toml
[dev-dependencies] sweet = # current version here [[example]] name = "sweet" path = "test/sweet.rs"
- create file
test/sweet.rs
#![feature(imported_main)] pub use sweet::*; #[sweet_test] fn it_works() -> Result<()>{ assert!(true == true); expect(true).to_be_true()?; expect("foobar") .not() .to_start_with("bazz")?; Ok(()) }
- run
cargo run --example sweet
- optional - try changing the above matchers so the test fails ⚡
As an example here is the output of a runner with a few tests:
Native Runner
The native runner is an alternative to vanilla rust unit and integration tests. It creates a single binary for all of your tests which speeds up compile times, see this blog for more info.
Usage
The native runner has a few cli options, run with --help
to see them all.
cargo run --example sweet --help
Options
[match]
Space seperated path globs to run, iemy_test
or/e2e/
-w, --watch
Clears screen and does not return error, for use withcargo watch
etc-p, --parallel
run tests in parallel-s, --silent
don't log results
Async Tests
Sweet allows async tests but cannot tell whether all awaited futures are Send
.
This is solved by adding the non_send
attribute:
// many async functions are parallelizable
#[sweet_test]
async fn example_parrallelizable_test(){
tokio::time::sleep(Duration::from_millis(100)).await.unwrap();
}
// some must be run on the main thread
#[sweet_test(non_send)]
async fn example_non_send_tests(){
fantoccini::ClientBuilder::native().connect("http://example.com").await;
bevy::app::new().add_plugins(DefaultPlugins).run();
}
Web Driver
This section is for native end-to-end tests. For in-browser end-to-end tests see end-to-end.
Sweet can be used with fantoccini or any other webdriver client.
Example
Note the non_send
flag, as fantoccini futures are not Send
.
use fantoccini::ClientBuilder;
use fantoccini::Locator;
use sweet::*;
#[sweet_test(non_send)]
async fn connects_to_example()->Result<()>{
let client = ClientBuilder::native()
.connect("http://localhost:9515").await?;
client.goto("https://example.com").await?;
let url = client.current_url().await?;
expect(url.as_ref()).to_be("https://example.com")?;
client.close().await?;
Ok(())
}
Quickstart - Web
- Follow native quickstart
- Install the cli:
cargo install sweet
- More details on the cli page
- Run
sweet --example my_example
- Optional - update your test to do some web stuff:
web_sys::window().unwrap() .document().unwrap() .body().unwrap() .set_inner_html("<h1>This is a heading</h1>"); expect(window()).get("h1")? .to_contain_text("This is a heading")?;
Here's an example of a runner with a few tests:
Note: the below noisy warning can be stopped by enabling
chrome://flags/#privacy-sandbox-ads-apis
Error with Permissions-Policy header: Origin trial controlled feature not enabled: 'browsing-topics'.
Matchers
expect(element)
Querying a html element is so common Sweet has matchers for some common checks:
to_contain_html
to_contain_text
to_contain_visible_test
Into<HtmlElement>
window().unwrap().document().unwrap().body().unwrap()
is a bit of a mouthful 🥴
Sweet provides some wrappers around common types, ie Option<Window>
:
//window implements Into<HtmlElement> by getting its document body
expect(web_sys::window()).to_contain_text("sweet as!")?;
//so does iframe
let page = visit("localhost:7777").await;
expect(page).to_contain_text("sweet as!")?;
Async Matchers
Lots of web stuff happens at weird times, so we've got helpers like poll()
, which will wait for 2 seconds before failing.
expect(page).poll(|p|
p.to_contain_text("sweet as!")).await?;
We can also retrieve child elements via polling
expect(page).poll(|p| p.get("div")).await?
.to_contain_text("sweet as!")?;
End-To-End
This section is for in-browser end-to-end tests. For native end-to-end tests see webdriver.
By default web tests run inside the iframe. This is great for testing components, but when we want to test a page provided by the server we need a different approach.
Test cases marked as e2e
will run in the parent process instead. The child iframe
can be retrieved via visit()
, at which point you can interact with the underlying document just like with unit tests.
Testing iframes from different origins can be tricky, by default a web browser will say "hey, this isnt your site, i won't let you see whats inside iframe.contentDocument
etc".
To make this easier, sweet provides a reverse proxy that will serve your url from the same origin. Its been tested on simple sites like these docs, but if you encounter any problems please create an issue.
visit()
visit does three things:
- Points the proxy to the provided url
- Sets the iframe
src
to the proxy url- fyi this is will be something like
/_proxy_/http://localhost:3000
- fyi this is will be something like
- awaits the iframe
load
event
Example
Here's an example of an end-to-end test running on these docs:
sweet!{
test e2e "docs origin" {
let page = visit("http://localhost:3000").await?;
expect(page)
.poll(|p|p.to_contain_text("Forky")).await?;
}
}
And the output looks like this:
Matchers
Matchers are an ergonomic way to make assertions. Providing intellisense and type-specific assertions, they can make for an enjoyable testing experience.
The expect(val)
function returns a Matcher<T>
where T
is the type of the value passed in. What assertions are available for that matcher depend on the
expect(true).to_be_false()?;
Negation
All matchers can be negated by calling not()
expect("foobar").not().to_contain("bazz")?;
Built-in Matchers
Some examples of built-in matchers are:
- String
expect("foobar").to_start_with("foo")?;
- Result
expect(my_result).to_be_ok()?;
- Numbers (ord)
expect(2).to_be_greater_than(1)?;
Extending Matchers
Matchers are easy to extend, particulary using the extend
crate.
#![allow(unused)] fn main() { use anyhow::Result; use extend::ext; use sweet::*; #[derive(Debug)] struct Awesomeness(u32); #[ext] pub impl Matcher<Awesomeness> { fn to_be_more_awesome_than(&self, other:Awesomeness) -> Result<()> { let outcome = self.0 > other.0; let expected = format!("to be more awesome than {:?}", other); self.assert_correct(outcome, &expected) } } }
Note that here we are calling self.assert_correct()
which does to things:
- checks the outcome is true, or false in the case of negation:
expect(foo).not().to_be_more_awesome_than(bar)
- Formats a pretty backtraced output error if needed.
Sweet CLI
The Sweet CLI is a tool for building, serving & running tests in-browser. It can:
- Build the tests with
cargo build
&wasm bindgen
- Serve the tests on a dev server with live reload
- Run the tests using
chromedriver
Usage:
# headless
sweet --example my_test
# interactive
sweet --example my_test --interactive
# workspaces
sweet --example my_test -p my_crate
# help
sweet --help
Requirements
- wasm-bindgen-cli
cargo install -f wasm-bindgen-cli
- chromedriver
- Not required for interactive mode
-
# windows choco install chromedriver # mac brew install --cask chromedriver # linux sudo apt install chromium-chromedriver
- If your chrome version gets updated you will need to update chromedriver too:
choco upgrade chromedriver
etc
Help
sweet --help
Arguments:
[match]... filter suites by path glob, ie `my_test` or `/e2e/`
Options:
-p, --package <package> pass the --package flag to cargo run
--release pass the --release flag to cargo run
--secure run the dev server with https
--static <static> directory for static files (ie .css) used by component tests
-w, --watch live reload file changes
--headed run the tests with a visible browser window
-i, --interactive just start the server for viewing in your browser
Macros
#[sweet_test]
Tests can be declared via an attribute.
#[sweet_test]
fn foobar(){}
//accepts several flags, async functions or an `anyhow::Result` return type
#[sweet_test(skip,only,e2e,non_send)]
async fn foobar()->Result<()>{
expect(true).to_be_true()
}
sweet!
A layout more familiar to front-end developers. Note that rust formatters may not indent etc. the contents of this macro correctly.
sweet!{
it "has less boilerplate" {
expect(true).to_be_true()?;
}
test "is an alias for it"{}
it skip "wont run"{}
it only "will exclude non-onlys in this suite"{}
it e2e "(in-browser) runs in the parent process"{}
}
Workspaces
Workspaces are fully supported by Sweet, here's a few notes:
Duplicate Binaries
As per this PR crates that contain identical example names is not supported. For this reason it is recommended to update your Cargo.toml
.
[[example]]
- name = "sweet"
+ name = "test_crate_a"
path = "test/sweet.rs"
This will achieve two things:
- Avoid weird bugs where running
crate_a
actually runscrate_b
- Reduce unneseccary recompilation.
Running cargo run -p crate_a --example sweet_crate_a
is a bit of a mouthfull, I solve this with just:
#justfile
test crate *args:
cargo run -p {{crate}} --example sweet_{{crate}} -- {{args}}
Now you can run:
just test crate_a
CI / CD
Sweet has full CI/CD support for all test types. In fact, the tests for this repo are all run using Github Actions.
An example workflow may look something like this:
name: 🔎 Test Crates
on:
push:
branches: main
pull_request:
branches: main
env:
CARGO_TERM_COLOR: always
jobs:
build_and_test:
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: 📂 Checkout
uses: actions/checkout@v3
- name: 📂 Cache
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: 🔨 Install Chromedriver
uses: nanasess/setup-chromedriver@v2
- name: 🔨 Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
default: true
- name: 🔨 Install Wasm Target
run: rustup target add wasm32-unknown-unknown
- name: 🔨 Install Wasm Bindgen
uses: baptiste0928/cargo-install@v2
with:
crate: wasm-bindgen-cli
version: '0.2.87' # TODO ensure this matches your wasm-bindgen version
- name: 🔨 Install Sweet Cli
uses: baptiste0928/cargo-install@v2
with:
crate: sweet-cli
- name: 🔨 Build
run: cargo build
- name: 🔎 Test Native
run: cargo run --example sweet
- name: 🔎 Test Wasm
run: sweet --example sweet
FAQ
Why use [[example]]
instead of [[test]]
This makes it easier for the wasm test runner to produce cleaner output, but if you're only running native tests feel free to use [[test]]
with harness=false
.
What about wasm-bindgen-test?
Sweet has different priorities from wasm-bindgen-test in its current state, namely a focus on UI & interactivity.
Changelog
TODO
- Kill long-running tests
- Display typename in failed tests:
tests::foo::works
main
- rename
bevy_ecs
feature tobevy
- move time extensions to
bevy
feature
0.1.33
- 17/10/2023
- modularity & improved documentation
0.1.32
- 13/10/2023
- attribute macros
0.1.31
- 13/09/2023
- Headless in-browser tests
- ci/cd workflows
0.1.19
- 10/08/2023
- End-to-end testing
- Nicer async ergonomics:
matcher.poll().await?
0.1.18
- 03/08/2023
- Component Testing
- Native Testing
- Parallel
- Async
- Wasm Runner
Contributing
If this project is of interest to you feel free to have a dig around!
Everything is very early days so there are no doubt lots of bugs and missing features. If you find something that could be improved please open an issue or PR.
Getting Started
Most of this is for my own reference, but you may find it useful:
- Install Rust
- Installer
- Use powershell and be sure to carefully follow steps for build tools
- Install Depedencies
-
choco install just choco install cygwin # check just all check # tools cargo install cargo-watch cargo-edit rustup toolchain install nightly rustup component add rustfmt --toolchain nightly cargo +nightly fmt rustup default nightly # test - compilation will take several minutes just all test
-
Cygwin
Justfiles require cygwin to work on windows.
- install cygwin
- add to path:
C:\tools\cygwin\bin
Wasm
- follow the bevy guide
- setup
rustup target install wasm32-unknown-unknown cargo install wasm-server-runner cargo install -f wasm-bindgen-cli #.cargo/config.toml [target.wasm32-unknown-unknown] runner = "wasm-server-runner"
- run
#run cargo run -p forky_play --example maze --target wasm32-unknown-unknown #compile cargo build -p forky_play --example maze --release --target wasm32-unknown-unknown #build bindings wasm-bindgen --out-dir ./html/maze --target web ./target/wasm32-unknown-unknown/release/examples/maze.wasm cd html && live-server