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) • Software architect, consultant and instructor • Software Engineering Lecturer @ Ruppin Academic Center • OzCode (www.oz-code.com) Evangelist 2
Reactive Extensions (Rx) Your headache relief pill to Asynchronous Event based applications Push Async Triggers Events 3
Reacting to changes 4
Pull Model Pull Model 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); } Facebook Twitter App Linkedin 5
Push Model Push Model ???? LoadMessages(string hashtag) { facebook.Search(hashtag); twitter.Search(hashtag); linkedin.Search(hashtag); } DoSomething( ) msg 6
Interfaces Interfaces 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(); } } 7
Observables and Observers Observables and Observers Subscribe(observer) OnNext(X 1 ) ⁞ observable observer subscription OnNext(X n ) IDisposable ⁞ 8
Observables and Observers Observables and Observers OnCompleted() observable observer OnError(Exception) ⁞ 9
Rx packages 10
Push Model with Rx Observables Push Model 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
Creating Observables 12
Observables Factories Observables Factories Observable.Range(1, 10) .Subscribe(x => Console.WriteLine(x)); ⁞ Observable.Interval(TimeSpan.FromSeconds(1)) .Subscribe(x => Console.WriteLine(x)); ⁞ 1 sec 1 sec Observable.FromEventPattern(SearchBox, "TextChanged") ⁞ 13
Observable Queries (Rx Operators) 14
Rx operators Filtering Projection Partitioning Joins Grouping Set Select Where GroupBy Skip CombineLatest Distinct SelectMany GroupByUntil Concat Take OfType DistinctUntilChanged TakeUntil Materialize join Buffer Generation Element Quantifiers Aggregation Error Handling Time and Concurrency Catch Range All Sum ElementAt Timeout OnErrorResumeNext Defer Average Any First TimeInterval Using Repeat Contains Scan Single 15
Reactive Search Reactive Search 16
Reactive Search - Rules Reactive Search - Rules 1. At least 3 characters 2. Don ’ t overflow server (0.5 sec delay) 3. Don ’ t send the same string again 4. Discard results if another search was requested 17
18
19
Where(s => s.Length > 2 ) 20
Where(s => s.Length > 2 ) Throttle(TimeSpan.FromSeconds(0.5)) 21
Where(s => s.Length > 2 ) Throttle(TimeSpan.FromSeconds(0.5)) DistinctUntilChanged() 22
Select(text => SearchAsync(text)) “ REA ” “ REA ” results are ignored since we “ REAC ” switched to the “ REAC ” results Switch() 24
Abstracting Time and Concurrency 25
Schedulers Schedulers Thread Pool Task Scheduler other 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
Parameterizing Concurrency Parameterizing Concurrency // // 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 ); 28
textChanged .Throttle(TimeSpan.FromSeconds(0.5), DefaultScheduler.Instance ) .DistinctUntilChanged() .SelectMany(text => SearchAsync(text)) .Switch() .Subscribe(/*handle the results*/); 29
Changing Execution Context Changing Execution Context // // 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 ) 30
Changing Execution Context Changing Execution Context textChanged .Throttle(TimeSpan.FromSeconds(0.5)) .DistinctUntilChanged() .Select(text => SearchAsync(text)) .Switch() .ObserveOn(DispatcherScheduler.Current) .Subscribe(/*handle the results*/); 31
Changing Execution Context Changing Execution Context textChanged .Throttle(TimeSpan.FromSeconds(0.5)) .DistinctUntilChanged() .Select(text => SearchAsync(text)) .Switch() .ObserveOnDispatcher() .Subscribe(/*handle the results*/); 32
Virtual Time What is time? “ 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 Time can be a anything that is sequential and comparable 33
Virtual Time Scheduler 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> { ... } 34
Testing Rx 35
Reactive Search – Rules Tests Reactive Search - Rules At least 3 characters 1. One letter word, No Search performed Don ’ t overflow server (0.5 sec delay) 2. 2 words typed, 0.2 sec gap, search only the last Don ’ t send the same string again 3. 2 words, 1 sec gap, same value, search only first Discard results if another search was requested 4. 2 words, 1 sec gap, slow first search, fast last search, last results shown 36
Questions and Answers Q: How can we test the code without a real user interaction? A: Separation of concerns. Separate the logic from the view Q: How can we test the code without a real server? A: Enable Dependency Injection and mock the service client Q: How can we test the code deterministically without a real asynchronicity and concurrency? A: Leverage the Rx Schedulers and provide a Scheduler you can control via DI Q: How can we test the code without REALLY waiting for the time to pass? A: Leverage the Rx TestScheduler which provides a virtualization of time 37
Separating the logic from the view SearchView Before (Presentation, Logic, State) After SearchView SearchViewModel (Presentation) (Logic, State) 38
Separating the logic from the view SearchView.xaml <Window x:Class="TestableReactiveSearch.SearchView"> <DockPanel> <TextBox x:Name="SearchBox" Text="{Binding SearchTerm … }" DockPanel.Dock="Top “ /> <ListBox x:Name="SearchResults" ItemsSource="{Binding SearchResults} “ /> </DockPanel> SearchViewModel.cs </Window> public class SearchViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public SearchViewModel() { // Rx query } public string SearchTerm { get { ... } set { ... } } public IEnumerable<string> SearchResults { get { ... } set { ... } } } 39
Separating the logic from the view – fixing the Rx query 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() Same query as before .ObserveOnDispatcher() .Subscribe( results => SearchResults = results, err => { Debug.WriteLine(err); }, () => { /* OnCompleted */ }); } 40
Injecting the Search Service client public class SearchViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public SearchViewModel() { // rest of rx query } ... } 41
Injecting the Search Service client public class SearchViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public SearchViewModel( ISearchServiceClient searchServiceClient ) { // rest of rx query } ... } 42
Recommend
More recommend