自己的九又四分之三站台

自己的九又四分之三站台

Testing is an essential part of software quality. Unit testing advocates have become common in recent years; it seems that you read or hear about it everywhere. Some promote test-driven development, a style of coding that ensures you have comprehensive tests when the application is complete. The benefits of unit testing on code quality and overall time to completion are well known, and yet many developers still don’t write unit tests.

测试是软件质量的重要组成部分。近年来,单元测试倡导者已经变得很普遍;似乎你在任何地方都读到或听说过它。有些人提倡测试驱动开发,这是一种编码风格,可确保在应用程序完成时进行全面的测试。单元测试对代码质量和总体完成时间的好处是众所周知的,但是许多开发人员仍然不编写单元测试。

I encourage you to write at least some unit tests. Start with the code in which you feel the least confidence. In my experience, unit tests have given me two main advantages:

我鼓励您至少编写一些单元测试。从您感到最不自信的代码开始。根据我的经验,单元测试给了我两个主要优势:

Better understanding of the code. You know that part of the application that works but you have no idea how? It’s always kind of in the back of your mind when the really weird bug reports come in. Writing unit tests for code you find difficult is a great way to get a clear understanding of how it works. After writing unit tests describing its behavior, the code is no longer mysterious; you end up with a set of unit tests that describe its behavior and the dependencies that code has on the rest of the code.

更好地理解代码. 你知道应用程序运行的那部分,但你不知道如何运行?当真正奇怪的bug报告出现时,它总是在你的脑海里。为您觉得困难的代码编写单元测试是清晰理解其工作原理的好方法。在编写了描述其行为的单元测试后,代码不再神秘;你最终会得到一组单元测试,这些单元测试描述了它的行为以及代码对其他代码的依赖关系。

Greater confidence to make changes. Sooner or later, you’ll get that feature request that requires you to change the code that scares you, and you’ll no longer be able to pretend it isn’t there (I know how that feels; I’ve been there!). It’s best to be proactive: write the unit tests for the scary code before the feature request comes in. Once your unit tests are complete, you’ll have an early warning system that will alert you immediately if your changes break existing behavior. When you have a pull request, unit tests also give you greater confidence that the code changes don’t break existing behavior.

更有信心做出改变. 迟早,你会收到功能请求,要求你修改令你害怕的代码,而你将不再能够假装它不存在(我知道那是什么感觉;我也经历过!)最好是积极主动:在特性请求到来之前为可怕的代码编写单元测试。一旦您的单元测试完成,您将拥有一个早期预警系统,如果您的更改破坏了现有的行为,它将立即提醒您。当您有拉取请求时,单元测试还可以让您更加确信代码更改不会破坏现有的行为。

Both of these advantages apply to your own code just as much as others’ code. I’m sure there are other advantages, too. Does unit testing decrease the frequency of bugs? Most likely. Does unit testing reduce the overall time on a project? Possibly. But the advantages I’ve described are definite; I experience them every time I write unit tests. So, that’s my sales pitch for unit testing.

这两个优点既适用于您自己的代码,也适用于其他人的代码。我相信还有其他的好处。单元测试是否减少了bug出现的频率?最有可能。单元测试是否减少了项目的总时间?可能。但我所描述的优点是明确的;每次编写单元测试时,我都会遇到这些问题。这就是我对单元测试的推销。

This chapter contains recipes that are all about testing. A lot of developers (even ones who normally write unit tests) shy away from testing concurrent code because they assume it’s hard. However, as these recipes will show, unit testing concurrent code isn’t as difficult as they think. Modern features and libraries, such as async and System.Reactive, have put a lot of thought into testing, and it shows. I encourage you to use these recipes to write unit tests, especially if you’re new to concurrency (i.e., the new concurrent code appears hard or scary).

