| Crates.io | rust-actions |
| lib.rs | rust-actions |
| version | 0.2.1 |
| created_at | 2025-12-27 15:33:39.45204+00 |
| updated_at | 2025-12-27 18:18:44.263943+00 |
| description | BDD testing framework with GitHub Actions YAML syntax |
| homepage | https://github.com/alexchoi0/rust-actions |
| repository | https://github.com/alexchoi0/rust-actions |
| max_upload_size | |
| id | 2007412 |
| size | 138,048 |
A BDD testing framework for Rust using GitHub Actions YAML syntax instead of Gherkin.
#[derive(Args)] and #[derive(Outputs)]#[step("name")] attributeAdd to your Cargo.toml:
[dev-dependencies]
rust-actions = "0.1"
tokio = { version = "1", features = ["full", "test-util"] }
// tests/world.rs
use rust_actions::prelude::*;
#[derive(World)]
pub struct TestWorld {
pub users: Vec<User>,
pub rng: SeededRng,
}
impl TestWorld {
pub async fn setup() -> Result<Self> {
Ok(Self {
users: vec![],
rng: SeededRng::new(),
})
}
}
pub struct User {
pub id: String,
pub username: String,
pub email: String,
}
// tests/steps.rs
use rust_actions::prelude::*;
use crate::world::{TestWorld, User};
#[derive(Deserialize, Args)]
pub struct CreateUserArgs {
pub username: String,
pub email: String,
}
#[derive(Serialize, Outputs)]
pub struct UserOutput {
pub id: String,
pub username: String,
}
#[step("user/create")]
pub async fn create_user(world: &mut TestWorld, args: CreateUserArgs) -> Result<UserOutput> {
let id = world.rng.next_uuid().to_string();
world.users.push(User {
id: id.clone(),
username: args.username.clone(),
email: args.email,
});
Ok(UserOutput { id, username: args.username })
}
# tests/features/user.yaml
name: User Management
scenarios:
- name: Create a new user
steps:
- name: Create user Alice
id: alice
uses: user/create
with:
username: alice
email: alice@example.com
post-assert:
- ${{ outputs.id != "" }}
- ${{ outputs.username == "alice" }}
// tests/main.rs
use rust_actions::prelude::*;
mod steps;
mod world;
use world::TestWorld;
#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn run_features() {
RustActions::<TestWorld>::new()
.features("tests/features")
.run()
.await;
}
name: Feature Name
env:
DB_URL: postgres://localhost/test
containers:
postgres: postgres:15
redis: redis:7
scenarios:
- name: Scenario Name
steps:
- name: Step description
id: step_id # Optional: reference outputs later
uses: step/name # Required: step to execute
with: # Optional: step arguments
arg1: value1
arg2: ${{ steps.previous.outputs.field }}
continue-on-error: true # Optional: don't fail on error
pre-assert: # Optional: assertions before step
- ${{ env.DB_URL != "" }}
post-assert: # Optional: assertions after step
- ${{ outputs.id != "" }}
Access data using ${{ }} expressions:
# Environment variables
${{ env.DB_URL }}
# Previous step outputs
${{ steps.user.outputs.id }}
# Container info
${{ containers.postgres.url }}
${{ containers.postgres.host }}
${{ containers.postgres.port }}
# Current step outputs (in post-assert only)
${{ outputs.id }}
Inline assertions support comparison operators and object matching:
post-assert:
# Scalar comparisons
- ${{ outputs.id != "" }}
- ${{ outputs.count > 0 }}
- ${{ outputs.status == "active" }}
# Object partial matching
- '${{ outputs contains { "username": "alice" } }}'
# Array contains
- '${{ outputs.tags contains "admin" }}'
- '${{ outputs.users contains { "name": "bob" } }}'
# Full object equality
- '${{ outputs == { "id": "123", "name": "alice" } }}'
Supported operators:
==, !=, >, <, >=, <=contains#[step("my/step")]
async fn my_step(world: &mut TestWorld, args: MyArgs) -> Result<MyOutput> {
// Implementation
}
#[derive(Deserialize, Args)]
struct MyArgs {
required_field: String,
#[serde(default)]
optional_field: Option<String>,
}
#[derive(Serialize, Outputs)]
struct MyOutput {
id: String,
created_at: String,
}
#[step("simple/step")]
async fn simple_step(world: &mut TestWorld) -> Result<()> {
// No args, no outputs
Ok(())
}
rust-actions provides helpers for deterministic testing:
use rust_actions::prelude::*;
#[derive(World)]
pub struct TestWorld {
pub rng: SeededRng,
}
impl TestWorld {
pub async fn setup() -> Result<Self> {
Ok(Self {
rng: SeededRng::new(), // Seeded from scenario name
})
}
}
#[step("user/create")]
async fn create_user(world: &mut TestWorld, args: Args) -> Result<Output> {
let id = world.rng.next_uuid(); // Deterministic UUID
let token = world.rng.next_string(32); // Deterministic string
// ...
}
Uses tokio's test-util for time manipulation:
#[step("time/advance")]
async fn advance_time(_world: &mut TestWorld, args: TimeArgs) -> Result<()> {
tokio::time::advance(args.duration).await;
Ok(())
}
Feature: User Management
✓ Create a new user (5ms)
✓ Create user Alice
1 scenarios ✓ (1 passed)
1 steps (1 passed, 0 failed)
MIT