---
title: Test code that resolves credentials
description: Use MemoryStore to test credential resolution without touching a real keychain, and avoid the env-var races that make such tests flaky.
tags:
- how-to
- testing
---
# Test code that resolves credentials
`MemoryStore` is exported from the crate root, not hidden behind a
`test` feature, so downstream test suites can use it directly.
## Inject a `MemoryStore` instead of the platform keychain
```rust
use rtb_credentials::{CredentialStore, MemoryStore, Resolver, SecretString};
use std::sync::Arc;
#[tokio::test]
async fn resolves_from_the_keychain_leg() {
let store = Arc::new(MemoryStore::new());
store
.set("mytool", "anthropic", SecretString::from("test-secret".to_string()))
.await
.unwrap();
let resolver = Resolver::new(store);
// ...
}
```
Write production code that takes `Arc<dyn CredentialStore>` rather than
calling `Resolver::with_platform_default()` internally. That one choice
is what makes the code testable — `with_platform_default` hard-wires
`KeyringStore` and cannot be substituted.
## Assert on the secret with `expose_secret`
```rust
use rtb_credentials::ExposeSecret;
let secret = resolver.resolve(&cref).await.unwrap();
assert_eq!(secret.expose_secret(), "test-secret");
```
`SecretString` does not implement `PartialEq`, so comparing two
`SecretString`s directly does not compile. Compare the exposed `&str`.
## Build a `CredentialRef` without deserialising config
All four fields are public, and `Default` supplies an empty reference:
```rust
use rtb_credentials::{CredentialRef, KeychainRef};
let cref = CredentialRef {
keychain: Some(KeychainRef {
service: "mytool".to_string(),
account: "anthropic".to_string(),
}),
..CredentialRef::default()
};
```
## Avoid environment variables in parallel tests
Rust runs tests in threads within one process, so `set_var` and
`remove_var` are shared mutable state across every test in the binary. A
test that sets `CI`, or a variable another test reads, will
intermittently corrupt its neighbours.
Two rules keep this manageable:
**Use a unique variable name per test.** The crate's own suite does
exactly this — `RTBCRED_T4_VAR`, `RTBCRED_T8_ENV` — so no two tests
contend for the same name.
**Prefer the keychain leg.** A `MemoryStore` is per-test state with no
global reach, so a test exercising resolution through the keychain leg
needs no environment mutation at all. Reach for env vars only when the
env legs are what you are testing.
`CI` is the one name that cannot be made unique, since the crate reads
that specific variable. A test that sets it must restore the previous
value:
```rust
let prior = std::env::var("CI").ok();
// ... set CI, run the assertion ...
match prior {
Some(v) => unsafe { std::env::set_var("CI", v) },
None => unsafe { std::env::remove_var("CI") },
}
```
Even restored, such a test races with any concurrent test that resolves
a literal. Consider keeping the CI-refusal test in its own test binary.
## Do not use `LiteralStore` as a test keychain
`LiteralStore::get` returns its one secret for **every** service and
account, so a test using it cannot detect a lookup with the wrong
identifiers — every key appears to exist.
`MemoryStore` returns `NotFound` for keys that were not set, which is
what makes a negative assertion meaningful.
## Assert on the failure paths
The behaviours most likely to surprise a user are the ones worth
pinning:
```rust
use rtb_credentials::CredentialError;
// nothing configured
let empty = CredentialRef::default();
assert!(matches!(
resolver.resolve(&empty).await,
Err(CredentialError::NotFound { .. })
));
```
A store returning a `Keychain` error rather than `NotFound` aborts the
whole chain, so a test that a fallback takes over should use a store
returning `NotFound` — which `MemoryStore` does for any key it was not
given.