Тест рефакторинга TDD для поддержки MultiThreading - PullRequest
7 голосов
/ 11 декабря 2008

Итак, я новичок в TDD, и я успешно создал небольшой пример приложения, используя шаблон MVP. Основная проблема моего текущего решения заключается в том, что оно блокирует поток пользовательского интерфейса, поэтому я пытался настроить Presenter для использования SynchronizationContext.Current, но когда я запускаю свои тесты, SynchronizationContext.Current имеет значение null.

Ведущий перед тем

public class FtpPresenter : IFtpPresenter
{
    ...
    void _view_GetFilesClicked(object sender, EventArgs e)
    {
        _view.StatusMessage = Messages.Loading;

        try
        {
            var settings = new FtpAuthenticationSettings()
            {
                Site = _view.FtpSite,
                Username = _view.FtpUsername,
                Password = _view.FtpPassword
            };
            var files = _ftpService.GetFiles(settings);

            _view.FilesDataSource = files;
            _view.StatusMessage = Messages.Done;        
        }
        catch (Exception ex)
        {
            _view.StatusMessage = ex.Message;
        }
    }
    ...
}

Тест перед заправкой

[TestMethod]
public void Can_Get_Files()
{
    var view = new FakeFtpView();
    var presenter = new FtpPresenter(view, new FakeFtpService(), new FakeFileValidator());

    view.GetFiles();
    Assert.AreEqual(Messages.Done, view.StatusMessage);
}

Теперь, после того как я добавил в Presenter поток SynchronizationContext, я попытался установить AutoResetEvent в своем поддельном представлении для StatusMessage, но при запуске теста SynchronizationContext.Current имеет значение null. Я понимаю, что модель потоков, которую я использую в своем новом Presenter, не идеальна, но является ли это правильным методом для тестирования многопоточности? Почему мой SynchronizationContext.Current нулевой? Что мне делать вместо этого?

Ведущий после потоков

public class FtpPresenter : IFtpPresenter
{
    ...
    void _view_GetFilesClicked(object sender, EventArgs e)
    {
        _view.StatusMessage = Messages.Loading;

        try
        {
            var settings = new FtpAuthenticationSettings()
            {
                Site = _view.FtpSite,
                Username = _view.FtpUsername,
                Password = _view.FtpPassword
            };
            // Wrap the GetFiles in a ThreadStart
            var syncContext = SynchronizationContext.Current;
            new Thread(new ThreadStart(delegate
            {
                var files = _ftpService.GetFiles(settings);
                syncContext.Send(delegate
                {
                    _view.FilesDataSource = files;
                    _view.StatusMessage = Messages.Done;
                }, null);
            })).Start();
        }
        catch (Exception ex)
        {
            _view.StatusMessage = ex.Message;
        }
    }
    ...
}

Испытание после нарезания резьбы

[TestMethod]
public void Can_Get_Files()
{
    var view = new FakeFtpView();
    var presenter = new FtpPresenter(view, new FakeFtpService(), new FakeFileValidator());

    view.GetFiles();
    view.GetFilesWait.WaitOne();
    Assert.AreEqual(Messages.Done, view.StatusMessage);
}

Ложный вид

public class FakeFtpView : IFtpView
{
    ...
    public AutoResetEvent GetFilesWait = new AutoResetEvent(false);
    public event EventHandler GetFilesClicked = delegate { };
    public void GetFiles()
    {
        GetFilesClicked(this, EventArgs.Empty);
    }
    ...
    private List<string> _statusHistory = new List<string>();
    public List<string> StatusMessageHistory
    {
        get { return _statusHistory; }
    }
    public string StatusMessage
    {
        get
        {
            return _statusHistory.LastOrDefault();
        }
        set
        {
            _statusHistory.Add(value);
            if (value != Messages.Loading)
                GetFilesWait.Set();
        }
    }
    ...
}

Ответы [ 2 ]

3 голосов
/ 11 декабря 2008

Я столкнулся с похожими проблемами с ASP.NET MVC, где отсутствует HttpContext. Одна вещь, которую вы можете сделать, это предоставить альтернативный конструктор, который позволит вам вставить фиктивный SynchronizationContext или выставить открытый сеттер, который делает то же самое. Если вы не можете изменить SynchronizationContext внутри, то создайте свойство, которое вы установили для SynchronizationContext.Current в конструкторе по умолчанию, и используйте это свойство в своем коде. В вашем альтернативном конструкторе вы можете назначить фиктивный контекст свойству - или вы можете назначить его напрямую, если вы предоставите ему открытый установщик.

открытый класс FtpPresenter: IFtpPresenter { public SynchronizationContext CurrentContext {get; задавать; }

   public FtpPresenter() : this(null) { }

   public FtpPresenter( SynchronizationContext context )
   {
       this.CurrentContext = context ?? SynchronizationContext.Current;
   }

   void _view_GetFilesClicked(object sender, EventArgs e)
   {
     ....
     new Thread(new ThreadStart(delegate
        {
            var files = _ftpService.GetFiles(settings);
            this.CurrentContext.Send(delegate
            {
                _view.FilesDataSource = files;
                _view.StatusMessage = Messages.Done;
            }, null);
        })).Start();

    ...
   }

Еще одно замечание, которое я хотел бы сделать, заключается в том, что ваш докладчик, вероятно, будет зависеть от интерфейса с классом Thread, а не от Thread напрямую. Я не думаю, что ваши модульные тесты должны создавать новые потоки, а скорее взаимодействовать с фиктивным классом, который просто обеспечивает вызов соответствующих методов для создания потоков. Вы также можете ввести эту зависимость.

Если SynchronizationContext.Current не существует при вызове конструктора, вам может потребоваться переместить логику присваивания в Current в геттер и выполнить ленивую загрузку.

1 голос
/ 18 декабря 2008

В вашем докладчике слишком много логики приложений. Я бы скрывал контексты и потоки внутри конкретной модели и тестировал только функциональность.

...