本章包含的食谱都是关于测试的。许多开发人员(甚至是那些通常编写单元测试的开发人员)都回避测试并发代码,因为他们认为这很难。然而,正如这些食谱所示,单元测试并发代码并不像他们想象的那么困难。现代功能和库,如async和System.Reactive,我在测试上花了很多心思,结果很明显。我鼓励您使用这些方法来编写单元测试,特别是如果您是并发的新手(也就是说,新的并发代码看起来很难或可怕)。

7.1. Unit Testing async Methods 单元测试异步方法

Problem 问题

You have an async method that you need to unit test.

您有一个需要进行单元测试的异步方法。

Solution 解决方案

Most modern unit test frameworks support async Task unit test methods, including MSTest, NUnit, and xUnit. MSTest began support for these tests with Visual Studio 2012. If you use another unit test framework, you may have to upgrade to the latest version.

大多数现代单元测试框架都支持异步任务单元测试方法,包括MSTest、NUnit和xUnit.MSTest从Visual Studio 2012开始支持这些测试。如果您使用另一个单元测试框架,您可能必须升级到最新版本。

Here is an example of an async MSTest unit test:

下面是一个异步MSTest单元测试的例子:

[TestMethod] public async Task MyMethodAsync_ReturnsFalse() 
{
	var objectUnderTest = ...; 
	bool result = await objectUnderTest.MyMethodAsync(); 
	Assert.IsFalse(result); 
}

The unit test framework will notice that the return type of the method is Task and will intelligently wait for the task to complete before marking the test “successful” or “failed.”

单元测试框架将注意到方法的返回类型是Task,并在标记测试“成功”或“失败”之前智能地等待任务完成。

If your unit test framework doesn’t support async Task unit tests, then it’ll need some help to wait for the asynchronous operation under test. One option is that you can use GetAwaiter().GetResult() to synchronously block on the task; if you then use GetAwaiter().GetResult() instead of Wait(), it avoids the AggregateException wrapper if the task has an exception. However, I prefer to use the AsyncContext type from the Nito.AsyncEx NuGet package:

如果单元测试框架不支持异步任务单元测试,那么它将需要一些帮助来等待被测的异步操作。一种选择是,您可以使用GetAwaiter().GetResult()来同步阻塞任务;如果您随后使用GetAwaiter(). getresult()而不是Wait(),那么如果任务有异常,它将避免使用AggregateException包装器。然而,我更喜欢使用Nito的AsyncContext类型。AsyncEx NuGet包:

[TestMethod] 
public void MyMethodAsync_ReturnsFalse() 
{ 
	AsyncContext.Run(async () => 
	{ 
		var objectUnderTest = ...; 
		bool result = await objectUnderTest.MyMethodAsync(); 
		Assert.IsFalse(result); 
	}); 
}

AsyncContext.Run will wait until all asynchronous methods complete.

AsyncContext.Run将等待所有异步方法完成。

Discussion 讨论

Mocking asynchronous dependencies can be a bit awkward at first. It’s a good idea to at least test how your methods respond to synchronous success (mocking with Task.FromResult), synchronous errors (mocking with Task.FromException), and asynchronous success (mocking with Task.Yield and a return value). You’ll find coverage of Task.FromResult and Task.FromException in Recipe 2.2. Task.Yield can be used to force asynchronous behavior, and is primarily useful for unit tests:

一开始,模拟异步依赖关系可能会有点尴尬。至少测试一下您的方法如何响应同步成功(用Task. fromresult进行模拟)、同步错误(用Task. fromexception进行模拟)和异步成功(用Task. fromexception进行模拟)是一个好主意。Yield和返回值)。您将在Recipe 2.2中找到对Task.FromResult和Task.FromException的介绍。Task.Yield可以用来强制异步行为,主要用于单元测试:

interface IMyInterface 
{ 
	Task SomethingAsync(); 
} 

class SynchronousSuccess : IMyInterface 
{ 
	public Task SomethingAsync() 
	{ 
		return Task.FromResult(13); 
	} 
} 

class SynchronousError : IMyInterface 
{
	public Task SomethingAsync() 
	{ 
		return Task.FromException(new InvalidOperationException()); 
	} 
} 

