방프리
23.11.19 C# 동시성 프로그래밍 (테스트) 본문
1. async 메서드의 단위 테스트
//단위 테스트 프레임워크가 async Task를 지원한다면
[TestMethod]
public async Task MyMethodAsync_ReturnsFalse()
{
var objectUnderTest = ...;
bool result = await objectUnderTest.MyMethodAsync();
Assert.IsFalse(result);
}
//async Task를 지원하지 않는다면, Nito.AsyncEx Nuget 패키지 사용해볼 것
[TestMethod]
public void MyMethodAsync_ReturnsFalse()
{
AsyncContext.Run(async () =>
{
var objectUnderTest = ...;
bool result = await objectUnderTest.MyMethodAsync();
Assert.IsFalse(result);
});
}
2. async 메서드의 실패 사례를 단위 테스트
- 실패 사례를 테스트 진행 시 항상 예외를 호출하는 것이 아닌 특정 코드에서 예외를 발생시켜 확인하도록 해야한다.
- 그렇기 때문에 항상 예외를 발생시키는 ExpectedException은 좋은 테스트 코드가 아니다.
//ThrowAsync를 지원할 때
[Fact]
public async Task Divide_WhenDenominatorIsZero_ThrowDivideByZero()
{
await Assert.ThrowsAsync<DivideByZeroException>(async () =>
{
await MyClass.DivideAsync(4, 0);
});
}
//ThrowAsync를 지원하지 않는 프레임워크라면
public static async Task<TException> ThrowsAsync<TException>(Func<Task> 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;
}
}
3. async void 메서드의 단위 테스트
- 이 케이스를 테스트해야하나? 해당 케이스는 테스트가 아니라 코드부터 변경해야한다.
되도록이면 async void가 아닌 async Task. 형태로 바꾸어야 한다. 부득이하게 사용해야 한다면
Nito.AsyncEx를 사용해보자
[TestMethod]
public void MyMethodAsync_DoesNotThrow()
{
AsyncContext.Run(() =>
{
var objectUnderTest = new Sut(); //...;
objectUnderTest.MyVoidMethodAsync();
}
}
4. 데이터 흐름 메시의 단위 테스트
- 데이터 흐름 메시를 단위 테스트할 때 예외가 일어난 블록은 다음 블록으로 전파할 때마다 새로운 AggregateException
으로 감싼다.
//헬퍼 메서드를 통해 예외의 데이터를 삭제하고 사용자 지정 블록을 통해 예외 전파
[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<InvalidOperationException>(O
ex.Flatten().InnerException, false);
}
}
public static void AssertExceptionIs<TException>(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 == false && ex.GetType() != typeof(TException))
{
Assert.Fail($"Exception is of type {ex.GetType().Name}, but " +
$"{typeof(TException).Name} was expected.");
}
}
5. System.Reactive 옵저버블의 단위 테스트
//Return을 통한 시퀸스를 만들어 내는 연산자
public interface IHttpService
{
IObservable<string> GetString(string url);
}
public class MyTimeoutClass
{
private readonly IHttpService _httpService;
public MyTimeoutClass(IHttpService httpService)
{
_httpService = httpService;
}
public IObservable<string> GetStringWithTimeout(string url)
{
return _httpService.GetString(url)
.Timeout(TimeSpan.FromSecond(1));
}
}
//SingleAsync 연산자를 통한 Task 반환
class SuccessHttpServiceStub : IHttpService
{
public IObservable<string> 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);
}
//오류를 반환하는 Observable
private class FailureHttpServiceStub : IHttpService
{
public IObservable<string> GetString(string url)
{
return Observable.Throw<string>(new HttpRequestException());
}
}
[TestMethod]
public async Task MyTimeoutClass_FailedGet_PropagatesFailure()
{
var stub = new FailureHttpServiceStub();
var my = new MyTimeoutClass(stub);
await ThrowsAsync<HttpRequestException>(async () =>
{
await my.GetStringWithTimeout("Http://www.example.com/")
.SingleAsync();
}
}
6. 시간과 관련이 있는 System.Reactive 옵저버블의 단위 테스트
- Timeout, Window, Buffer, Throttle 과 Sample을 사용하는 옵저버블을 시간과 연관하여 테스트를 진행할 때 너무 많은 작업이 없도록 하고 싶을 때
//TimeScheduler를 사용한 테스트
public interface IHttpService
{
IObservable<string> GetString(string url);
}
public class MyTimeoutClass
{
private readonly IHttpService _httpService;
public MyTimeoutClass(IHttpService httpService)
{
_httpService = httpService;
}
public IObservable<string> GetStringWithTimeout(string url,
IScheduler scheduler = null)
{
return _httpService.GetString(url)
.Timeout(TimeSpan.FromSeconds(1), scheduler ?? Scheduler.Default);
}
}
private class SuccessHttpServiceStub : IHttpService
{
public IScheduler Scheduler { get; set; }
public TimeSpan Delay { get; set; }
public IObservable<string> GetString(string url)
{
return Observable.Return("stub")
.Delay(Delay, Scheduler);
}
}
위의 코드를 통해 다음의 테스트가 가능해진다.
[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);
}
'C# > 동시성 처리' 카테고리의 다른 글
24.05.21 C# 동시성 프로그래밍 (컬렉션) (0) | 2024.05.21 |
---|---|
24.05.09 C# 동시성 프로그래밍 (상호운용) (0) | 2024.05.09 |
23.11.12 C# 동시성 프로그래밍 (Reactive) (1) | 2023.11.12 |
23.11.11 C# 동시성 프로그래밍 (TPL) (0) | 2023.11.11 |
23.11.08 C# 동시성 프로그래밍 (병렬 처리) (0) | 2023.11.08 |
Comments