Как вставить заглушку для объекта-черты в тип в Rust и сохранить ссылку на него? - PullRequest
0 голосов
/ 12 июня 2019

Я пытаюсь разработать какой-нибудь инструмент CLI для отслеживания времени в Rust. Я пытаюсь покрыть большую часть кода модульными тестами и застрял в том, как использовать объекты-заглушки с ограничениями прав доступа Rusts.

У меня есть тип Tracker, который имеет функции для запуска / остановки времени отслеживания. Эти функции запрашивают текущее системное время.

Чтобы сделать этот дизайн тестируемым, я ввел черту TimeService, которая предоставляет текущую метку времени. У меня есть «реальная» реализация SystemTimeService, которая возвращает текущее системное время, и поддельная реализация FakeTimeService для тестов, в которых время может быть установлено извне.

Это текущая реализация:

pub struct Tracker {
    // ...
    time_service: Box<TimeService>,
}

trait TimeService {
    fn time(&self) -> u64;
}

Вот тест, который я хотел бы реализовать:

#[test]
fn tracker_end_when_called_tracks_total_time() {
    let (tracker, time_service) = init_tracker();

    time_service.set_seconds(0);
    tracker.start("feature1");
    time_service.set_seconds(10);
    tracker.end();

    assert_eq!(tracker.total_time, 10);
}

fn init_tracker() -> (Tracker, &FakeTimeService) {
    let time_service = Box::new(FakeTimeService::new());
    let tracker = Tracker::with_time_service(time_service);

    // does not compile, as time service's ownership has been
    // moved to the tracker.
    (tracker, &*time_service)
}

Проблема в том, что я не знаю, как получить доступ к сервису фальшивого времени из модульного теста, так как трекер владеет им.

Я могу представить несколько решений:

  1. Используйте Rc вместо Box внутри трекера для совместного владения службой времени
  2. Сделайте Tracker универсальным и добавьте аргумент типа для используемой TimeTracker реализации
  3. Добавление времени жизни к функции init_tracker

Мне не нравится использовать решение 1, так как это не выразило бы идею, что сервис является частью трекера (кажется, нарушает инкапсуляцию).

Решение 2, вероятно, жизнеспособно, но в этом случае мне нужно сделать признак TimeService и используемые реализации общедоступными, чего я также хотел бы избежать.

Таким образом, наиболее многообещающее решение без изменения дизайна - это время жизни. Можно ли добавить время жизни к переменным таким образом, чтобы функция init_tracker могла возвращать трекер и службу поддельного времени?

Есть ли другие решения / лучшие практики?

1 Ответ

2 голосов
/ 12 июня 2019

Вы можете сделать тип FakeTimeService клонируемым, сделав его состояние общим для нескольких клонированных экземпляров:

use std::rc::Rc;

#[derive(Clone)]
struct FakeTimeService {
    state: Rc<FakeTimeServiceState>,
}

impl TimeService for FakeTimeService { ... }

impl FakeTimeService {
    fn state(&self) -> &FakeTimeServiceState {
        *self.state
    }
}

fn init_tracker() -> (Tracker, FakeTimeService) {
    let time_service = FakeTimeService::new();
    let tracker = Tracker::with_time_service(Box::new(time_service.clone()));
    (tracker, time_service)
}

Теперь вы сможете независимо изменять состояние службы поддельного времени, сохраняя интерфейси реализация Tracker такая же, как и раньше.

Скорее всего, вы захотите иметь некоторое изменяемое состояние внутри FakeTimeService, чтобы настроить предварительные условия теста.Поэтому вам, вероятно, потребуется использовать некоторую внутреннюю изменчивость для состояния FakeTimeService:

use std::rc::Rc;
use std::cell::{RefCell, RefMut};

#[derive(Clone)]
struct FakeTimeService {
    state: Rc<RefCell<FakeTimeServiceState>>,
}

impl FakeTimeService {
    fn state_mut(&self) -> RefMut<FakeTimeServiceState> {
        self.state.borrow_mut()
    }
}

struct FakeTimeServiceState {
    init_value: i32,
}

impl FakeTimeServiceState {
    fn set_initial_value(&mut self, x: i32) {
        self.init_value = x;
    }
}

let (tracker, time_service) = init_tracker();
time_service.state_mut().set_initial_value(123);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...