class AsynchronousSuccess : IMyInterface
{ 
	public async Task SomethingAsync() 
	{ 
		await Task.Yield(); // Force asynchronous behavior. 
		return 13; 
	} 
}

When testing asynchronous code, deadlocks and race conditions may surface more often than when testing synchronous code. I find the per-test timeout setting useful; in Visual Studio, you can add a test settings file to your solution that enables you to set individual test timeouts. The default value is quite high; I usually have a per-test timeout setting of two seconds.

在测试异步代码时,死锁和竞争条件可能比测试同步代码时更频繁地出现。我发现每个测试超时设置很有用;在Visual Studio中,您可以向解决方案添加测试设置文件,使您能够设置单独的测试超时。默认值相当高;我通常将每个测试的超时设置为两秒。

The AsyncContext type is in the Nito.AsyncEx NuGet package.

AsyncContext类型在Nito.AsyncEx的NuGet包中。

7.2.Unit Testing async Methods Expected to Fail单元测试异步方法可能会失败

Problem 问题

You need to write a unit test that checks for a specific failure of an async Task method.

您需要编写一个单元测试来检查异步任务方法的特定失败。

Solution 解决方案

If you’re doing desktop or server development, MSTest does support failure testing via the regular ExpectedExceptionAttribute:

如果你正在做桌面或服务器开发,MSTest确实支持通过常规的ExpectedExceptionAttribute进行故障测试:

// Not a recommended solution; see below. 
[TestMethod] 
[ExpectedException(typeof(DivideByZeroException))] 
public async Task Divide_WhenDenominatorIsZero_ThrowsDivideByZero() 
{ 
	await MyClass.DivideAsync(4, 0); 
}

However, this solution isn’t the best: ExpectedException is actually a poor design. The exception it expects may be thrown by any of the methods called by your unit test method. A better design checks that a particular piece of code throws that exception, not the unit test as a whole.

然而,这个解决方案并不是最好的:ExpectedException实际上是一个糟糕的设计。它期望的异常可能由单元测试方法调用的任何方法抛出。更好的设计是检查抛出异常的特定代码段,而不是整个单元测试。

Most modern unit test frameworks include Assert.ThrowsAsync<TException> in some form. For example, you can use xUnit’s ThrowsAsync like this:

大多数现代单元测试框架包括Assert.ThrowsAsync<TException>以某种形式。例如,你可以像这样使用xUnit的ThrowsAsync
[Fact] 
public async Task Divide_WhenDenominatorIsZero_ThrowsDivideByZero() 
{ 
	await Assert.ThrowsAsync(async () => 
	{ 
		await MyClass.DivideAsync(4, 0); 
	}); 
}

Do not forget to await the task returned by ThrowsAsync! The await will propagate any assertion failures that it detects. If you forget the await and ignore the compiler warning, your unit test will always silently succeed regardless of your method’s behavior.

不要忘记等待由ThrowsAsync返回的任务!await将传播它检测到的任何断言失败。如果忘记了await并忽略编译器警告,那么无论方法的行为如何,单元测试都将静默地成功。

Unfortunately, several other unit test frameworks don’t include an equivalent async-compatible ThrowsAsync. If you find yourself in this boat, create your own:

不幸的是,其他几个单元测试框架没有包含与异步兼容的等效ThrowsAsync。如果你发现自己在这条船上,创造你自己的:

