2016-10-15 12 views
0

Итак, я пишу модульные тесты для одной из моих моделей просмотра в мобильном приложении Xamarin. Метод я тестирую выглядит так:Недетерминированные модульные тесты с Rx, Xamarin

public async Task RefreshItems() 
{ 
    var departamentsObservable = _dataService.GetDepartaments(); 

    departamentsObservable.ObserveOn(SynchronizationContext.Current).Subscribe(items => 
    { 
     Departaments.ReplaceWithRange(items); 
    }); 

    await departamentsObservable.FirstOrDefaultAsync(); 
} 

_dataService.GetDepartaments(); метод возвращает IObservable<IEnumerable<Departament>>.

Я использую Observable и Subscribe вместо простого метода, который возвращает Task<IEnumerable<Departament>>, потому что в моем случае Observable будет «вернуться» в два раза (один раз с данными из кэша и другого времени с недавно извлеченными данными формы сетью).

Для тестирования я конечно фиктивный _dataService.GetDepartaments(); метод так:

public IObservable<IEnumerable<Departament>> GetDepartaments() 
{ 
    return Observable.Return(MockData.Departaments); 
} 

Так метод немедленно возвращает макет данных.

И мой тест для RefreshItems метода выглядит следующим образом:

[Fact] 
public async Task RefreshItemsTest() 
{ 
    await _viewModel.RefreshItems(); 

    Assert.Equal(MockData.Departaments, _viewModel.Departaments, 
       new DepartamentComparer()); 
} 

Проблема заключается в том, что этот тест случайным образом выходит из строя (1 в 10 раз примерно). В принципе модель Departaments, которая должна обновляться, когда Observable «возвращает», пуста.

Я должен добавить, что я использую xUnit 2.1.0 тестовую среду и консольный консоль xUnit в Xamarin Studio.

EDIT: внушения Enigmativity проливает последовательность не содержит элементов только исключение при запуске в тестовом бегуна. Ниже приведен минимальный пример Уокинг код для демонстрации вопрос:

using System; 
using System.Collections.ObjectModel; 
using System.Threading.Tasks; 
using System.Reactive.Linq; 
using System.Threading; 
using System.Collections.Generic; 

namespace TestApp 
{ 
    public class TestViewModel 
    { 
     public ObservableCollection<TestDepartament> Departaments { get; set; } 

     private ITestDataService _dataService; 

     public TestViewModel(ITestDataService dataService) 
     { 
      _dataService = dataService; 
      Departaments = new ObservableCollection<TestDepartament>(); 
     } 

     public async Task RefreshItems() 
     { 
      var facultiesObservable = _dataService.GetDepartaments(); 

      await facultiesObservable.ObserveOn(SynchronizationContext.Current).Do(items => 
      { 
       Departaments.Clear(); 
       foreach(var item in items) 
        Departaments.Add(item); 
      }); 
     } 
    } 

    public interface ITestDataService 
    { 
     IObservable<IEnumerable<TestDepartament>> GetDepartaments(); 
    } 

    public class MockDataService : ITestDataService 
    { 
     public IObservable<IEnumerable<TestDepartament>> GetDepartaments() 
     { 
      return Observable.Return(TestMockData.Departaments); 
     } 
    } 

    public static class TestMockData 
    { 
     public static List<TestDepartament> Departaments 
     { 
      get 
      { 
       var departaments = new List<TestDepartament>(); 

       for (int i = 0; i < 15; i++) 
       { 
        departaments.Add(new TestDepartament 
        { 
         Name = $"Departament {i}", 
         ImageUrl = $"departament_{i}_image_url", 
         ContentUrl = $"departament_{i}_content_url", 
        }); 
       } 

       return departaments; 
      } 
     } 
    } 

    public class TestDepartament 
    { 
     public string ContentUrl { get; set; } 
     public string Name { get; set; } 
     public string ImageUrl { get; set; } 
    } 
} 

И это XUnit тест:

public class DepartamentsViewModelTests 
{ 
    private readonly TestViewModel _viewModel; 

    public DepartamentsViewModelTests() 
    { 
     var dataService = new MockDataService(); 
     _viewModel = new TestViewModel(dataService); 
    } 

