effectful 0.2.1

Effect<A, E, R> (sync + async), context/layers, pipe — interpreter-style, no bundled executor
Documentation
# Mocking Services — Test Doubles via Layers

In effectful, a test double is just a different service value or layer. Production code gets a real service; tests get an in-memory or spy service with the same service type.

## The Pattern

Wrap your interface in a cloneable service struct.

```rust,ignore
trait DbImpl: Send + Sync {
    fn get_user(&self, id: UserId) -> Effect<User, DbError, ()>;
    fn save_user(&self, user: User) -> Effect<(), DbError, ()>;
}

#[derive(Clone, Service)]
struct Db {
    inner: Arc<dyn DbImpl>,
}

impl Db {
    fn get_user(&self, id: UserId) -> Effect<User, DbError, ()> {
        self.inner.get_user(id)
    }

    fn save_user(&self, user: User) -> Effect<(), DbError, ()> {
        self.inner.save_user(user)
    }
}
```

## Test Double

```rust,ignore
struct InMemoryDb {
    users: Mutex<HashMap<UserId, User>>,
}

impl DbImpl for InMemoryDb {
    fn get_user(&self, id: UserId) -> Effect<User, DbError, ()> {
        match self.users.lock().expect("users lock").get(&id).cloned() {
            Some(user) => succeed(user),
            None => fail(DbError::NotFound(id)),
        }
    }

    fn save_user(&self, user: User) -> Effect<(), DbError, ()> {
        self.users.lock().expect("users lock").insert(user.id, user);
        succeed(())
    }
}
```

## Injecting the Test Double

```rust,ignore
#[test]
fn get_user_returns_saved_user() {
    let db = Db { inner: Arc::new(InMemoryDb::new()) };
    let env = db.to_context();

    let effect = Db::use_(|db| {
        effect! {
            bind* db.save_user(User { id: UserId::new(1), name: "Alice".into() });
            bind* db.get_user(UserId::new(1))
        }
    });

    let exit = run_test(effect, env);
    assert!(matches!(exit, Exit::Success(user) if user.name == "Alice"));
}
```

Business logic is unchanged. Only the service value changes.

## Spies

When you need to assert calls, add tracking to the test double.

```rust,ignore
#[derive(Clone, Service)]
struct Mailer {
    sent: Arc<Mutex<Vec<Email>>>,
}

impl Mailer {
    fn send(&self, email: Email) -> Effect<(), MailError, ()> {
        self.sent.lock().expect("sent lock").push(email);
        succeed(())
    }
}

#[test]
fn registration_sends_welcome_email() {
    let mailer = Mailer { sent: Arc::new(Mutex::new(Vec::new())) };
    let env = mailer.clone().to_context();

    let exit = run_test(register_user("alice@example.com"), env);
    assert!(matches!(exit, Exit::Success(_)));

    let sent = mailer.sent.lock().expect("sent lock");
    assert_eq!(sent.len(), 1);
}
```

## Failing Services

Test failure handling by providing a service whose methods fail.

```rust,ignore
struct FailingDb;

impl DbImpl for FailingDb {
    fn get_user(&self, _id: UserId) -> Effect<User, DbError, ()> {
        fail(DbError::ConnectionLost)
    }

    fn save_user(&self, _user: User) -> Effect<(), DbError, ()> {
        fail(DbError::ConnectionLost)
    }
}

#[test]
fn get_user_propagates_db_errors() {
    let env = Db { inner: Arc::new(FailingDb) }.to_context();
    let exit = run_test(get_user(UserId::new(1)), env);
    assert!(matches!(exit, Exit::Failure(Cause::Fail(DbError::ConnectionLost))));
}
```

## Layer-Based Setup

For larger tests, package doubles in layers.

```rust,ignore
fn test_layer() -> Layer<(Db, Mailer), AppError, ()> {
    Layer::succeed(Db { inner: Arc::new(InMemoryDb::new()) })
        .merge(Layer::succeed(Mailer::spy()))
}

#[test]
fn full_registration_flow_works() {
    let exit = run_test(full_registration_flow().provide(test_layer()), ());
    assert!(matches!(exit, Exit::Success(_)));
}
```

## What You Don't Need

- No mock framework.
- No `#[cfg(test)]` in business logic.
- No global service registry reset between tests.
- No special mocking API beyond ordinary services and layers.