DioxusLabs / dioxus

Fullstack app framework for web, desktop, mobile, and more.
https://dioxuslabs.com
Apache License 2.0
20.32k stars 779 forks source link

request for a best practice of writing tests for the hooks #955

Closed OneeMe closed 1 year ago

OneeMe commented 1 year ago

Specific Demand

I want to write tests for the hooks which holds all my business logic of my app, but I can't find any suggestions for this.

Implement Suggestion

In react, we can use react-hooks-test-library to write tests for the hooks. The library provides some necessary methods to simulate the render process and component context.

In current Dioxus implementation, the ScopeState is not a public API so we can't write a test version cx for the custom hooks, Is it possible to write a common test version ScopeState to supports writing test for the hooks

ealmloff commented 1 year ago

The Virtual Dom and Scope are very closely coupled, so we cannot easily isolate them. The virtual DOM does expose the tools necessary to test hooks.

Here is an example of a test for a hook:

[package]
name = "dioxus-test"
version = "0.1.0"
edition = "2021"

[dependencies]
dioxus = "*"
futures = "0.3.28"

[features]
use futures::FutureExt;
use std::{cell::RefCell, sync::Arc};

use dioxus::prelude::*;

fn main() {
    test(
        |cx| use_ref(cx, || 0).clone(),
        |value, mut proxy| match proxy.generation {
            0 => {
                value.set(1);
            }
            1 => {
                assert_eq!(*value.read(), 1);
                value.set(2);
            }
            2 => {
                proxy.rerun();
            }
            3 => {}
            _ => todo!(),
        },
        |proxy| assert_eq!(proxy.generation, 4),
    );
}

fn test<V: 'static>(
    initialize: impl FnMut(&ScopeState) -> V + 'static,
    check: impl FnMut(V, MockProxy) + 'static,
    mut final_check: impl FnMut(MockProxy) + 'static,
) {
    #[derive(Props)]
    struct MockAppComponent<
        I: FnMut(&ScopeState) -> V + 'static,
        C: FnMut(V, MockProxy) + 'static,
        V,
    > {
        hook: RefCell<I>,
        check: RefCell<C>,
    }

    impl<I: FnMut(&ScopeState) -> V, C: FnMut(V, MockProxy), V> PartialEq
        for MockAppComponent<I, C, V>
    {
        fn eq(&self, _: &Self) -> bool {
            true
        }
    }

    fn mock_app<I: FnMut(&ScopeState) -> V, C: FnMut(V, MockProxy), V>(
        cx: Scope<MockAppComponent<I, C, V>>,
    ) -> Element {
        let value = cx.props.hook.borrow_mut()(cx);

        cx.props.check.borrow_mut()(value, MockProxy::new(cx));

        render! {
            div {}
        }
    }

    let mut vdom = VirtualDom::new_with_props(
        mock_app,
        MockAppComponent {
            hook: RefCell::new(initialize),
            check: RefCell::new(check),
        },
    );

    let _ = vdom.rebuild();

    while vdom.wait_for_work().now_or_never().is_some() {
        let _ = vdom.render_immediate();
    }

    final_check(MockProxy::new(vdom.base_scope()));
}

struct MockProxy {
    rerender: Arc<dyn Fn()>,
    pub generation: usize,
}

impl MockProxy {
    fn new(scope: &ScopeState) -> Self {
        let generation = scope.generation();
        let rerender = scope.schedule_update();

        Self {
            rerender,
            generation,
        }
    }

    pub fn rerun(&mut self) {
        (self.rerender)();
    }
}