    [Fact] 
    public async Task RefreshItemsTest() 
    { 
     await _viewModel.RefreshItems(); 

     Assert.Equal(TestMockData.Departaments, _viewModel.Departaments); 
    } 
} 
+0

Вы отлажена и сравнили результаты вручную? Возможно, «ДепартаментКомпьютер» испорчен? –

+0

@ m-y Спасибо за ваш ответ. Да, я отлаживал свой тест, а длина коллекции «Депозиты» в представлении модели действительно равна 0, когда тест терпит неудачу. – Andrius

ответ

0

Ваш код создает две подписки к наблюдаемой - иногда departamentsObservable.ObserveOn(SynchronizationContext.Current).Subscribe(...) выдает значения перед темawait departamentsObservable.FirstOrDefaultAsync() завершает, а иногда он не.

Две подписки означают два независимых времени, через которые проходит источник. Когда await быстро завершится, ваша другая подписка не будет вызвана (или вызывается после вызова вашего Assert.Equal), и, следовательно, значения не добавляются в список.

Попробуйте вместо этого:

public async Task RefreshItems() 
{ 
    var departamentsObservable = _dataService.GetDepartaments(); 

    await departamentsObservable.ObserveOn(SynchronizationContext.Current).Do(items => 
    { 
     Departaments.ReplaceWithRange(items); 
    }); 
} 

Теперь у вас есть только одна подписка, которая ожидает последнего значения будет производиться.

Если вы ожидаете только одного значения, и наблюдаемый, естественно, не заканчивается, тогда в конце появится .Take(1).

+0

Большое спасибо за ваше объяснение. Я заменил свой код на ваше предложение, но я получаю * Sequence не содержит исключений элементов *. – Andrius

+0

@ Andrius - где вы получаете эту ошибку? Можете ли вы добавить код в конец вашего вопроса? – Enigmativity

+0

Я получаю исключение в этой строке в моей модели просмотра: 'waitaitatesObservable.ObserveOn (SynchronizationContext.Current) .Do (items =>« Дайте мне знать, если вам нужен еще один код. – Andrius

2

Вы намерены заблокировать некоторый GUI, пока это обновление происходит. По блоку я имею в виду прогресс пользователя в блоке, показывая счетчик или другой недетерминированный индикатор выполнения?Или вы хотите, чтобы пользователь мог продолжать делать то, что они делали, но иметь возможность сигнализировать о том, что происходит какой-то фоновый процесс, а также сигнализировать, когда этот процесс будет выполнен?

Для Бывшего, я думаю, что текущая подпись у вас разумная, то есть вы возвращаете Task. Однако реализация, я думаю, может быть улучшена.

public async Task RefreshItems() 
{ 
    var items = await _dataService.GetDepartaments() 
     .Take(1) 
     .ObserveOn(SynchronizationContext.Current) 
     .ToTask(); 

    Departaments.ReplaceWithRange(items); 
} 

Примечание Take(1). Если вы попытаетесь конвертировать из IObservable<T> в Task<T>/Task, вы получаете только последнее значение или получаете, когда последовательность будет завершена. Без Take(1) мы могли просто ждать вечно. Однако, я думаю, у вас есть сценарий, который вы загружаете из кеша, поэтому можете получить 0,1 или 2 вызова OnNext. В этом случае я не знаю, что ждет последнего значения? Я также отмечаю отсутствие ошибок при обработке ошибок.

я бы, вероятно, в моем собственном коде сделать что-то вроде этого FWIW

public void RefreshItems() 
{ 
    Departments.Clear(); 
    _state = States.Processing(); 
    var items = _dataService.GetDepartaments() 
     .SubscribeOn(_schedulerProvider.Background) 
     .ObserveOn(_schedulerProvider.Foreground) 
     .Subscribe(
      item=> Departaments.Add(item), 
      ex => _state = States.Faulted(ex), 
      () => _state = States.Idle()); 
} 

