Closed liamwh closed 9 months ago
I assume the issue that you're having is around the Gherkin And
steps which conflict with the rigidity of the test framework that enforces:
list of events + command ==> list of events
I think this enforcement is ideal in most cases (i.e., unit tests) as it encourages the developer to think in CQRS terms as well as helps debugging state issues. The question then is the best way to keep this rigidity for most cases while providing some flexibility when Cucumber is desired.
It might be overkill but should the solution also be extensible for other test methodologies (e.g., contract testing)?
After thinking about this issue with a fresh mind, I realised I should be able to simply create a vec of Commands / Events / Errors in the World struct cucumber provides, and then just set up and execute the test framework in the "then" part of the BDD test. I validated this theory and the below code is working. Please excuse the scrappy code, I just slapped it together quickly as a POC:
#[derive(Debug, Default, World, Clone)]
pub struct BankAccountWorld {
given_events: Vec<BankAccountEvent>,
when_comand: Option<BankAccountCommand>,
then_events: Vec<BankAccountEvent>,
then_error: Option<BankAccountError>,
}
fn get_test_framework() -> AccountTestFramework {
let services = BankAccountServices::new(Box::<MockBankAccountServices>::default());
AccountTestFramework::with(services)
}
#[given(regex = r"^I want to open a bank account with ID (\d+)$")]
async fn given_i_want_to_open_a_bank_account_with_id(
world: &mut BankAccountWorld,
account_id: String,
) {
world.when_comand = Some(BankAccountCommand::OpenAccount(
BankAccountOpenAccountCommandData {
account_id: account_id.clone(),
},
));
dbg!(&world.when_comand);
}
#[then(regex = r"^I should have a bank account with ID (\d+)$")]
async fn i_should_have_a_bank_account_with_id(world: &mut BankAccountWorld, account_id: String) {
world.then_events.push(BankAccountEvent::AccountOpened {
account_id: account_id.clone(),
});
let world = world.clone();
tokio::task::spawn_blocking(move || {
get_test_framework()
.given(world.given_events.clone())
.when(
world
.when_comand
.clone()
.expect("Expected a command to bet set"),
)
.then_expect_events(world.then_events.clone());
});
}
I really like your thinking with the rigidity/enforcement using the framework being ideal Dave. So as I no longer have the need for any additional action from the framework's part, I'm happy to consider this closed, but I'll leave the issue open and up to you if you decide to do anything with it 👍
I've been playing around with it a fair amount, and I've gone back on my previous statement. I'm being forced to apply various workarounds, which would simply be much simpler if I wasn't forced to immediately chain .when and .then methods after the .given method.
@liamwh any ideas on additions that might help?
Adding an and
implementation to the given
could help a bit I think, thoughts?
I certainly agree that adding an and
implementation would be a more ergonomic API and therefore fully support that addition. 👍
However, I don't feel that solves the issues using the testing framework inside the cucumber tests, because of spawning another runtime:
Ideally, I'd love to be able to use the framework inside the cucumber tests as economically as possible, something like the following:
I've added an and
to get started and investigating that runtime.
Can you push your experiment for me to take a look at (if this is in Veloxide maybe a branch)?
Hi, apologies for the delay, I must've overlooked any notification and am only back here because I checked it manually. Regardless, here's a link to the branch that I was playing around on as requested. Thanks for looking into this! https://github.com/liamwh/Veloxide/tree/include-bank-account-bdd-tests
The solutions that I've tried feel both brittle and a little incorrect. Instead I've opted to add a method when_async
that should allow this to be used under an async
test. I'll continue to investigate this and combine them in the next major release if I can find a clean way to do.
An async test should now look something like:
#[tokio::test]
async fn test() {
let executor = TestFramework::<MyAggregate>::with(MyService)
.given_no_previous_events();
let validator = executor.when_async(MyCommands::DoSomething).await;
...
}
@liamwh is this solution working for you?
Hi serverlesstechnology,
Once again, I'm loving the framework and intend to promote it heavily very soon!
One major thing I need to achieve before doing so, is leveraging the TestFramework inside cucumber tests. Being able to use .feature files to setup BDD test scenarios is important to effectively communicate the tests with stakeholders who cannot read code.
For example, I'd love to be able to do something like this:
However, the methods on the test framework build a AggregateTestExecuter, and consume self, instead of borrowing, so any means of storing state in the World struct becomes a bit of a hacky work around.
Could we perhaps implement a cleaner solution so that the given, when and then methods do not have to be chained and state can be stored between calls? If you have a preference for direction, I'd be happy to contribute if you prefer not to write the code yourself 👍