1
Tamir Dresher (@tamir_dresher) Senior Software Architect
J
Testing Time and Concurrency with Rx
1
Testing Time and Concurrency with Rx Tamir Dresher (@tamir_dresher) - - PowerPoint PPT Presentation
Testing Time and Concurrency with Rx Tamir Dresher (@tamir_dresher) Senior Software Architect J 1 1 About Me @tamir_dresher tamirdr@codevalue.net http://www.TamirDresher.com. Author of Rx.NET in Action (manning publications)
1
Tamir Dresher (@tamir_dresher) Senior Software Architect
J
1
2
@tamir_dresher tamirdr@codevalue.net http://www.TamirDresher.com.
About Me
Reactive Extensions (Rx)
Your headache relief pill to Asynchronous Event based applications
Async Push Triggers Events
3
Reacting to changes
4
IEnumerable<Message> LoadMessages(string hashtag) { var statuses = facebook.Search(hashtag); var tweets = twitter.Search(hashtag); var updates = linkedin.Search(hashtag); return statuses.Concat(tweets).Concat(updates); }
Twitter App Linkedin Facebook
Pull Model
Pull Model
5
???? LoadMessages(string hashtag) { facebook.Search(hashtag); twitter.Search(hashtag); linkedin.Search(hashtag); }
DoSomething( ) msg
Push Model
Push Model
6
namespace System { public interface IObservable<out T> { IDisposable Subscribe(IObserver<T> observer); } public interface IObserver<in T> { void OnNext(T value); void OnError(Exception error); void OnCompleted(); } }
Interfaces
Interfaces
7
Subscribe(observer)
subscription OnNext(X1) OnNext(Xn) ⁞
IDisposable
Observables and Observers
Observables and Observers
8
OnCompleted() OnError(Exception)
Observables and Observers
Observables and Observers
9
Rx packages
10
Push Model
Push Model with Rx Observables
class ReactiveSocialNetworksManager { //members public IObservable<Message> ObserveMessages(string hashtag) { : } } var mgr = new ReactiveSocialNetworksManager(); mgr.ObserveMessages("Rx") .Subscribe( msg => Console.WriteLine($"Observed:{msg} \t"), ex => { /*OnError*/ }, () => { /*OnCompleted*/ });
11
12
Observable.Range(1, 10) .Subscribe(x => Console.WriteLine(x)); Observable.Interval(TimeSpan.FromSeconds(1)) .Subscribe(x => Console.WriteLine(x)); Observable.FromEventPattern(SearchBox, "TextChanged")
1 sec 1 sec
Observables Factories
Observables Factories
13
14
Filtering Projection Partitioning Joins Grouping Set Element Generation Quantifiers Aggregation Error Handling Time and Concurrency Where OfType Select SelectMany Materialize Skip Take TakeUntil CombineLatest Concat join GroupBy GroupByUntil Buffer Distinct
DistinctUntilChanged
Timeout TimeInterval ElementAt First Single Range Repeat Defer All Any Contains Sum Average Scan Catch
OnErrorResumeNext
Using
Rx operators
15
Reactive Search
Reactive Search
16
Reactive Search - Rules
Reactive Search - Rules
17
18
19
20
Where(s => s.Length > 2 )
21
Where(s => s.Length > 2 )
Throttle(TimeSpan.FromSeconds(0.5))
22
Where(s => s.Length > 2 )
Throttle(TimeSpan.FromSeconds(0.5)) DistinctUntilChanged()
24
Select(text => SearchAsync(text)) “REA” “REAC” Switch() “REA” results are ignored since we switched to the “REAC” results
25
Thread Pool Task Scheduler
Schedulers
Schedulers
26
public interface IScheduler { DateTimeOffset Now { get; } IDisposable Schedule<TState>( TState state, Func<IScheduler, TState, IDisposable> action); IDisposable Schedule<TState>(TimeSpan dueTime, TState state, Func<IScheduler, TState, IDisposable> action); IDisposable Schedule<TState>(DateTimeOffset dueTime, TState state, Func<IScheduler, TState, IDisposable> action); }
27
// // Runs a timer on the default scheduler // IObservable TimeSpan // // Every operator that introduces concurrency // has an overload with an IScheduler // IObservable T TimeSpan IScheduler scheduler);
Parameterizing Concurrency
Parameterizing Concurrency
28
textChanged .Throttle(TimeSpan.FromSeconds(0.5), DefaultScheduler.Instance) .DistinctUntilChanged() .SelectMany(text => SearchAsync(text)) .Switch() .Subscribe(/*handle the results*/);
29
// // runs the observer callbacks on the specified // scheduler. // IObservable T ObserveOn<T>(IScheduler); // // runs the observer subscription and unsubsciption on // the specified scheduler. // IObservable T SubscribeOn<T>(IScheduler)
Changing Execution Context
Changing Execution Context
30
textChanged .Throttle(TimeSpan.FromSeconds(0.5)) .DistinctUntilChanged() .Select(text => SearchAsync(text)) .Switch() .ObserveOn(DispatcherScheduler.Current) .Subscribe(/*handle the results*/);
Changing Execution Context
Changing Execution Context
31
textChanged .Throttle(TimeSpan.FromSeconds(0.5)) .DistinctUntilChanged() .Select(text => SearchAsync(text)) .Switch() .ObserveOnDispatcher() .Subscribe(/*handle the results*/);
Changing Execution Context
Changing Execution Context
32
Virtual Time
What is time? Time can be a anything that is sequential and comparable
33
“Time is the indefinite continued progress of existence and events ... Time is a component quantity of various measurements used to sequence events, to compare the duration of events or the intervals between them…”
https://en.wikipedia.org/wiki/Time
Virtual Time Scheduler
34
public abstract class VirtualTimeSchedulerBase<TAbsolute, TRelative> : IScheduler, IServiceProvider, IStopwatchProvider where TAbsolute : IComparable<TAbsolute> { public TAbsolute Clock { get; protected set;} public void Start() public void Stop() public void AdvanceTo(TAbsolute time) public void AdvanceBy(TRelative time) ... } public class TestScheduler : VirtualTimeScheduler<long, long> { ... }
35
1. At least 3 characters One letter word, No Search performed 2. Don’t overflow server (0.5 sec delay) 2 words typed, 0.2 sec gap, search only the last 3. Don’t send the same string again 2 words, 1 sec gap, same value, search only first 4. Discard results if another search was requested 2 words, 1 sec gap, slow first search, fast last search, last results shown
Reactive Search - Rules
Reactive Search – Rules Tests
36
Questions and Answers
Q: How can we test the code without a real user interaction? Q: How can we test the code without a real server? Q: How can we test the code deterministically without a real asynchronicity and concurrency? Q: How can we test the code without REALLY waiting for the time to pass?
37
A: Separation of concerns. Separate the logic from the view A: Enable Dependency Injection and mock the service client A: Leverage the Rx Schedulers and provide a Scheduler you can control via DI A: Leverage the Rx TestScheduler which provides a virtualization of time
Separating the logic from the view
38
SearchView (Presentation, Logic, State) SearchView (Presentation) SearchViewModel (Logic, State)
Before After
Separating the logic from the view
39
<Window x:Class="TestableReactiveSearch.SearchView"> <DockPanel> <TextBox x:Name="SearchBox" Text="{Binding SearchTerm …}" DockPanel.Dock="Top“/> <ListBox x:Name="SearchResults" ItemsSource="{Binding SearchResults}“/> </DockPanel> </Window> public class SearchViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public SearchViewModel() { // Rx query } public string SearchTerm { get { ... } set { ... } } public IEnumerable<string> SearchResults { get { ... } set { ... } } } SearchView.xaml SearchViewModel.cs
Separating the logic from the view – fixing the Rx query
40
public SearchViewModel() { var terms = Observable.FromEventPattern<PropertyChangedEventArgs>(this, nameof(PropertyChanged)) .Where(e => e.EventArgs.PropertyName == nameof(SearchTerm)) .Select(_ => SearchTerm); _subscription = terms .Where(txt => txt.Length >= 3) .Throttle(TimeSpan.FromSeconds(0.5)) .DistinctUntilChanged() .Select(txt => searchServiceClient.SearchAsync(txt)) .Switch() .ObserveOnDispatcher() .Subscribe( results => SearchResults = results, err => { Debug.WriteLine(err); }, () => { /* OnCompleted */ }); }
Same query as before
Injecting the Search Service client
41
public class SearchViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public SearchViewModel() { // rest of rx query }
...
}
Injecting the Search Service client
42
public class SearchViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public SearchViewModel(ISearchServiceClient searchServiceClient) { // rest of rx query }
...
}
Simple test – first try
43
[TestMethod] public void Search_OneLetterWord_NoSearchSentToService() { var fakeServiceClient = Substitute.For<ISearchServiceClient>(); var vm = new SearchViewModel(fakeServiceClient); vm.SearchTerm = "A"; fakeServiceClient.DidNotReceive().SearchAsync("A"); }
Injecting concurrency
44
public interface IConcurrencyProvider { IScheduler TimeBasedOperations { get; } IScheduler Task { get; } IScheduler Thread { get; } IScheduler Dispatcher { get; } } class ConcurrencyProvider : IConcurrencyProvider { public ConcurrencyProvider() { TimeBasedOperations = DefaultScheduler.Instance; Task = TaskPoolScheduler.Default; Thread = NewThreadScheduler.Default; Dispatcher=DispatcherScheduler.Current; } public IScheduler TimeBasedOperations { get; } public IScheduler Task { get; } public IScheduler Thread { get; } public IScheduler Dispatcher { get; } }
Injecting concurrency
45
public SearchViewModel(ISearchServiceClient searchServiceClient, IConcurrencyProvider concurrencyProvider) { var terms = Observable.FromEventPattern<PropertyChangedEventArgs>(this, nameof(PropertyChanged)) .Where(e => e.EventArgs.PropertyName == nameof(SearchTerm)).Select(_=>SearchTerm); _subscription = terms .Where(txt => txt.Length >= 3) .Throttle(TimeSpan.FromSeconds(0.5), concurrencyProvider.Thread) .DistinctUntilChanged() .Select(txt => searchServiceClient.SearchAsync(txt)) .Switch() .ObserveOn(concurrencyProvider.Dispatcher) .Subscribe( results => SearchResults = results, err => { Debug.WriteLine(err); }, () => { /* OnCompleted */ }); }
Simplest Rx Test
Install-Package Microsoft.Reactive.Testing To simplify the Rx testing, derive your test class from ReactiveTest
46
using Microsoft.Reactive.Testing; [TestClass] public class SearchViewModelTests : ReactiveTest { // Test Methods }
Test 1: Search is sent after half a sec
47
const long ONE_SECOND = TimeSpan.TicksPerSecond; [TestMethod] public void MoreThanThreeLetters_HalfSecondGap_SearchSentToService() { var fakeServiceClient = Substitute.For<ISearchServiceClient>(); var fakeConcurrencyProvider = Substitute.For<IConcurrencyProvider>(); var testScheduler = new TestScheduler(); fakeConcurrencyProvider.ReturnsForAll<IScheduler>(testScheduler); var vm = new SearchViewModel(fakeServiceClient, fakeConcurrencyProvider); testScheduler.Start(); vm.SearchTerm = "reactive"; testScheduler.AdvanceBy(ONE_SECOND / 2); fakeServiceClient.Received().SearchAsync("reactive"); }
TestScheduler
TestScheduler provides two methods for creating observables:
CreateColdObservable – Creates an observable that emits its value relatively to when each observer subscribes. CreateHotObservable – Creates and observable that emits its values regardless to the observer subscription time, and each emission is configured to the absolute scheduler clock
48
var testScheduler = new TestScheduler(); ITestableObservable<int> coldObservable = testScheduler.CreateColdObservable<int>( OnNext<int>(20, 1), OnNext<int>(40, 2), OnCompleted<int>(60) );
Test 2: first search is discarded if another search happens
49
public void TwoValidWords_SlowSearchThenFastSearch_SecondSearchResultsOnly() { var fakeServiceClient = Substitute.For<ISearchServiceClient>(); var fakeConcurrencyProvider = Substitute.For<IConcurrencyProvider>(); var testScheduler = new TestScheduler(); fakeConcurrencyProvider.ReturnsForAll<IScheduler>(testScheduler); fakeServiceClient.SearchAsync("first").Returns(testScheduler.CreateColdObservable( OnNext<IEnumerable<string>>(2 * ONE_SECOND, new[] {"first"}), ...); fakeServiceClient.SearchAsync("second").Returns(testScheduler.CreateColdObservable( OnNext<IEnumerable<string>>(1, new[] { "second" }), ...); var vm = new SearchViewModel(fakeServiceClient, fakeConcurrencyProvider); testScheduler.Start(); vm.SearchTerm = "first"; testScheduler.AdvanceBy(ONE_SECOND); vm.SearchTerm = "second"; testScheduler.AdvanceBy(5 * ONE_SECOND); Assert.AreEqual("second", vm.SearchResults.First()); }
Summary
Pull vs. Push model Rx operators Building Rx queries Rx Concurrency Model Virtual Time Testing Time and Concurrency with TestScheduler
50
Your headache relief pill to Asynchronous and Event based applications
Async Push Triggers Events
Reactive Extensions
Reactive Extensions
51
www.reactivex.io github.com/Reactive-Extensions www.manning.com/dresher
Tamir Dresher (@tamir_dresher)
52