【问题标题】:How to unit test this library?如何对这个库进行单元测试?
【发布时间】:2010-10-26 18:48:14
【问题描述】:

我有一个外部库,它有一个在后台线程上执行长时间运行任务的方法。完成后,它会在启动该方法的线程(通常是 UI 线程)上触发 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 在事件中返回数据,我该如何对它的调用进行单元测试?

【问题讨论】:

    标签: c# multithreading unit-testing events


    【解决方案1】:

    我不确定我是否做对了。如果触发事件,您想检查外部库吗?或者你想检查你是否做了一些事情,特别是当事件被触发时?

    如果是后者,我会为此使用模拟。但问题是,您的代码似乎很难测试,因为您在用户界面中执行逻辑操作。尝试编写“被动”视图,并让演示者发挥作用。例如通过使用模型视图演示者模式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();
        }
    }
    

    在某个地方组装它(例如在 Program 类中)

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

    然后您可以轻松地在单元测试中测试演示者。 gui 中的部分现在足够小,无法进行测试。

    可能的测试可能如下所示

        [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】:

      好吧,我相信BackgroundWorker 使用当前的SynchronizationContext。您可能会实现自己的 SynchronizationContext 子类以允许您进行更多控制(甚至可能在同一线程上运行代码,尽管这会破坏 依赖 在不同线程中运行的任何内容)并调用SetSynchronizationContext 在运行测试之前。

      您需要订阅测试中的事件,然后检查您的处理程序是否被调用。 (Lambda 表达式对此很有用。)

      例如,假设您有一个 SynchronizationContext,它允许您仅在需要时运行所有工作,并告诉您何时完成,您的测试可能:

      • 设置同步上下文
      • 创建组件
      • 使用设置局部变量的 lambda 订阅处理程序
      • 致电LongRunningTask()
      • 验证尚未设置局部变量
      • 让同步上下文完成所有工作...等到它完成(有超时)
      • 验证现在是否已设置局部变量

      诚然,这有点令人讨厌。如果你可以同步地测试它正在做的工作,那会容易得多。

      【讨论】:

        【解决方案3】:

        您可以创建一个扩展方法来帮助将其转换为同步调用。您可以进行调整,例如使其更通用并传入超时变量,但至少它会使单元测试更易于编写。

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

        【讨论】:

          【解决方案4】:

          为了测试异步代码,我使用了一个类似的助手:

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

          【讨论】:

            猜你喜欢
            • 2022-11-12
            • 2018-03-30
            • 2018-10-29
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多