Skip to content

MAP Holons Test Strategy

Steve Melville edited this page Feb 11, 2025 · 7 revisions

MAP testing follows two general approaches:

  1. Sweetests leverage Rust's rstest testing framework.
  2. Tryorama tests leverages Holochain's Tryorama JavaScript-based testing framework.

Sweetest Integration Tests

Test Contexts

The HolonsContextBehavior trait provides access to a space manager.

pub trait HolonsContextBehavior {
    /// Provides access to the holon space manager for interacting with holons and their relationships.
    fn get_space_manager(&self) -> Rc<&dyn HolonSpaceBehavior>;
}

Concrete implementations of that trait provide access to a HolonSpaceManager that has been configured for either client-side or guest-side use. Note that HolonSpaceManager is a holons_core component, so the source code is the same for both client and guest space managers. They differ in the HolonService they have been injected with upon initialization of their context.

The HolonSpaceBehavior trait implemented by HolonSpaceManager provides access to a set of services.

pub trait HolonSpaceBehavior {
    fn get_cache_access(&self) -> Arc<dyn HolonCacheAccess>;
    fn get_holon_service(&self) -> Arc<dyn HolonServiceApi>;
    fn get_nursery_access(&self) -> Arc<RefCell<dyn NurseryAccess>>;
    fn get_space_holon(&self) -> Option<HolonReference>;
    fn get_staging_behavior_access(&self) -> Arc<RefCell<dyn HolonStagingBehavior>>;
    fn export_staged_holons(&self) -> SerializableHolonPool;
    fn import_staged_holons(&self, staged_holons: SerializableHolonPool);
    fn get_transient_state(&self) -> Rc<RefCell<dyn HolonCollectionApi>>;
}

There are three different contexts relevant to sweetests.

fixture_context (client-side)

  • Test fixtures are responsible for setting up the test steps for a given test case along with the test data (holons and relationships) those steps require. Notice that relationships are expressed via HolonReferences. The fixture_context allows access to the actually holon they being references by providing access to the services that resolve them: CacheAccess (for SmartReferences) or NurseryAccess (for StagedReferences).

  • An empty context is initialized by each fixture and goes out of scope when the fixture ends. Fixture contexts are never shipped between client and guest and their Nursery's are never committed.

test_context (client-side)

  • Each test case executes within its own client-side context. Test step executors (i.e., the rust functions that implement test steps), use the test_context to stage holons and their relationships (via the NurseryAccess service) and to get persisted holons and their relationships (via the CacheAccess service).

  • an empty context is initialized by the rstest_dance_test function at the beginning of each test case execution and a reference to it (as a &dyn HolonsContextBehavior trait object) is passed as a parameter to each test step executor. if a test step invokes a dance, its Nursery is shipped to the guest-side via the session_state field on DanceRequest.

  • when the dance result is returned the test_context is restored from session_state so that any changes to the Nursery that were made by the guest are now reflected back in the client's Nursery.

  • the commit_dance persists any staged holons in the Nursery. Once completed successfully, it clears the staged holons and keyed_index from the Nursery, making it available to stage additional holons and relationships.

guest_context (guest-side)

  • The guest_context is used by dance implementation functions that rely on the space manager and the services it provides.
  • When Dancer's dance function is invoked for a new DanceRequest, it initializes the guest_context from the session_state passed via the DanceRequest.
  • The dancer passes a reference to the context (as a &dyn HolonsContextBehavior trait object) to the DanceFunction it dispatches to handle the dance request.
  • DanceFunctions may perform operations that add, remove, update or clear the Nursery.
  • The dancer is responsible for including the updated Nursery via the session_state field in the DanceResponse.

rstest_dance_tests Function

Sweetests are organized around a set of test cases. Since the external API to the MAP guest is organized around dances, all integration testing is driven from the dances crate -- specifically, the rstest_dance_tests function defined in the dances_tests.rs module.

