Skip to content

Commit a16dda7

Browse files
authored
refactor: formalise when and then structure of test actions and assertions (#171)
* An attempt at creating a when wrapper for assertions * Remove unnecessary mutability modifier for session assertions * Move when_time_elapses to the new structure for actions * Move all mock_counterparty actions to When implementation * Create Then wrapper struct for session ref assertions * Move all mock counterparty assertions to Then struct implementation
1 parent 69367db commit a16dda7

12 files changed

Lines changed: 237 additions & 169 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use crate::common::mock_counterparty::MockCounterparty;
2+
use crate::common::test_messages::TestMessage;
3+
use hotfix::message::FixMessage;
4+
use hotfix::session::SessionRef;
5+
use std::time::Duration;
6+
7+
pub struct When<T> {
8+
pub target: T,
9+
}
10+
11+
pub fn when<T>(target: T) -> When<T> {
12+
When { target }
13+
}
14+
15+
impl When<&SessionRef<TestMessage>> {
16+
pub async fn requests_disconnect(self) {
17+
self.target
18+
.disconnect("Test Session Finished".to_string())
19+
.await;
20+
}
21+
22+
pub async fn sends_message(self, message: TestMessage) {
23+
self.target.send_message(message).await;
24+
}
25+
}
26+
27+
impl When<&mut MockCounterparty<TestMessage>> {
28+
pub async fn has_previously_sent(&mut self, message: impl FixMessage) {
29+
self.target.push_previously_sent_message(message).await;
30+
}
31+
32+
pub async fn resends_message(&mut self, sequence_number: u64) {
33+
self.target.resend_message(sequence_number).await;
34+
}
35+
36+
pub async fn sends_message(&mut self, message: impl FixMessage) {
37+
self.target.send_message(message).await;
38+
}
39+
40+
pub async fn sends_gap_fill(&mut self, start_seq_no: u64, new_seq_no: u64) {
41+
self.target.send_gap_fill(start_seq_no, new_seq_no).await;
42+
}
43+
44+
pub async fn sends_logon(&mut self) {
45+
self.target.send_logon().await;
46+
}
47+
}
48+
49+
impl When<Duration> {
50+
pub async fn elapses(self) {
51+
tokio::time::advance(self.target).await;
52+
}
53+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use crate::common::mock_counterparty::MockCounterparty;
2+
use crate::common::test_messages::TestMessage;
3+
use hotfix::session::{SessionRef, Status};
4+
use hotfix_message::message::Message;
5+
use std::time::Duration;
6+
7+
pub const DEFAULT_TIMEOUT: Duration = Duration::from_millis(500);
8+
9+
pub struct Then<T> {
10+
target: T,
11+
}
12+
13+
pub fn then<T>(target: T) -> Then<T> {
14+
Then { target }
15+
}
16+
17+
impl Then<&SessionRef<TestMessage>> {
18+
pub async fn status_changes_to(self, expected_status: Status) {
19+
self.status_changes_within_time(expected_status, DEFAULT_TIMEOUT)
20+
.await;
21+
}
22+
23+
pub async fn status_changes_within_time(self, expected_status: Status, timeout: Duration) {
24+
let deadline = tokio::time::Instant::now() + timeout;
25+
let retry_interval = Duration::from_millis(1);
26+
27+
let mut session_info = self.target.get_session_info().await;
28+
while tokio::time::Instant::now() < deadline {
29+
if session_info.status == expected_status {
30+
return;
31+
}
32+
tokio::time::sleep(retry_interval).await;
33+
session_info = self.target.get_session_info().await;
34+
}
35+
36+
let actual_status = session_info.status;
37+
panic!(
38+
"session did not reach expected status within timeout. Expected: {expected_status:?}, Actual: {actual_status:?}"
39+
);
40+
}
41+
}
42+
43+
impl Then<&mut MockCounterparty<TestMessage>> {
44+
pub async fn receives<F>(self, assertion: F)
45+
where
46+
F: FnOnce(&Message),
47+
{
48+
self.target
49+
.assert_next_with_timeout(assertion, DEFAULT_TIMEOUT)
50+
.await;
51+
}
52+
53+
pub async fn gets_disconnected(self) {
54+
self.target
55+
.assert_disconnected_with_timeout(DEFAULT_TIMEOUT)
56+
.await;
57+
}
58+
}

crates/hotfix/tests/common/mock_counterparty.rs

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use crate::common::session_assertions::DEFAULT_TIMEOUT;
21
use hotfix::config::SessionConfig;
32
use hotfix::message::logon::{Logon, ResetSeqNumConfig};
43
use hotfix::message::sequence_reset::SequenceReset;
@@ -50,7 +49,7 @@ where
5049
}
5150
}
5251

