Skip to content
Permalink
Browse files

housekeeping: Add testing sample

  • Loading branch information...
glennawatson committed Mar 10, 2019
1 parent 3d7d15a commit fce49c8415735ac3b6b8da19299798f6d301d8ef
@@ -0,0 +1,51 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI.Samples.Testing.SimpleViewModels", "src\ReactiveUI.Samples.Testing.SimpleViewModels\ReactiveUI.Samples.Testing.SimpleViewModels.csproj", "{3BD3E9A3-BDF2-4F8F-A635-76B125CCCD9B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI.Samples.Testing.SimpleViewModelsUnitTests", "src\ReactiveUI.Samples.Testing.SimpleViewModels.Tests\ReactiveUI.Samples.Testing.SimpleViewModelsUnitTests.csproj", "{CECE0F04-2D7F-476E-8D0D-8EBC9FEAF598}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3BD3E9A3-BDF2-4F8F-A635-76B125CCCD9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3BD3E9A3-BDF2-4F8F-A635-76B125CCCD9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3BD3E9A3-BDF2-4F8F-A635-76B125CCCD9B}.Debug|x64.ActiveCfg = Debug|Any CPU
{3BD3E9A3-BDF2-4F8F-A635-76B125CCCD9B}.Debug|x64.Build.0 = Debug|Any CPU
{3BD3E9A3-BDF2-4F8F-A635-76B125CCCD9B}.Debug|x86.ActiveCfg = Debug|Any CPU
{3BD3E9A3-BDF2-4F8F-A635-76B125CCCD9B}.Debug|x86.Build.0 = Debug|Any CPU
{3BD3E9A3-BDF2-4F8F-A635-76B125CCCD9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3BD3E9A3-BDF2-4F8F-A635-76B125CCCD9B}.Release|Any CPU.Build.0 = Release|Any CPU
{3BD3E9A3-BDF2-4F8F-A635-76B125CCCD9B}.Release|x64.ActiveCfg = Release|Any CPU
{3BD3E9A3-BDF2-4F8F-A635-76B125CCCD9B}.Release|x64.Build.0 = Release|Any CPU
{3BD3E9A3-BDF2-4F8F-A635-76B125CCCD9B}.Release|x86.ActiveCfg = Release|Any CPU
{3BD3E9A3-BDF2-4F8F-A635-76B125CCCD9B}.Release|x86.Build.0 = Release|Any CPU
{CECE0F04-2D7F-476E-8D0D-8EBC9FEAF598}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CECE0F04-2D7F-476E-8D0D-8EBC9FEAF598}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CECE0F04-2D7F-476E-8D0D-8EBC9FEAF598}.Debug|x64.ActiveCfg = Debug|Any CPU
{CECE0F04-2D7F-476E-8D0D-8EBC9FEAF598}.Debug|x64.Build.0 = Debug|Any CPU
{CECE0F04-2D7F-476E-8D0D-8EBC9FEAF598}.Debug|x86.ActiveCfg = Debug|Any CPU
{CECE0F04-2D7F-476E-8D0D-8EBC9FEAF598}.Debug|x86.Build.0 = Debug|Any CPU
{CECE0F04-2D7F-476E-8D0D-8EBC9FEAF598}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CECE0F04-2D7F-476E-8D0D-8EBC9FEAF598}.Release|Any CPU.Build.0 = Release|Any CPU
{CECE0F04-2D7F-476E-8D0D-8EBC9FEAF598}.Release|x64.ActiveCfg = Release|Any CPU
{CECE0F04-2D7F-476E-8D0D-8EBC9FEAF598}.Release|x64.Build.0 = Release|Any CPU
{CECE0F04-2D7F-476E-8D0D-8EBC9FEAF598}.Release|x86.ActiveCfg = Release|Any CPU
{CECE0F04-2D7F-476E-8D0D-8EBC9FEAF598}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A01B622A-B041-46C4-8B61-76F2D9F62477}
EndGlobalSection
EndGlobal
@@ -0,0 +1,31 @@
using ReactiveUI.Samples.Testing.SimpleViewModels;
using Xunit;

