Как выполнить модульное тестирование этой библиотеки? - PullRequest
2 голосов
/ 26 октября 2010

У меня есть внешняя библиотека, в которой есть метод, который выполняет долгосрочную задачу в фоновом потоке.Когда это сделано, оно запускает событие Completed в потоке, который запускает метод (обычно в потоке пользовательского интерфейса).Выглядит это так:

public class Foo
{
    public delegate void CompletedEventHandler(object sender, EventArgs e);
    public event CompletedEventHandler Completed;

    public void LongRunningTask()
    {
        BackgroundWorker bw = new BackgroundWorker();
        bw.DoWork += new DoWorkEventHandler(bw_DoWork);
        bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
        bw.RunWorkerAsync();
    }

    void bw_DoWork(object sender, DoWorkEventArgs e)
    {
        Thread.Sleep(5000);
    }

    void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (Completed != null)
            Completed(this, EventArgs.Empty);
    }
}

Код, который вызывает эту библиотеку, выглядит следующим образом:

private void button1_Click(object sender, EventArgs e)
{
    Foo b = new Foo();
    b.Completed += new Foo.CompletedEventHandler(b_Completed);
    b.LongRunningTask();

    Debug.WriteLine("It's all done");    
}

void b_Completed(object sender, EventArgs e)
{
    // do stuff
}

Как мне протестировать вызов .LongRunningTask, если он возвращает данныесобытие?

Ответы [ 4 ]

2 голосов
/ 05 ноября 2010

Я не уверен, правильно ли я понял.Вы хотите проверить внешнюю библиотеку, если она запускает событие?Или вы хотите проверить, что вы делаете что-то особенно, если событие запускается?

Если это последнее, я бы использовал для этого макет.Проблема, однако, в том, что ваш код кажется трудным для тестирования, потому что вы делаете логические вещи в пользовательском интерфейсе.Попробуйте написать «пассивный» вид, и пусть ведущий сделает волшебство.Например, используя шаблон «Представление представления модели» http://msdn.microsoft.com/en-us/magazine/cc188690.aspx

В таком случае все будет выглядеть следующим образом.

Модель

public class Model : IModel
{
    public event EventHandler<SampleEventArgs> Completed;

    public void LongRunningTask()
    {
        BackgroundWorker bw = new BackgroundWorker();
        bw.DoWork += this.bw_DoWork;
        bw.RunWorkerCompleted += this.bw_RunWorkerCompleted;
        bw.RunWorkerAsync();

    }

    private void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (this.Completed != null)
        {
            this.Completed(this, new SampleEventArgs {Data = "Test"});
        }
    }

    private void bw_DoWork(object sender, DoWorkEventArgs e)
    {
        System.Threading.Thread.Sleep(5000);
    }
}

Представление

    public Form1()
    {
        InitializeComponent();
    }

    public event EventHandler Button1Clicked;

    public void Update(string data)
    {
        this.label1.Text = data;
    }

    private void Button1Click(object sender, EventArgs e)
    {
        if (this.Button1Clicked != null)
        {
            this.Button1Clicked(this, EventArgs.Empty);
        }
    }

Ведущий

public class Presenter
{
    private readonly IForm1 form1;
    private readonly IModel model;

    public Presenter(IForm1 form1, IModel model)
    {
        this.form1 = form1;
        this.model = model;

        this.form1.Button1Clicked += this.Form1Button1Clicked;
        this.model.Completed += this.ModelCompleted;
    }

    private void ModelCompleted(object sender, SampleEventArgs e)
    {
        this.form1.Update(e.Data);
    }

    private void Form1Button1Clicked(object sender, EventArgs e)
    {
        this.model.LongRunningTask();
    }
}

Где-то, где вы его собираете (например, в классе Программы)

var form = new Form1();
var model = new Model();
var presenter = new Presenter(form, model);
Application.Run(form);

И тогда вы можете легко просто протестировать докладчика в модульном тесте.Часть в графическом интерфейсе теперь достаточно мала, чтобы ее нельзя было проверить.

Возможный тест может выглядеть следующим образом

    [Test]
    public void Test()
    {
        var form1Mock = new Mock<IForm1>();
        var modelMock = new Mock<IModel>();

        var presenter = new Presenter(form1Mock.Object, modelMock.Object);

        modelMock.Setup(m => m.LongRunningTask()).Raises(m => m.Completed += null, new SampleEventArgs() { Data = "Some Data" });

        form1Mock.Raise(f => f.Button1Clicked += null, EventArgs.Empty);

        form1Mock.Verify(f => f.Update("Some Data"));
    } 
2 голосов
/ 26 октября 2010

Ну, я считаю, BackgroundWorker использует текущий SynchronizationContext. Вы могли бы потенциально реализовать свой собственный подкласс SynchronizationContext, чтобы предоставить вам больший контроль (возможно, даже выполнение кода в том же потоке, хотя это нарушит все, что зависит от его выполнения в другом потоке) и вызовет SetSynchronizationContext перед запуском теста.

Вам нужно подписаться на событие в вашем тесте, а затем проверить, был ли вызван ваш обработчик. (Лямбда-выражения хороши для этого.)

Например, предположим, что у вас есть SynchronizationContext, который позволяет вам выполнять всю работу только тогда, когда вы этого хотите, и сообщать вам, когда это будет сделано, ваш тест может:

  • Установить контекст синхронизации
  • Создать компонент
  • Подписаться на обработчик с помощью лямбды, которая устанавливает локальную переменную
  • Звоните LongRunningTask()
  • Убедитесь, что локальная переменная еще не установлена ​​
  • Заставить контекст синхронизации выполнять всю свою работу ... дождаться его завершения (с таймаутом)
  • Убедитесь, что локальная переменная установлена ​​

Это все немного противно, по общему признанию. Если вы сможете просто протестировать работу, которую она выполняет, синхронно, это будет намного проще.

1 голос
/ 31 октября 2010

Для тестирования асинхронного кода я использую аналогичный помощник:

public class AsyncTestHelper
{
    public delegate bool TestDelegate();

    public static bool AssertOrTimeout(TestDelegate predicate, TimeSpan timeout)
    {
        var start = DateTime.Now;
        var now = DateTime.Now;
        bool result = false;
        while (!result && (now - start) <= timeout)
        {
            Thread.Sleep(50);
            now = DateTime.Now;
            result = predicate.Invoke();
        }
        return result;
    }
}

В тестовом методе вызовите что-то вроде этого:

Assert.IsTrue(AsyncTestHelper.AssertOrTimeout(() => changeThisVarInCodeRegisteredToCompletedEvent, TimeSpan.FromMilliseconds(500)));
1 голос
/ 27 октября 2010

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

static class FooExtensions
{
    public static SomeData WaitOn(this Foo foo, Action<Foo> action)
    {
        SomeData result = null;
        var wait = new AutoResetEvent(false);

        foo.Completed += (s, e) =>
        {
            result = e.Data; // I assume this is how you get the data?
            wait.Set();
        };

        action(foo);
        if (!wait.WaitOne(5000)) // or whatever would be a good timeout
        {
            throw new TimeoutException();
        }
        return result;
    }
}

public void TestMethod()
{
    var foo = new Foo();
    SomeData data = foo.WaitOn(f => f.LongRunningTask());
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...