rust-actions

Crates.iorust-actions
lib.rsrust-actions
version0.2.1
created_at2025-12-27 15:33:39.45204+00
updated_at2025-12-27 18:18:44.263943+00
descriptionBDD testing framework with GitHub Actions YAML syntax
homepagehttps://github.com/alexchoi0/rust-actions
repositoryhttps://github.com/alexchoi0/rust-actions
max_upload_size
id2007412
size138,048
Alex Choi (alexchoi0)

documentation

https://docs.rs/rust-actions

README

rust-actions

A BDD testing framework for Rust using GitHub Actions YAML syntax instead of Gherkin.

Crates.io Documentation License: MIT

Features

  • GitHub Actions YAML syntax - Write tests in familiar YAML format
  • Typed step definitions - Full Rust type safety with #[derive(Args)] and #[derive(Outputs)]
  • Auto-registration - Steps are automatically discovered via #[step("name")] attribute
  • Inline assertions - Pre and post assertions with expression-based syntax
  • Deterministic testing - Seeded RNG for reproducible tests
  • Testcontainers support - Built-in Docker container management

Installation

Add to your Cargo.toml:

[dev-dependencies]
rust-actions = "0.1"
tokio = { version = "1", features = ["full", "test-util"] }

Quick Start

1. Define your World

// 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,
}

2. Define your Steps

// 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 })
}

3. Write your Feature File

# 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" }}

4. Run the Tests

// 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;
}

YAML Syntax

Basic Structure

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 != "" }}

Expression Syntax

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 }}

Assertions

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:

  • Comparison: ==, !=, >, <, >=, <=
  • Subset matching: contains

Step Definitions

Basic Step

#[step("my/step")]
async fn my_step(world: &mut TestWorld, args: MyArgs) -> Result<MyOutput> {
    // Implementation
}

Args and Outputs

#[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 without Args

#[step("simple/step")]
async fn simple_step(world: &mut TestWorld) -> Result<()> {
    // No args, no outputs
    Ok(())
}

Determinism

rust-actions provides helpers for deterministic testing:

SeededRng

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
    // ...
}

Time Control

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(())
}

Output

Feature: User Management
  ✓ Create a new user (5ms)
    ✓ Create user Alice

1 scenarios ✓ (1 passed)
1 steps (1 passed, 0 failed)

License

MIT

Commit count: 0

cargo fmt