namespace ReactiveUI.Samples.Testing.SimpleViewModelsUnitTests
{
/// <summary>
/// A few sample unit tests. Note how simple they are: since the concept of time
/// is not involved we do not have to model it in our unit tests. We test these almost
/// as if they were a non ReactiveUI object.
/// </summary>
public class CalculatorViewModelTest
{
[Fact]
public void TestTypingStringGetsError()
{
var fixture = new CalculatorViewModel();
fixture.InputText = "hi";
Assert.Equal("Error", fixture.ErrorText);
Assert.Equal("", fixture.ResultText);
}

[Fact]
public void TestTypingInteger()
{
var fixture = new CalculatorViewModel();
fixture.InputText = "50";
Assert.Equal("", fixture.ErrorText);
Assert.Equal("100", fixture.ResultText);
}
}
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Reactive.Testing" Version="4.0.0" />
<PackageReference Include="ReactiveUI.Testing" Version="9.11.3" />
<PackageReference Include="xunit" Version="2.4.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ReactiveUI.Samples.Testing.SimpleViewModels\ReactiveUI.Samples.Testing.SimpleViewModels.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,78 @@
using Microsoft.Reactive.Testing;
using ReactiveUI.Samples.Testing.SimpleViewModels;
using ReactiveUI.Testing;
using System.Reactive.Linq;
using Xunit;

namespace ReactiveUI.Samples.Testing.SimpleViewModelsUnitTests
{
/// <summary>
/// The web call ViewModel is time dependent. There is the webservice time and there
/// is the time that one waits for the user to stop typing. We could wait 800 ms, and test
/// that way. Or we can time travel with some nifty tools from the System.Reactive.Testing
/// namespace.
/// </summary>
public class WebCallViewModelTest
{
/// <summary>
/// Make sure no webservice call is send off until 800 ms have passed.
/// </summary>
[Fact]
public void TestNothingTill800ms()
{
// Run a test scheduler to put time under our control.
new TestScheduler().With(s =>
{
var fixture = new WebCallViewModel(new immediateWebService());
fixture.InputText = "hi";

// Run the clock forward to 800 ms. At that point, nothing should have happened.
s.AdvanceToMs(799);
Assert.Equal("", fixture.ResultText);

// Run the clock 1 tick past and the result should show up.
s.AdvanceToMs(801);
Assert.Equal("result hi", fixture.ResultText);
});
}

/// <summary>
/// User types something, pauses, then types something again.
/// </summary>
[Fact]
public void TestDelayAfterUpdate()
{
// Run a test scheduler to put time under our control.
new TestScheduler().With(s =>
{
var fixture = new WebCallViewModel(new immediateWebService());
fixture.InputText = "hi";

// Run the clock forward 300 ms, where they type again.
s.AdvanceToMs(300);
fixture.InputText = "there";

// Now, at 800, there should be nothing!
s.AdvanceToMs(799);
Assert.Equal("", fixture.ResultText);

// But, at 800+300+1, our result should appear!
s.AdvanceToMs(800 + 300 + 1);
Assert.Equal("result there", fixture.ResultText);
});
}

/// <summary>
/// This dummy webservice takes zero time so we can isolate the timing tests for
/// the typing above.
/// </summary>
class immediateWebService : IWebCaller
{
public System.IObservable<string> GetResult(string searchItems)
{
return Observable.Return("result " + searchItems);
}
}

}
}
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Reactive.Windows.Threading" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.2.5.0" newVersion="2.2.5.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Reactive.Core" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.1.30214.0" newVersion="2.1.30214.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Reactive.Interfaces" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.1.30214.0" newVersion="2.1.30214.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Reactive.Linq" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.1.30214.0" newVersion="2.1.30214.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
@@ -0,0 +1,50 @@
using System.Reactive.Linq;

namespace ReactiveUI.Samples.Testing.SimpleViewModels
{
/// <summary>
/// A view model with some straight forward calculations. There are no
/// async operations involve, nor are there any delays or other time related
/// calls.
/// Operation: The user enters text into the input text field.
/// If the text doesn't contain numbers, then an error message is shown in the ErrorText field
/// Otherwise the number x2 is shown in the ResultText field.
/// The ErrorText and ResultText fields should be empty when nothing is in InputText.
/// </summary>
public class CalculatorViewModel : ReactiveObject
{
private readonly ObservableAsPropertyHelper<string> _ErrorText;
private readonly ObservableAsPropertyHelper<string> _ResultText;

public string InputText
{
get => _InputText;
set => this.RaiseAndSetIfChanged(ref _InputText, value);
}
string _InputText;

public string ErrorText => _ErrorText.Value;

public string ResultText => _ResultText.Value;

public CalculatorViewModel()
{
var haveInput = this.WhenAny(x => x.InputText, x => x.Value)
.Where(x => !string.IsNullOrEmpty(x));

// Convert into a stream of parsed integers, or null if we fail.
var parsedIntegers = haveInput
.Select(x => int.TryParse(x, out var val) ? (int?)val : null);

// Now, the error text
parsedIntegers
.Select(x => x.HasValue ? "" : "Error")
.ToProperty(this, x => x.ErrorText, out _ErrorText);

// And the result, which is *2 of the input.
parsedIntegers
.Select(x => x.HasValue ? (x.Value * 2).ToString() : "")
.ToProperty(this, x => x.ResultText, out _ResultText);
}
}
}
@@ -0,0 +1,15 @@

using System;
namespace ReactiveUI.Samples.Testing.SimpleViewModels
{
public interface IWebCaller
{
/// <summary>
/// Return the web service call string result given the input.
/// Can take an indeterminate amount of time.
/// </summary>
/// <param name="searchItems"></param>
/// <returns></returns>
IObservable<string> GetResult(string searchItems);
}
}
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ReactiveUI" Version="9.11.3"/>
</ItemGroup>
</Project>
@@ -0,0 +1,52 @@

using System;
using System.Reactive;
using System.Reactive.Linq;

namespace ReactiveUI.Samples.Testing.SimpleViewModels
{
/// <summary>
/// A class that simulates a call to a web service, which is expected to
/// take some time. We also do some work so that we don't flood the
/// web service each time the user updates the input.
/// </summary>
public class WebCallViewModel : ReactiveObject
{
private readonly ObservableAsPropertyHelper<string> _ResultTextOAPH;
private string _InputText;

public string InputText
{
get => _InputText;
set => this.RaiseAndSetIfChanged(ref _InputText, value);
}

public string ResultText => _ResultTextOAPH.Value;

public ReactiveCommand<string, string> DoWebCall { get; }

/// <summary>
/// Setup the lookup logic, and use the interface to do the web call.
/// </summary>
/// <param name="caller"></param>
public WebCallViewModel(IWebCaller caller)
{
// Do a search when nothing new has been entered for 800 ms and it isn't
// an empty string... and don't search for the same thing twice.

var newSearchNeeded = this.WhenAny(p => p.InputText, x => x.Value)
.Throttle(TimeSpan.FromMilliseconds(800), RxApp.TaskpoolScheduler)
.DistinctUntilChanged()
.Where(x => !string.IsNullOrWhiteSpace(x));

DoWebCall = ReactiveCommand.CreateFromObservable<string, string>(x => caller.GetResult(x));

newSearchNeeded.InvokeCommand(DoWebCall);

// The results are stuffed into the property, on the proper thread
// (ToProperty takes care of that) when done. We never want the property to
// be null, so we give it an initial value of "".
DoWebCall.ToProperty(this, x => x.ResultText, out _ResultTextOAPH, "");
}
}
}

0 comments on commit fce49c8

Please sign in to comment.
You can’t perform that action at this time.