Это позволяет

  • _states поле/свойство, чтобы отразить то, что в настоящее время происходит (обработка, холостой ход или какая-то ошибка),
  • Элементы, которые будут добавлены в список Departments, в качестве прихода вместо одного большого блока,
  • Не смешивается Task и IObservable<T>
  • Имеет одно место, чтобы диктовать модель параллелизма. Похоже, что есть что-то, что переключает потоки в вашей программе, которые не входят в ваш код, так как у вас есть ObserveOn, но не соответствует SubscribeOn.

Редактировать

Вот это стиль я бы идти о создании этой ViewModel. Я предпочитаю не смешивать Task и IObservable. Я также предпочитаю использовать планировщики в контекстах синхронизации. Я добавил управление ресурсами, чтобы подписки не перекрывались (если Refresh вызывается несколько раз), и поэтому его можно отменить, когда ViewModel закончен. Здесь должно быть легко проверить 0,1 или многие значения. Он также позволяет тестировать сценарии ошибок (например, OnError через таймауты, сбои в сети и т. Д.).

Я также просто для развлечения добавил свойство Async State, чтобы внешние пользователи могли видеть, что ViewModel в настоящее время что-то обрабатывает.

Я действительно перегружаю тест, чтобы иметь много проблем. Я, вероятно, не делал этого на практике, но я думаю, что это облегчает чтение для SO.

http://share.linqpad.net/67hmc2.linq

void Main() 
{ 
    var schedulerProvider = new TestSchedulerProvider(); 

    var cachedData = Enumerable.Range(0,3).Select(i => new TestDepartament 
    { 
     Name = $"Departament {i}", 
     ImageUrl = $"departament_{i}_image_url", 
     ContentUrl = $"departament_{i}_content_url", 
    }).ToArray(); 

    var liveData = Enumerable.Range(10, 5).Select(i => new TestDepartament 
    { 
     Name = $"Departament {i}", 
     ImageUrl = $"departament_{i}_image_url", 
     ContentUrl = $"departament_{i}_content_url", 
    }).ToArray(); 

    var data = schedulerProvider.Background.CreateColdObservable<IEnumerable<TestDepartament>>(
     ReactiveTest.OnNext<IEnumerable<TestDepartament>>(100, cachedData), 
     ReactiveTest.OnNext<IEnumerable<TestDepartament>>(3000, liveData), 
     ReactiveTest.OnCompleted<IEnumerable<TestDepartament>>(3000)); 

    var dataService = Substitute.For<ITestDataService>(); 
    dataService.GetDepartaments().Returns(data); 

    var viewModel = new TestViewModel(dataService, schedulerProvider); 

    Assert.Equal(AsyncState.Idle, viewModel.State); 

    viewModel.RefreshItems(); 
    Assert.Equal(AsyncState.Processing, viewModel.State); 

    schedulerProvider.Background.AdvanceTo(110); 
    schedulerProvider.Foreground.Start(); 

    Assert.Equal(cachedData, viewModel.Departments); 

    schedulerProvider.Background.Start(); 
    schedulerProvider.Foreground.Start(); 

    Assert.Equal(liveData, viewModel.Departments); 
    Assert.Equal(AsyncState.Idle, viewModel.State); 
} 

// Define other methods and classes here 
public class TestViewModel : INotifyPropertyChanged, IDisposable 
{ 
    private readonly ITestDataService _dataService; 
    private readonly ISchedulerProvider _schedulerProvider; 
    private readonly SerialDisposable _refreshSubscription = new SerialDisposable(); 
    private AsyncState _state = AsyncState.Idle; 

    public ObservableCollection<TestDepartament> Departments { get;} = new ObservableCollection<UserQuery.TestDepartament>(); 
    public AsyncState State 
    { 
     get { return _state; } 
     set 
     { 
      _state = value; 
      OnPropertyChanged(nameof(State)); 
     } 
    } 


    public TestViewModel(ITestDataService dataService, ISchedulerProvider schedulerProvider) 
    { 
     _dataService = dataService; 
     _schedulerProvider = schedulerProvider; 
    } 