/// 
/// Ensures that an asynchronous delegate throws an exception. 
/// 
/// 
/// The type of exception to expect. 
/// 
/// The asynchronous delegate to test. 
/// 
/// Whether derived types should be accepted. 
/// 
public static async Task ThrowsAsync(Func action, bool allowDerivedTypes = true)where TException : Exception 
{ 
	try { 
		await action(); 
		var name = typeof(Exception).Name; 
		Assert.Fail($"Delegate did not throw expected exception {name}."); 
		return null; 
	} catch (Exception ex) 
	{ 
		if (allowDerivedTypes && !(ex is TException)) 
			Assert.Fail($"Delegate threw exception of type {ex.GetType().Name}" + $", but {typeof(TException).Name} or a derived type was expected."); 
		if (!allowDerivedTypes && ex.GetType() != typeof(TException)) 
			Assert.Fail($"Delegate threw exception of type {ex.GetType().Name}" + $", but {typeof(TException).Name} was expected."); 
		return (TException)ex;
	 } 
}
You can use the method just like it was any other Assert.ThrowsAsync<TException> method. Don’t forget to await the return value!
你可以像使用其他一样使用Assert.ThrowsAsync方法。不要忘记等待返回值!

Discussion 讨论

Testing error handling is just as important as testing the successful scenarios. Some would even say more important, since the successful scenario is the one that everyone tries before the software is released. If your application behaves strangely, it will be due to an unexpected error situation.

测试错误处理与测试成功的场景同样重要。有些人甚至会说更重要,因为成功的场景是每个人在软件发布之前都尝试过的场景。如果您的应用程序行为奇怪,那将是由于意外的错误情况。

However, I encourage developers to move away from ExpectedException. It’s better to test for an exception thrown at a specific point rather than testing for an exception at any time during the test. Instead of ExpectedException, use ThrowsAsync (or its equivalent in your unit test framework), or use the ThrowsAsync implementation, as in the last code example.

然而,我鼓励开发人员远离ExpectedException。最好测试在特定点抛出的异常,而不是在测试期间的任何时间测试异常。不要使用ExpectedException,而是使用ThrowsAsync(或在单元测试框架中使用它的等效函数),或者使用ThrowsAsync实现,如上一个代码示例中所示。

7.3.Unit Testing async void Methods单元测试异步void方法

Problem 问题

You have an async void method that you need to unit test.

您有一个需要进行单元测试的async void方法。

Solution 解决方案

Stop.
Rather than solving this problem, you should do your dead-level best to avoid it. If it’s possible to change your async void method to an async Task method, then do so.

停止。
与其解决这个问题,不如尽最大努力避免它。如果可以将async void方法更改为async Task方法,那么就这样做。

If your method must be async void (e.g., to satisfy an interface method signature), then consider writing two methods: an async Task method that contains all the logic, and an async void wrapper that just calls the async Task method and awaits the result. The async void method satisfies the architecture requirements, while the async Task method (with all the logic) is testable.

如果你的方法必须是async void(例如,为了满足接口方法签名),那么考虑编写两个方法:一个包含所有逻辑的async Task方法,以及一个只调用async Task方法并等待结果的async void包装器。async void方法满足体系结构需求,而async Task方法(包含所有逻辑)是可测试的。

If it’s impossible to change your method and you must unit test an async void method, then there is a way to do it. You can use the AsyncContext class from the Nito.AsyncEx library:

如果不可能改变你的方法,你必须对async void方法进行单元测试,那么有一种方法可以做到这一点。你可以使用Nito的AsyncContext类。AsyncEx库:

// Not a recommended solution; see the rest of this section. 
[TestMethod] 
public void MyMethodAsync_DoesNotThrow() 
{ 
	AsyncContext.Run(() => 
	{ 
		var objectUnderTest = new Sut(); // ...; 
		objectUnderTest.MyVoidMethodAsync(); 
	}); 
}

The AsyncContext type will wait until all asynchronous operations complete (including async void methods) and will propagate exceptions that they raise.

AsyncContext类型将等待所有异步操作完成(包括async void方法),并传播它们引发的异常。

The AsyncContext type is in the Nito.AsyncEx NuGet package.

AsyncContext类型在Nito.AsyncEx的NuGet包中。

Discussion 讨论

One of the key guidelines in async code is to avoid async void. I strongly recommend you refactor your code instead of using AsyncContext for unit testing async void methods.

异步代码的一个关键准则是避免async void。我强烈建议您重构代码,而不是使用AsyncContext进行单元测试async void方法。

