Тест рефакторинга TDD для поддержки MultiThreading

Так что я новичок в 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);
}

Теперь, после того как я добавил Threading SynchronizationContext в Presenter, я попытался установить 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();
        }
    }
    ...
}
11.12.2008 20:21:57
Хороший вопрос! Я пытаюсь решить подобную проблему!
Alex Kofman 10.10.2010 16:45:34
2 ОТВЕТА
РЕШЕНИЕ

Я столкнулся с аналогичными проблемами с 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 в геттер и выполнить ленивую загрузку.

3
11.12.2008 20:37:54
Что бы я использовал в своем тесте вместо SynchronizationContext.Current? У вас есть примеры кода?
bendewey 11.12.2008 20:35:35
Я не знаю, соответствует ли Synchronizationcontext интерфейсу. Если это так, вы можете создать макет класса, который использует тот же интерфейс, и вместо этого внедрить интерфейс. Если нет, вы можете определить оболочку для SynchronizationContext, которая реализует (ваш собственный) интерфейс и макетирует класс оболочки.
tvanfosson 11.12.2008 20:39:43
Посмотрите на источник для HttpContextWrapper (с которым я знаком) на www.codeplex.com/aspnet в дереве исходного кода MVC, чтобы узнать, как это сделать.
tvanfosson 11.12.2008 20:40:30
В продолжение мне нравится идея создания общедоступного SynchronizationContext SyncContext {get; набор; }
bendewey 11.12.2008 20:41:25
Еще лучше я добавил [TestInitialize ()] public void Initialize () {SynchronizationContext.SetSynchronizationContext (new SynchronizationContext ()); }
bendewey 11.12.2008 21:08:08

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

1
18.12.2008 19:04:52