방프리

23.11.19 C# 동시성 프로그래밍 (테스트) 본문

C#/동시성 처리

23.11.19 C# 동시성 프로그래밍 (테스트)

방프리 2023. 11. 19. 22:09

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