7.4. Unit Testing Dataflow Meshes单元测试数据流网格

Problem 问题

You have a dataflow mesh in your application, and you need to verify it works correctly.

您的应用程序中有一个数据流网格,您需要验证它是否正常工作。

Solution 解决方案

Dataflow meshes are independent: they have a lifetime of their own and are asynchronous by nature. So, the most natural way to test them is with an asynchronous unit test. The following unit test verifies the custom dataflow block from Recipe 5.6:

数据流网格是独立的:它们有自己的生命周期,本质上是异步的。因此,最自然的测试方法是使用异步单元测试。下面的单元测试验证了Recipe 5.6中的自定义数据流块:

[TestMethod] 
public async Task MyCustomBlock_AddsOneToDataItems() 
{ 
	var myCustomBlock = CreateMyCustomBlock(); 
	myCustomBlock.Post(3); 
	myCustomBlock.Post(13); 
	myCustomBlock.Complete(); 
	
	Assert.AreEqual(4, myCustomBlock.Receive()); 
	Assert.AreEqual(14, myCustomBlock.Receive()); 
	
	await myCustomBlock.Completion; 
}

Unit testing failures isn’t quite as straightforward, unfortunately. This is because exceptions in dataflow meshes are wrapped in another AggregateException each time they are propagated to the next block. The following example uses a helper method to ensure that an exception will discard data and propagate through the custom block:

不幸的是,单元测试失败并不是那么简单。这是因为每当数据流网格中的异常被传播到下一个块时,它们都被包装在另一个AggregateException中。下面的例子使用了一个helper方法来确保异常将丢弃数据并在自定义块中传播:

[TestMethod] 
public async Task MyCustomBlock_Fault_DiscardsDataAndFaults() 
{ 
	var myCustomBlock = CreateMyCustomBlock(); 
	myCustomBlock.Post(3); 
	myCustomBlock.Post(13); 
	(myCustomBlock as IDataflowBlock).Fault(new InvalidOperationException()); 
	
	try 
	{ 
		await myCustomBlock.Completion; 
	} 
	catch (AggregateException ex) 
	{ 
		AssertExceptionIs( ex.Flatten().InnerException, false); 
	} 
} 

public static void AssertExceptionIs(Exception ex, bool allowDerivedTypes = true) 
{ 
	if (allowDerivedTypes && !(ex is TException)) 
		Assert.Fail($"Exception is of type {ex.GetType().Name}, but " + $"{typeof(TException).Name} or a derived type was expected."); 
		
		if (!allowDerivedTypes && ex.GetType() != typeof(TException)) 
			Assert.Fail($"Exception is of type {ex.GetType().Name}, but " + $"{typeof(TException).Name} was expected."); 
}

Discussion 讨论

Unit testing of dataflow meshes directly is doable, but somewhat awkward. If your mesh is a part of a larger component, then you may find that it’s easier to just unit test the larger component (implicitly testing the mesh). But if you’re developing a reusable custom block or mesh, then unit tests like the preceding ones should be used.

直接对数据流网格进行单元测试是可行的,但有点尴尬。如果您的网格是较大组件的一部分,那么您可能会发现只对较大组件进行单元测试(隐式测试网格)更容易。但是,如果您正在开发可重用的自定义块或网格,那么应该使用前面的单元测试。

7.5. Unit Testing System.Reactive Observables单元测试 System.Reactive可观察对象

Problem 问题

Part of your program is using IObservable<T>, and you need to find a way to unit test it.
程序的一部分使用了IObservable<T>,您需要找到一种方法对其进行单元测试。

Solution 解决方案

System.Reactive has a number of operators that produce sequences (e.g., Return) and other operators that can convert a reactive sequence into a regular collection or item (e.g., SingleAsync). You can use operators like Return to create stubs for observable dependencies, and operators like SingleAsync to test the output.

