在 Rust 单元测试工具中,如何等待回调函数被调用?

3
请考虑以下函数:
pub fn shiny_function(&mut self, cb: Arc<Mutex<dyn FnMut(usize) + Send>>) {
    // Do stuff here...
}

现在的问题是,我该如何编写一个单元测试来检查回调(闭包)参数是否等于某个值?

明显的解决方案看起来像这样:

#[test]
fn progress_cb() {
    let cut = ... // cut stands for Class Under Test
    cut.shiny_function(Arc::new(Mutex::new(move |percent| {
        // Assert here maybe? I don't know.
    })));

    cut.shiny_function();

    // Or maybe assert it somehow here? I don't know.
}

但问题是测试在回调被调用之前就已经完成了。我该如何告诉测试框架等待回调被调用?
1个回答

1
您可以使用标准库中提供的常规并发结构来解决此问题。在本例中,我使用屏障(barrier)确保在测试函数退出之前到达闭包(closure)的末尾。我创建了一个值为2的屏障,因为必须在两个线程上调用wait两次才能释放屏障。当多次调用shiny_function时,可能不希望出现这种行为,因此您还可以替换另一个仅在单个位置阻塞的并发结构。
use std::sync::{Arc, Barrier};

#[test]
fn progress_cb() {
    let cut = ... // cut stands for Class Under Test

    // Create a barrier for this thread and clone it to move into the closure
    let barrier = Arc::new(Barrier::new(2));
    let barrier_clone = barrier.clone();

    cut.shiny_function(Arc::new(Mutex::new(move |percent| {
        // Perform tests
        assert_eq!(percent, foo);

        // Once we finish we can trigger the barrier so the outer thread can continue
        barrier_clone.wait();
    })));

    // Don't exit the function until the barrier has been resolved in the callback
    barrier.wait();
}

编辑:以下是一个结构体,如果由于闭包在每次调用时阻塞并阻止单个测试函数中稍后对shiny_function的调用,您可以使用它来解决屏障成为问题的情况。

use std::sync::{Arc, Mutex, Condvar};

pub struct SingleBlockingBarrier {
    target: u32,
    counter: Mutex<u32>,
    lock: Condvar,
}

impl SingleBlockingBarrier {
    pub fn new(target: u32) -> Arc<Self> {
        Arc::new(SingleBlockingBarrier {
            target,
            counter: Mutex::new(0),
            lock: Condvar::new(),
        })
    }

    pub fn signal(&self) {
        let mut guard = self.counter.lock().unwrap();
        *guard += 1;
        if *guard >= self.target {
            self.lock.notify_all();
        }
    }

    // Block until signal has been called the target number of times
    pub fn block_until_finished(&self) {
        let mut guard = self.counter.lock().unwrap();
        
        loop {
            if *guard >= self.target {
                return;
            }

            guard = self.lock.wait(guard).unwrap();
        }
    }
}

#[test]
fn progress_cb() {
    let cut = ... // cut stands for Class Under Test

    // Create a barrier for this thread and clone it to move into the closure
    let barrier = SingleBlockingBarrier::new(10);

    for _ in 0..10 {
        let barrier_clone = barrier.clone();

        cut.shiny_function(Arc::new(Mutex::new(move |percent| {
            // Perform tests
            assert_eq!(percent, foo);
    
            // Notify barrier that a worker has finished without blocking
            barrier_clone.signal();
        })));
    }

    // Block until all non-blocking barriers have been reached
    barrier.block_until_finished();
}

1
非常感谢您详细而清晰的回答!它帮助我解决了困境! - Refael Sheinker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接