Какие шаблоны существуют для имитации одной функции во время тестирования?

У меня есть функция, генерирующая соленый хеш-дайджест для некоторых данных. Для соли используется случайное значение u32. Это выглядит примерно так:

use rand::RngCore;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;

fn hash(msg: &str) -> String {
    let salt = rand::thread_rng().next_u32();

    let mut s = DefaultHasher::new();
    s.write_u32(salt);
    s.write(msg.as_bytes());
    format!("{:x}{:x}", &salt, s.finish())
}

В тесте я хотел бы проверить, что он выдает ожидаемые значения с учетом известной соли и строки. Как мне имитировать (swizzle?) rand::thread_rng().next_u32() в тесте, чтобы генерировать конкретное значение? Другими словами, чем можно заменить комментарий в этом примере, чтобы тест прошел?

mod tests {
    #[test]
    fn test_hashes() {
        // XXX How to mock ThreadRng::next_u32() to return 3892864592?
        assert_eq!(hash("foo"), "e80866501cdda8af09a0a656");
    }
}

Некоторые подходы, на которые я смотрел:

  • Мне известно, что ThreadRng возвращает rand::thread_rng() реализует RngCore, поэтому теоретически я мог бы где-нибудь установить переменную для хранения ссылки на RngCore и реализовать свой собственный издевательский вариант для установки во время тестирования. Я использовал такой подход в Go и Java, но не смог заставить средство проверки типов Rust разрешить его.

  • Я просмотрел список фиктивных фреймворков, таких как MockAll, но оказалось, что они предназначены для имитации struct или типаж для передачи методу, а этот код не передает его, и я бы не хотел, чтобы пользователи библиотеки могли передавать RngCore.

  • Используйте макрос #[cfg(test)] для вызова другой функции, указанной в модуле тестов, а затем заставьте эту функцию считать значение, которое нужно вернуть из другого места. Это я начал работать, но мне пришлось использовать небезопасную изменяемую статическую переменную, чтобы установить значение для насмешливого метода, которое нужно найти, что кажется грубым. Есть ли способ лучше?

В качестве справки я опубликую ответ, используя метод #[cfg(test)] + небезопасная изменяемая статическая переменная, но надеюсь, что есть более простой способ сделать такие вещи.


person theory    schedule 06.01.2020    source источник
comment
Спасибо за ссылки на дубликаты, @Shepmaster. Я думаю, читая их, я в конечном итоге смогу понять, как это сделать в стиле Rust, я еще недостаточно свободно говорю, чтобы они были супер очевидными. Ваш раздел о методах в этом ответе выглядит ближе всего к тому, что я думаю, было бы лучше; Я поработаю над его приложением к моему примеру и размещу здесь ссылку на игровую площадку, как только буду доволен.   -  person theory    schedule 06.01.2020
comment
Звучит неплохо! Вас также может заинтересовать Как избежать волнового эффекта при изменении конкретной структуры на универсальную?, чтобы избежать внешних изменений API. просто чтобы включить внедрение зависимостей для тестирования.   -  person Shepmaster    schedule 06.01.2020


Ответы (1)


В тестовом модуле используйте lazy-static, чтобы добавить статическую переменную. с мьютексом для безопасности потоков, создайте функцию, подобную next_u32(), чтобы возвращать ее значение, и попросите тесты установить статическую переменную в известное значение. Он должен вернуться к возврату правильного случайного числа, если оно не установлено, поэтому здесь я сделал его Vec<u32>, чтобы он мог сказать:

mod tests {
    use super::*;
    use lazy_static::lazy_static;
    use std::sync::Mutex;

    lazy_static! {
        static ref MOCK_SALT: Mutex<Vec<u32>> = Mutex::new(vec![]);
    }

    // Replaces random salt generation when testing.
    pub fn mock_salt() -> u32 {
        let mut sd = MOCK_SALT.lock().unwrap();
        if sd.is_empty() {
            rand::thread_rng().next_u32()
        } else {
            let ret = sd[0];
            sd.clear();
            ret
        }
    }

    #[test]
    fn test_hashes() {
        MOCK_SALT.lock().unwrap().push(3892864592);
        assert_eq!(hash("foo"), "e80866501cdda8af09a0a656");
    }
}

Затем измените hash(), чтобы при тестировании вызывался tests::mock_salt() вместо rand::thread_rng().next_u32() (первые три строки тела функции новые):

fn hash(msg: &str) -> String {
    #[cfg(test)]
    let salt = tests::mock_salt();
    #[cfg(not(test))]
    let salt = rand::thread_rng().next_u32();

    let mut s = DefaultHasher::new();
    s.write_u32(salt);
    s.write(msg.as_bytes());
    format!("{:x}{:x}", &salt, s.finish())
}

Использование макросов позволяет Rust во время компиляции определять, какую функцию вызывать, поэтому эффективность нетестовых сборок не снижается. Это означает, что в исходном коде есть некоторая информация о модуле тестов, но он не включен в двоичный файл, поэтому должен быть относительно безопасным. Я полагаю, что может быть пользовательский макрос, чтобы как-то автоматизировать это. Что-то вроде:

    #[mock(rand::thread_rng().next_u32())]
    let salt = rand::thread_rng().next_u32();

Будет автоматически генерировать имитируемый метод в модуле тестов (или где-то еще?), вставлять его сюда и предоставлять функции для тестов, чтобы установить значение --- только при тестировании, конечно. Хотя вроде много.

Игровая площадка.

person theory    schedule 06.01.2020
comment
применен - person Shepmaster; 06.01.2020
comment
Должен признать, что я не слежу за применением изменяемого одиночного ответа здесь, хотя lazy-static выглядит как хорошая замена небезопасным вещам, которые у меня есть. Любопытно, однако, что беглые Rustaceans сделали бы из этого макро-подхода. - person theory; 06.01.2020
comment
Ваш static mut MOCK_SALT является глобальным изменяемым синглтоном. Использование lazy_static! и Mutex позволяет избежать небезопасности. На самом деле ваш код вероятно приводит к неопределенному поведению, особенно если учесть, что все тесты выполняются в отдельных потоках. Вот почему unsafe нельзя использовать легкомысленно. - person Shepmaster; 06.01.2020
comment
Обновлено для использования lazy_static! и Mutex для безопасности потоков. - person theory; 13.01.2020