Testing Time and Concurrency with Rx Tamir Dresher (@tamir_dresher) - - PowerPoint PPT Presentation

testing time and concurrency with rx
SMART_READER_LITE
LIVE PREVIEW

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)


slide-1
SLIDE 1

1

Tamir Dresher (@tamir_dresher) Senior Software Architect

J

Testing Time and Concurrency with Rx

1

slide-2
SLIDE 2

2

  • 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

@tamir_dresher tamirdr@codevalue.net http://www.TamirDresher.com.

About Me

slide-3
SLIDE 3

Reactive Extensions (Rx)

Your headache relief pill to Asynchronous Event based applications

Async Push Triggers Events

3

slide-4
SLIDE 4

Reacting to changes

4

slide-5
SLIDE 5

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

slide-6
SLIDE 6

???? LoadMessages(string hashtag) { facebook.Search(hashtag); twitter.Search(hashtag); linkedin.Search(hashtag); }

DoSomething( ) msg

Push Model

Push Model

6

slide-7
SLIDE 7

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

slide-8
SLIDE 8
  • bservable
  • bserver

Subscribe(observer)

subscription OnNext(X1) OnNext(Xn) ⁞

IDisposable

Observables and Observers

Observables and Observers

8

slide-9
SLIDE 9

OnCompleted() OnError(Exception)

  • bservable
  • bserver

Observables and Observers

Observables and Observers

9

slide-10
SLIDE 10

Rx packages

10

slide-11
SLIDE 11

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

slide-12
SLIDE 12

Creating Observables

12

slide-13
SLIDE 13

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

slide-14
SLIDE 14

Observable Queries (Rx Operators)

14

slide-15
SLIDE 15

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

slide-16
SLIDE 16

Reactive Search

Reactive Search

16

slide-17
SLIDE 17
  • 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

Reactive Search - Rules

Reactive Search - Rules

17

slide-18
SLIDE 18

18

slide-19
SLIDE 19

19

slide-20
SLIDE 20

20

Where(s => s.Length > 2 )

 

slide-21
SLIDE 21

21

Where(s => s.Length > 2 )

 

Throttle(TimeSpan.FromSeconds(0.5))

slide-22
SLIDE 22

22

Where(s => s.Length > 2 )

 

Throttle(TimeSpan.FromSeconds(0.5)) DistinctUntilChanged()

slide-23
SLIDE 23

24

Select(text => SearchAsync(text)) “REA” “REAC” Switch() “REA” results are ignored since we switched to the “REAC” results

slide-24
SLIDE 24

Abstracting Time and Concurrency

25

slide-25
SLIDE 25

Thread Pool Task Scheduler

  • ther

Schedulers

Schedulers

26

slide-26
SLIDE 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

slide-27
SLIDE 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

slide-28
SLIDE 28

textChanged .Throttle(TimeSpan.FromSeconds(0.5), DefaultScheduler.Instance) .DistinctUntilChanged() .SelectMany(text => SearchAsync(text)) .Switch() .Subscribe(/*handle the results*/);

29

slide-29
SLIDE 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

slide-30
SLIDE 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

slide-31
SLIDE 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

slide-32
SLIDE 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

slide-33
SLIDE 33

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> { ... }

slide-34
SLIDE 34

Testing Rx

35

slide-35
SLIDE 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

slide-36
SLIDE 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

slide-37
SLIDE 37

Separating the logic from the view

38

SearchView (Presentation, Logic, State) SearchView (Presentation) SearchViewModel (Logic, State)

Before After

slide-38
SLIDE 38

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

slide-39
SLIDE 39

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

slide-40
SLIDE 40

Injecting the Search Service client

41

public class SearchViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public SearchViewModel() { // rest of rx query }

...

}

slide-41
SLIDE 41

Injecting the Search Service client

42

public class SearchViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public SearchViewModel(ISearchServiceClient searchServiceClient) { // rest of rx query }

...

}

slide-42
SLIDE 42

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"); }

slide-43
SLIDE 43

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; } }

slide-44
SLIDE 44

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 */ }); }

slide-45
SLIDE 45

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 }

slide-46
SLIDE 46

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"); }

slide-47
SLIDE 47

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) );

slide-48
SLIDE 48

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()); }

slide-49
SLIDE 49

Summary

Pull vs. Push model Rx operators Building Rx queries Rx Concurrency Model Virtual Time Testing Time and Concurrency with TestScheduler

50

slide-50
SLIDE 50

Your headache relief pill to Asynchronous and Event based applications

Async Push Triggers Events

Reactive Extensions

Reactive Extensions

51

slide-51
SLIDE 51

www.reactivex.io github.com/Reactive-Extensions www.manning.com/dresher

Tha Thank nk You

  • u

Tamir Dresher (@tamir_dresher)

52