    public void RefreshItems() 
    { 
     Departments.Clear(); 
     State = AsyncState.Processing; 
     _refreshSubscription.Disposable = _dataService.GetDepartaments() 
      .SubscribeOn(_schedulerProvider.Background) 
      .ObserveOn(_schedulerProvider.Foreground) 
      .Subscribe(
       items => 
       { 
        Departments.Clear(); 
        foreach (var item in items) 
        { 
         Departments.Add(item); 
        } 
       }, 
       ex => State = AsyncState.Faulted(ex.Message), 
       () => State = AsyncState.Idle); 
    } 

    #region INotifyPropertyChanged implementation 

    public event PropertyChangedEventHandler PropertyChanged; 

    private void OnPropertyChanged(string propertyName) 
    { 
     var handler = PropertyChanged; 
     if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); 
    } 

    #endregion 

    public void Dispose() 
    { 
     _refreshSubscription.Dispose(); 
    } 
} 

public interface ITestDataService 
{ 
    IObservable<IEnumerable<TestDepartament>> GetDepartaments(); 
} 

public interface ISchedulerProvider 
{ 
    IScheduler Foreground { get;} 
    IScheduler Background { get;} 
} 
public class TestSchedulerProvider : ISchedulerProvider 
{ 
    public TestSchedulerProvider() 
    { 
     Foreground = new TestScheduler(); 
     Background = new TestScheduler(); 
    } 
    IScheduler ISchedulerProvider.Foreground { get { return Foreground; } } 
    IScheduler ISchedulerProvider.Background { get { return Background;} } 
    public TestScheduler Foreground { get;} 
    public TestScheduler Background { get;} 
} 
public sealed class AsyncState 
{ 
    public static readonly AsyncState Idle = new AsyncState(false, null); 
    public static readonly AsyncState Processing = new AsyncState(true, null); 

    private AsyncState(bool isProcessing, string errorMessage) 
    { 
     IsProcessing = isProcessing; 
     IsFaulted = string.IsNullOrEmpty(errorMessage); 
     ErrorMessage = ErrorMessage; 
    } 

    public static AsyncState Faulted(string errorMessage) 
    { 
     if(string.IsNullOrEmpty(errorMessage)) 
      throw new ArgumentException(); 
     return new AsyncState(false, errorMessage); 
    } 

    public bool IsProcessing { get; } 
    public bool IsFaulted { get; } 
    public string ErrorMessage { get; } 
} 

public class TestDepartament 
{ 
    public string ContentUrl { get; set; } 
    public string Name { get; set; } 
    public string ImageUrl { get; set; } 
} 
+0

Спасибо за ваш отзыв Ли. Я использую структуру Akavache и метод 'GetAndFetchLatest'. Таким образом, наблюдаемые могут «возвращаться» 0, 1 или 2 раза. Я показываю spinner для пользователя, пока нет данных. Когда какие-либо данные (местные или извлеченные) поступают и заполняют наблюдаемую коллекцию «Департаменты», я удаляю счетчик. Если данные поступают во второй раз (то есть сначала локальный, а затем вновь загруженный), я молча обновляю наблюдаемую коллекцию 'Departaments'. Во всем процессе пользовательский интерфейс не блокируется один раз, поэтому пользователь может выполнить любое действие в любое время. Я все еще новичок в Rx, и я попробую ваше предложение. Благодарю. – Andrius

+0

@ Lee - Я попробовал свой код и, похоже, работает. В вашем примере вы «ожидаете» всего выражения, но перегрузка 'Subscribe' возвращает' IDisposable', поэтому я думаю, что это опечатка. Я прав? Если это так, это имеет одну проблему: метод 'RefreshItems' становится невозможным для тестирования, так как нет ничего, что можно было бы ожидать. Неужели моя логика испорчена? – Andrius

+0

Правильно. У меня был неправильный оператор 'await' (вы не можете ждать подписки). Это полностью проверяется, поскольку теперь вы используете только Rx, поэтому можете опираться на библиотеку тестирования. –

0

@lee-campbell «s ответ является большим, но это не хватает Xamarin форм реализации его ISchedulerProvider.

Вот что я использую:

public sealed class SchedulerProvider : ISchedulerProvider 
    { 
     public IScheduler Foreground => new SynchronizationContextScheduler(SynchronizationContext.Current); 
     public IScheduler Background => TaskPoolScheduler.Default; 
    } 
Смежные вопросы