System.Reactive有许多产生序列的操作符(例如,Return)和其他可以将反应序列转换为常规集合或项的操作符(例如,SingleAsync)。你可以使用Return这样的操作符来创建可观察依赖的存根,使用SingleAsync这样的操作符来测试输出。

Consider the following code, which takes an HTTP service as a dependency and applies a timeout to the HTTP call:

考虑下面的代码,它将HTTP服务作为依赖项,并对HTTP调用应用超时:

public interface IHttpService 
{ 
	IObservable GetString(string url); 
} 

public class MyTimeoutClass 
{ 
	private readonly IHttpService _httpService; 
	
	public MyTimeoutClass(IHttpService httpService) 
	{ 
		_httpService = httpService; 
	} 
	
	public IObservable GetStringWithTimeout(string url) 
	{ 
		return _httpService.GetString(url) .Timeout(TimeSpan.FromSeconds(1)); 
	} 
}
The system under test is MyTimeoutClass, which consumes an observable dependency and produces an observable as output.The Return operator creates a cold sequence with a single element in it; you can use Return to build a simple stub. The SingleAsync operator returns a Task<T> that is completed when the next event arrives. SingleAsync can be used for simple unit tests like the following:
被测试的系统是MyTimeoutClass,它使用一个可观察对象依赖并产生一个可观察对象作为输出。Return操作符创建一个包含单个元素的冷序列;您可以使用Return构建一个简单的存根。SingleAsync操作符返回一个Task<T>将在下一个事件到达时完成。SingleAsync可以用于如下简单的单元测试:
class SuccessHttpServiceStub : IHttpService 
{ 
	public IObservable GetString(string url) 
	{ 
		return Observable.Return("stub"); 
	} 
} 

[TestMethod] 
public async Task MyTimeoutClass_SuccessfulGet_ReturnsResult() 
{ 
	var stub = new SuccessHttpServiceStub(); 
	var my = new MyTimeoutClass(stub); 
	var result = await my.GetStringWithTimeout("http://www.example.com/") .SingleAsync(); 
	
	Assert.AreEqual("stub", result); 
}

Another operator important in stub code is Throw, which returns an observable that ends with an error. The operator enables us to unit test the error case as well. The following example uses the ThrowsAsync helper from Recipe 7.2:

存根代码中另一个重要的操作符是Throw,它返回一个以错误结束的可观察对象。操作符还使我们能够对错误情况进行单元测试。下面的例子使用了Recipe 7.2中的ThrowsAsync helper:

private class FailureHttpServiceStub : IHttpService
{ 
	public IObservable GetString(string url) 
	{ 
		return Observable.Throw(new HttpRequestException()); 
	} 
} 

[TestMethod] 
public async Task MyTimeoutClass_FailedGet_PropagatesFailure() 
{ 
	var stub = new FailureHttpServiceStub(); 
	var my = new MyTimeoutClass(stub); 
	await ThrowsAsync(async () => 
	{
		await my.GetStringWithTimeout("http://www.example.com/") .SingleAsync(); 
	}); 
}

Discussion 讨论

Return and Throw are great for creating observable stubs, and SingleAsync is an easy way to test observables with async unit tests. They’re a good combination for simple observables, but they don’t hold up well once you start dealing with time. For example, if you wanted to test the timeout capability of MyTimeoutClass, the unit test would have to wait for that amount of time. That, however, would be a poor approach: it makes your unit tests unreliable by introducing a race condition, and it doesn’t scale well as you add more unit tests. Recipe 7.6 covers a special way that System.Reactive empowers you to stub out time itself.

Return和Throw对于创建可观察存根非常有用,SingleAsync是一种使用异步单元测试测试可观察对象的简单方法。对于简单的可观察对象,它们是一个很好的组合,但一旦你开始处理时间,它们就不适用了。例如,如果您想测试MyTimeoutClass的超时能力,单元测试将不得不等待一定的时间。然而,这将是一种糟糕的方法:它通过引入竞争条件使单元测试不可靠,并且随着添加更多单元测试,它也不能很好地扩展。配方7.6介绍了System.Reactive反应使你能够消灭时间本身。