#[rstest]
#[case::simple_undescribed_create_holon_test(simple_create_holon_fixture())]
#[case::simple_add_related_holon_test(simple_add_remove_related_holons_fixture())]

#[tokio::test(flavor = "multi_thread")]
async fn rstest_dance_tests(#[case] input: Result<DancesTestCase, HolonError>)

Notice this function is parameterized by #[case]. Preceding the function declaration with one or more #[case] statements allows selective control over which test cases are invoked in any given test run. For example, the following code will result in the rstest_dance_tests function being invoked twice -- once for the simple_undescribed_create_holon_test case and once for the simple_add_related_holon_test case. Each test case is invoked asynchronously and independently from other test cases.

Each case has an associated fixture function that sets up the test steps and associated data for that test case and returns a DancesTestCase object that is passed as an input parameter to the rstest_dance_tests function. Every test case follows the same basic flow:

  1. Initialization -- sets up tracing, a mock conductor, an empty HolonsContext, and an empty test_state. Note that test_context is different from the fixture_context used by the fixture and the guest_context used by the back-end (guest-side) during dance execution (as described above).
  2. Test Step Execution. Unpack the test case and iterate through the steps for that test case. Matching on the test step allows different execute_xxx functions to be dispatched for each kind of test step.

The test_state accumulates state as the test case progresses and a mutable reference to it is passed into every test step executor.

// from dances/tests/shared_test/test_data_types.rs
pub struct DanceTestState {
    pub session_state: SessionState,
    pub created_holons: BTreeMap<MapString, Holon>,
}

The session_state field holds the state that is ping-ponged back and forth between client and guest. The created_holons map allows holons that have been successfully committed during the execution of this test case to be retrieved via their key in later test steps.

// from dances/src/session_state.rs
pub struct SessionState {
    staging_area: StagingArea,
    local_holon_space: Option<HolonReference>,
}

// from dances/src/staging_area.rs
pub struct StagingArea {
    staged_holons: Vec<Holon>,         // Contains all holons staged for commit
    index: BTreeMap<MapString, usize>, // Allows lookup by key to staged holons for which keys are defined
}

The session_state is included as part of the DanceRequest by the build_xxx_ function. It is part of every dance call and is restored from the DanceResponse when a response is received.

_test_state.session_state = response.state.clone();

Test Fixtures

Each case has an associated fixture function that sets up the test steps and associated data for that test case and returns a DancesTestCase object that is passed as an input parameter to the rstest_dance_tests function. The fixture can stage a set of holons and relationships in its fixture_context. Such holons can be supplied to the test_steps the fixture is setting up in order to supply the data required by that test_step.

Test Data Types

We have defined a set of data structures and protocols designed to make it easier to quickly define test cases.

DanceTestStep

Each test case is composed of a set of test steps. Test steps are defined independently so that may be reused in different test cases. The DanceTestStep enum defines the set of available test steps and the data associated with each test step. Here is an excerpt:

pub enum DanceTestStep {
  AbandonStagedChanges(StagedReference, ResponseStatusCode), // Marks a staged Holon as 'abandoned'
    AddRelatedHolons(
        StagedReference,
        RelationshipName,
        Vec<HolonReference>,
        ResponseStatusCode,
        Holon,
    ), // Adds relationship between two Holons
    Commit,      
  
}

Each test step generally invokes one or more dances.

DancesTestCase Each test case is defined by an instance of DancesTestCase: pub struct DancesTestCase { pub name: String, pub description: String, pub steps: VecDeque, }

Notice that the test case defines a sequential set of steps. DancesTestCase offers a set of add_xxx_step methods that allow test steps to be added to the test case, where xxx specifies a particular a specific test step. For example, the following method adds a stage_holon_step to test case. pub fn add_stage_holon_step(&mut self, holon: Holon) -> Result<(), HolonError> { self.steps.push_back(DanceTestStep::StageHolon(holon)); Ok(()) }

Tryorama Tests

Clone this wiki locally