53-
pub async fn when_previously_sent(&mut self, message: impl FixMessage) {
52+
pub async fn push_previously_sent_message(&mut self, message: impl FixMessage) {
5453
let raw_message = generate_message(
5554
&self.session_config.sender_comp_id,
5655
&self.session_config.target_comp_id,
@@ -61,14 +60,14 @@ where
6160
self.sent_messages.push(raw_message);
6261
}
6362

64-
pub async fn when_message_is_resent(&mut self, sequence_number: u64) {
63+
pub async fn resend_message(&mut self, sequence_number: u64) {
6564
let message = self.sent_messages[sequence_number as usize - 1].clone();
6665
self.session_ref
6766
.new_fix_message_received(RawFixMessage::new(message))
6867
.await;
6968
}
7069

71-
pub async fn when_gap_fill_is_sent(&mut self, start_seq_no: u64, new_seq_no: u64) {
70+
pub async fn send_gap_fill(&mut self, start_seq_no: u64, new_seq_no: u64) {
7271
let sequence_reset = SequenceReset {
7372
gap_fill: true,
7473
new_seq_no,
@@ -85,15 +84,15 @@ where
8584
.await;
8685
}
8786

88-
pub async fn when_logon_is_sent(&mut self) {
87+
pub async fn send_logon(&mut self) {
8988
let logon = Logon::new(
9089
self.session_config.heartbeat_interval,
9190
ResetSeqNumConfig::NoReset(None),
9291
);
93-
self.when_message_is_sent(logon).await;
92+
self.send_message(logon).await;
9493
}
9594

96-
pub async fn when_message_is_sent(&mut self, message: impl FixMessage) {
95+
pub async fn send_message(&mut self, message: impl FixMessage) {
9796
let raw_message = generate_message(
9897
&self.session_config.sender_comp_id,
9998
&self.session_config.target_comp_id,
@@ -138,15 +137,7 @@ where
138137
}
139138
}
140139

141-
pub async fn then_receives<F>(&mut self, assertion: F)
142-
where
143-
F: FnOnce(&Message),
144-
{
145-
self.assert_next_with_timeout(assertion, DEFAULT_TIMEOUT)
146-
.await;
147-
}
148-
149-
async fn assert_next_with_timeout<F>(&mut self, assertion: F, timeout: Duration)
140+
pub(crate) async fn assert_next_with_timeout<F>(&mut self, assertion: F, timeout: Duration)
150141
where
151142
F: FnOnce(&Message),
152143
{
@@ -163,11 +154,7 @@ where
163154
}
164155
}
165156

166-
pub async fn then_gets_disconnected(&mut self) {
167-
self.assert_disconnected_with_timeout(DEFAULT_TIMEOUT).await;
168-
}
169-
170-
async fn assert_disconnected_with_timeout(&mut self, timeout: Duration) {
157+
pub async fn assert_disconnected_with_timeout(&mut self, timeout: Duration) {
171158
if tokio::time::timeout(timeout, async {
172159
// keep consuming messages until a disconnect occurs
173160
while self.get_next().await.is_some() {}

crates/hotfix/tests/common/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
pub mod actions;
2+
pub mod assertions;
13
pub mod mock_application;
24
pub mod mock_counterparty;
3-
pub mod session_actions;
4-
pub mod session_assertions;
55
pub mod setup;
66
pub mod test_messages;

crates/hotfix/tests/common/session_actions.rs

Lines changed: 0 additions & 22 deletions
This file was deleted.

crates/hotfix/tests/common/session_assertions.rs

Lines changed: 0 additions & 36 deletions
This file was deleted.

crates/hotfix/tests/common/setup.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
use crate::common::actions::when;
2+
use crate::common::assertions::then;
13
use crate::common::mock_application::MockApplication;
24
use crate::common::mock_counterparty::MockCounterparty;
3-
use crate::common::session_assertions::SessionAssertions;
45
use crate::common::test_messages::TestMessage;
56
use hotfix::application::ApplicationRef;
67
use hotfix::config::SessionConfig;
@@ -35,11 +36,11 @@ pub async fn given_a_connected_session_with_store(
3536
pub async fn given_an_active_session() -> (SessionRef<TestMessage>, MockCounterparty<TestMessage>) {
3637
let (session, mut mock_counterparty) = given_a_connected_session().await;
3738

38-
mock_counterparty
39-
.then_receives(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "A"))
39+
then(&mut mock_counterparty)
40+
.receives(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "A"))
4041
.await;
41-
mock_counterparty.when_logon_is_sent().await;
42-
session.then_status_changes_to(Status::Active).await;
42+
when(&mut mock_counterparty).sends_logon().await;
43+
then(&session).status_changes_to(Status::Active).await;
4344

4445
(session, mock_counterparty)
4546
}
Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use crate::common::session_actions::SessionActions;
1+
use crate::common::actions::when;
2+
use crate::common::assertions::then;
23
use crate::common::setup::given_an_active_session;
34
use crate::common::test_messages::TestMessage;
45
use hotfix::message::FixMessage;
@@ -8,21 +9,21 @@ async fn test_new_order_single() {
89
let (session, mut mock_counterparty) = given_an_active_session().await;
910

1011
// we send a new order to the counterparty and they receive it successfully
11-
session
12-
.when_message_is_sent(TestMessage::dummy_new_order_single())
12+
when(&session)
13+
.sends_message(TestMessage::dummy_new_order_single())
1314
.await;
14-
mock_counterparty
15-
.then_receives(|msg| {
15+
then(&mut mock_counterparty)
16+
.receives(|msg| {
1617
let parsed = TestMessage::parse(msg);
1718
assert_eq!(parsed.message_type(), "D");
1819
})
1920
.await;
2021

21-
mock_counterparty
22-
.when_message_is_sent(TestMessage::dummy_execution_report())
22+
when(&mut mock_counterparty)
23+
.sends_message(TestMessage::dummy_execution_report())
2324
.await;
2425
// TODO: we currently have no good way of asserting this message was received
2526

26-
session.when_disconnect_is_requested().await;
27-
mock_counterparty.then_gets_disconnected().await;
27+
when(&session).requests_disconnect().await;
28+
then(&mut mock_counterparty).gets_disconnected().await;
2829
}

crates/hotfix/tests/session_test_cases/heartbeat_tests.rs

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use crate::common::session_actions::{SessionActions, when_time_elapses};
1+
use crate::common::actions::when;
2+
use crate::common::assertions::then;
23
use crate::common::setup::{HEARTBEAT_INTERVAL, given_an_active_session};
34
use hotfix_message::Part;
45
use hotfix_message::fix44::MSG_TYPE;
@@ -18,13 +19,15 @@ async fn test_heartbeats() {
1819
let (session, mut mock_counterparty) = given_an_active_session().await;
1920

2021
// let's wait enough time for a heartbeat and assert that the heartbeat was sent
21-
when_time_elapses(Duration::from_secs(HEARTBEAT_INTERVAL + 1)).await;
22-
mock_counterparty
23-
.then_receives(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "0"))
22+
when(Duration::from_secs(HEARTBEAT_INTERVAL + 1))
23+
.elapses()
24+
.await;
25+
then(&mut mock_counterparty)
26+
.receives(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "0"))
2427
.await;
2528

26-
session.when_disconnect_is_requested().await;
27-
mock_counterparty.then_gets_disconnected().await;
29+
when(&session).requests_disconnect().await;
30+
then(&mut mock_counterparty).gets_disconnected().await;
2831
}
2932

3033
/// Tests the peer timeout and disconnection mechanism:
@@ -42,19 +45,23 @@ async fn test_peer_timeout() {
4245
let (_session, mut mock_counterparty) = given_an_active_session().await;
4346

4447
// let's wait enough time for a heartbeat and assert that the heartbeat was sent
45-
when_time_elapses(Duration::from_secs(HEARTBEAT_INTERVAL + 1)).await;
46-
mock_counterparty
47-
.then_receives(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "0"))
48+
when(Duration::from_secs(HEARTBEAT_INTERVAL + 1))
49+
.elapses()
50+
.await;
51+
then(&mut mock_counterparty)
52+
.receives(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "0"))
4853
.await;
4954

5055
// we wait enough time for the peer deadline to pass
51-
when_time_elapses(Duration::from_secs(peer_interval - HEARTBEAT_INTERVAL)).await;
56+
when(Duration::from_secs(peer_interval - HEARTBEAT_INTERVAL))
57+
.elapses()
58+
.await;
5259
// a TestRequest (type '1') is sent to the counterparty
53-
mock_counterparty
54-
.then_receives(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "1"))
60+
then(&mut mock_counterparty)
61+
.receives(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "1"))
5562
.await;
5663

5764
// we wait even longer and the counterparty never responds, so we disconnect from the counterparty
58-
when_time_elapses(Duration::from_secs(peer_interval)).await;
59-
mock_counterparty.then_gets_disconnected().await;
65+
when(Duration::from_secs(peer_interval)).elapses().await;
66+
then(&mut mock_counterparty).gets_disconnected().await;
6067
}

0 commit comments

Comments
 (0)