7.6. Unit Testing System.Reactive Observables with Faked Scheduling用假调度对System.Reactive可观察对象进行单元测试

Problem 问题

You have an observable that is dependent on time, and want to write a unit test that is not dependent on time. Observables that depend on time include ones that use timeouts, windowing/buffering, and throttling/sampling. You want to unit test these but do not want your unit tests to have excessive runtimes.

你有一个依赖于时间的可观察对象,并且想要编写一个不依赖于时间的单元测试。依赖于时间的可观察对象包括使用超时、窗口/缓冲和节流/采样的可观察对象。您希望对它们进行单元测试,但不希望单元测试具有过多的运行时。

Solution 解决方案

It’s certainly possible to put delays in your unit tests; however, there are two problems with that approach: 1) the unit tests take a long time to run, and 2) there are race conditions because the unit tests all run at the same time, making timing unpredictable.

The System.Reactive (Rx) library was designed with testing in mind; in fact, the Rx library itself is extensively unit tested. To enable thorough unit testing, Rx introduced a concept called a scheduler, and every Rx operator that deals with time is implemented using this abstract scheduler.

To make your observables testable, you need to allow your caller to specify the scheduler. For example, you can take the MyTimeoutClass from Recipe 7.5 and add a scheduler:

在单元测试中添加延迟当然是可能的;然而,这种方法有两个问题:1)单元测试需要很长时间来运行,2)存在竞争条件,因为单元测试都在同时运行,使得时间不可预测。

这个系统。响应式(Rx)库在设计时就考虑了测试;事实上,Rx库本身进行了广泛的单元测试。为了支持彻底的单元测试,Rx引入了一个称为调度器的概念,处理时间的每个Rx操作符都使用这个抽象调度器实现。

要使可观察对象可测试,需要允许调用方指定调度器。例如,你可以从Recipe 7.5中获取MyTimeoutClass并添加一个调度器:

public interface IHttpService 
{ 
	IObservable GetString(string url); 
} 

public class MyTimeoutClass 
{ 
	private readonly IHttpService _httpService; 
	
	public MyTimeoutClass(IHttpService httpService) 
	{ 
		_httpService = httpService; 
	} 
	
	public IObservable GetStringWithTimeout(string url, IScheduler scheduler = null) 
	{ 
		return _httpService.GetString(url) .Timeout(TimeSpan.FromSeconds(1), scheduler ?? Scheduler.Default); 
	} 
}

Next, you can modify your HTTP service stub so that it also understands scheduling, then introduce a variable delay:

接下来,你可以修改你的HTTP服务存根,这样它也能理解调度,然后引入一个可变延迟:

private class SuccessHttpServiceStub : IHttpService 
{ 
	public IScheduler Scheduler 
	{ 
		get; 
		set; 
	} 
	
	public TimeSpan Delay 
	{ 
		get; 
		set; 
	} 
	
	public IObservable GetString(string url) 
	{ 
		return Observable.Return("stub") .Delay(Delay, Scheduler);
	} 
}	  

Now you can go ahead and use TestScheduler, a type included in the System.Reactive library. TestScheduler gives you powerful control over (virtual) time.

现在您可以继续使用TestScheduler,这是系统中包含的一种System.Reactive library. TestScheduler为您提供了对(虚拟)时间的强大控制。

TestScheduler is in a separate NuGet package from the rest of System.Reactive; you’ll need to install the Microsoft.Reactive.Testing NuGet package.

TestScheduler在一个独立的NuGet包中,与System.Reactive的其余部分分开;你需要安装Microsoft.Reactive.Testing NuGet包。

TestScheduler gives you complete control over time, but you often just need to set up your code and then call TestScheduler.Start. Start will virtually advance time until everything is done. A simple success test case could look like the following:

TestScheduler为您提供了对时间的完全控制,但您通常只需要设置代码,然后调用TestScheduler. Start. Start实际上会提前完成所有事情。一个简单的成功测试用例可能如下所示:

