Модульное тестирование того, что устаревший код определенным образом реагирует на ThreadAbortException

У меня есть часть устаревшего кода, который я хочу протестировать. Вот репродукция основных моментов:

public class LegacyUnit
{
    private readonly ICollaborator collaborator;

    public LegacyUnit(ICollaborator collaborator) 
    {
        this.collaborator = collaborator;
    }

    public object GetStuff(HttpContextBase context, string input)
    {
        try 
        {
            if (input == "")
            {
                context.Response.End();
            }

            collaborator.DoOtherStuff();

            return "Done!";
        }
        catch (ThreadAbortException) 
        { }

        return null;
    }
}

У этого устаревшего устройства есть некоторые проблемы, но пока я просто пытаюсь его протестировать. В частности, я хочу проверить, что collaborator.DoOtherStuff не вызывается, если Response.End() вызывает ThreadAbort.

Проблема: как вызвать такое исключение?

Я прочитал этот вопрос и ответы на него на ThreadAbortException и понял, что он особенный. Однако из этих сообщений я не вижу, как справиться с этим в модульных тестах.

Вот моя попытка:

[Test]
public void DoesNotCallCollaboratorOnThreadAbort()
{
    var testResponseMock = new Mock<HttpResponseBase>();
    var testContextMock = new Mock<HttpContextBase>();
    var collaboratorMock = new Mock<ICollaborator>();

    testContextMock.Setup(x => x.Response).Returns(testResponseMock.Object);
    testResponseMock.Setup(x => x.End()).Throws<ThreadAbortException>(); // Compile error

    var unit = new LegacyUnit(collaboratorMock.Object);
    unit.GetStuff(testContextMock.Object, "");

    collaboratorMock.Verify(c => c.DoOtherStuff(), Times.Never);
}

Очевидно, компилятор жалуется: ThreadAbortException не имеет доступного конструктора. Кроме того, это sealed (вероятно, по уважительным причинам), поэтому создание «тестируемого» подкласса не сработает.

Каков правильный способ получить такой код для тестирования? Возможно ли это вообще, или LegacyUnit слишком недружелюбен к тестам?


Полное минимальное воспроизведение (пустая библиотека классов .NET 4.5 с NUnit 2.6.4 и Moq 4.5.9):

public interface ICollaborator
{
    void DoOtherStuff();
}

public class LegacyUnit
{
    private readonly ICollaborator collaborator;

    public LegacyUnit(ICollaborator collaborator)
    {
        this.collaborator = collaborator;
    }

    public object GetStuff(HttpContextBase context, string input)
    {
        try
        {
            if (input == "") context.Response.End();
            collaborator.DoOtherStuff();
            return "Done!";
        }
        catch (ThreadAbortException)
        { }

        return null;
    }
}

[TestFixture]
public class LegacyUnitTests
{
    [Test]
    public void DoesNotCallCollaboratorOnThreadAbort()
    {
        var testResponseMock = new Mock<HttpResponseBase>();
        var testContextMock = new Mock<HttpContextBase>();
        var collaboratorMock = new Mock<ICollaborator>();

        testContextMock.Setup(x => x.Response).Returns(testResponseMock.Object);
        testResponseMock.Setup(x => x.End()).Throws<ThreadAbortException>(); // Compile error here

        var unit = new LegacyUnit(collaboratorMock.Object);
        unit.GetStuff(testContextMock.Object, "");

        collaboratorMock.Verify(c => c.DoOtherStuff(), Times.Never);
    }
}

person Jeroen    schedule 15.06.2016    source источник
comment
DoOtherStuff также не выполняется, когда End() генерирует исключение любого типа.   -  person Jehof    schedule 15.06.2016
comment
Истинный. В моем реальном случае вокруг других типов исключений происходит множество других вещей. Пришлось пожертвовать некоторой полнотой, чтобы сделать репродукцию минимально возможной.   -  person Jeroen    schedule 15.06.2016


Ответы (1)


ThreadAbortException вызывается в целевом потоке вызовом Abort в нем. Вы можете создать поток для запуска теста и вызвать Abort в своем макете testResponseMock.End, например.

testContextMock.Setup(x => x.Response).Returns(testResponseMock.Object);

var unit = new LegacyUnit(collaboratorMock.Object);
var thread = new Thread(() => unit.GetStuff(testContextMock.Object, ""));

testResponseMock.Setup(x => x.End()).Callback(() => { Thread.CurrentThread.Abort(); });

thread.Start();
thread.Join();

collaboratorMock.Verify(c => c.DoOtherStuff(), Times.Never);
person Lee    schedule 15.06.2016
comment
Круто, кажется, это хорошо работает. Одна вещь: мне пришлось заменить .Do(...) на .Callback(...) для моей версии Moq. Вы случайно не знаете, откуда это несоответствие? - person Jeroen; 15.06.2016
comment
@Jeroen - Извините, я не заметил тег moq и подумал, что вы используете rhinomock. Я обновил ответ. - person Lee; 15.06.2016