[TestMethod] 
public void MyTimeoutClass_SuccessfulGetShortDelay_ReturnsResult() 
{ 
	var scheduler = new TestScheduler(); 
	var stub = new SuccessHttpServiceStub 
	{ 
		Scheduler = scheduler, 
		Delay = TimeSpan.FromSeconds(0.5), 
	}; 
		
	var my = new MyTimeoutClass(stub); 
	string result = null; 
	
	my.GetStringWithTimeout("http://www.example.com/", scheduler) .Subscribe(r => { result = r; }); 
	
	scheduler.Start(); 
	Assert.AreEqual("stub", result); 
}

The code simulates a network delay of half a second. It’s important to note that this unit test does not take half a second to run; on my machine, it takes about 70 milliseconds. The half-second delay only exists in virtual time. The other notable difference in this unit test is that it isn’t asynchronous; since you’re using TestScheduler, all your tests can complete immediately.Now that everything is using test schedulers, it’s easy to test timeout situations:

该代码模拟了半秒的网络延迟。需要注意的是,这个单元测试不需要半秒钟就能运行;在我的机器上,大约需要70毫秒。半秒延迟只存在于虚拟时间中。这个单元测试的另一个显著区别是,它不是异步的;由于使用的是TestScheduler,所以所有的测试都可以立即完成。现在一切都在使用测试调度器,很容易测试超时情况:

[TestMethod] 
public void MyTimeoutClass_SuccessfulGetLongDelay_ThrowsTimeoutException() 
{ 
	var scheduler = new TestScheduler(); 
	var stub = new SuccessHttpServiceStub 
	{ 
		Scheduler = scheduler, 
		Delay = TimeSpan.FromSeconds(1.5), 
	}; 
	
	var my = new MyTimeoutClass(stub); 
	Exception result = null; 
	
	my.GetStringWithTimeout("http://www.example.com/", scheduler) .Subscribe(_ => Assert.Fail("Received value"), ex => { result = ex; }); 
	scheduler.Start(); 
	Assert.IsInstanceOfType(result, typeof(TimeoutException)); 
}

Once again, the preceding unit test does not take 1 second (or 1.5 seconds) to run; it executes immediately using virtual time.

同样,前面的单元测试不会花费1秒(或1.5秒)来运行;它使用虚拟时间立即执行。

Discussion 讨论

In this recipe we’ve just scratched the surface on System.Reactive schedulers and virtual time. I recommend that you start unit testing when you start writing System.Reactive code; as your code grows more and more complex, you can rest assured that Microsoft.Reactive.Testing is capable of handling it.

在这个食谱中,我们只是触及了系统的表面。响应式调度器和虚拟时间。我建议您在开始编写System时就开始进行单元测试。反应性代码;当你的代码变得越来越复杂时,你可以放心,Microsoft.Reactive.Testing有能力处理它。

TestScheduler also has AdvanceTo and AdvanceBy methods, which enable you to gradually step through virtual time. These may be useful in some situations, but you should strive to have your unit tests only test one thing. To test a timeout, you could write a single unit test that partially advanced the TestScheduler and ensured that the timeout didn’t happen early, and then advanced the TestScheduler past the timeout value and ensured that the timeout did happen. However, I prefer to run separate unit tests as much as possible; for example, one unit test ensuring that the timeout didn’t happen early, and a different unit test ensuring that the timeout did happen later.

TestScheduler也有AdvanceTo和AdvanceBy方法,它们使您能够逐步通过虚拟时间。这些在某些情况下可能是有用的,但是您应该努力让您的单元测试只测试一个东西。要测试超时,您可以编写一个单元测试,它部分地推进TestScheduler并确保超时不会提前发生,然后推进TestScheduler超过超时值并确保超时确实发生。然而,我更喜欢尽可能地运行单独的单元测试;例如,一个单元测试确保超时没有提前发生,另一个单元测试确保超时稍后发生。

01-24 08:26