4

私は TPL を使用していますが、TPL を使用するコードの単体テストが難しいと感じています。

問題が発生する可能性があると感じているため、ラッパーを導入しないようにしています。

TPL でプロセッサ アフィニティを設定できることは理解していますが、スレッドの最大値を (おそらくアプリ ドメインごとに) 設定することをお勧めします。したがって、スレッドの最大値を 1 に設定すると、TPL は使用されたスレッドを強制的に使用することになります。

どう思いますか?これは可能ですか (私はそうではないと確信しています)、それは可能であるべきですか?

編集:ここに例があります

public class Foo
{
    public Foo( )
    {
        Task.Factory.StartNew( () => somethingLong( ) )
            .ContinueWith( a => Bar = 1 ) ;
    }
}

[Test] public void Foo_should_set_Bar_to_1( )
{
    Assert.Equal(1, new Foo( ).Bar ) ;
}

遅延を導入しない限り、テストはおそらく合格しません。Task.MaximumThreads=1TPLがシリアルに実行されるようにしたいと思います。

4

3 に答える 3

4

TaskSchedulerから派生した独自のクラスを作成しTaskScheduler、それを に渡すことができますTaskFactory。これで、作成したオブジェクトをそのスケジューラTaskに対して実行できます。

1 つのスレッドを使用するように設定する必要はありません。

次に、アサートの直前に、それを呼び出しますDispose()。内部的には、次のように記述しTaskSchedulerます:-

public void Dispose()
{
    if (tasks != null)
    {
        tasks.CompleteAdding();

        foreach (var thread in threads) thread.Join();

        tasks.Dispose();
        tasks = null;
    }
}

これにより、すべてのタスクが実行されたことが保証されます。これで、アサートを進めることができます。

ContinueWith(...)物事が起こっているときに進行状況を確認したい場合は、タスクの実行後にアサーションを追加するために使用することもできます。

于 2010-09-02T01:59:46.283 に答える
2

実際、これは TPL に関する問題というよりも、ラムダの重いコードのテスト可能性に関する問題です。Hightechrider の提案は良いものですが、基本的に、テストはコードと同じくらい TPL をテストしています。最初のタスクが終了し、ContinueWith が次のタスクを開始するときにそれをテストする必要はありません。

ラムダ内のコードが非常に大きい場合、明確に定義されたパラメーターを使用して、よりテストしやすいメソッドにコードを引き出すと、読みやすく、よりテストしやすいコードになる可能性があります。その周りに単体テストを書くことができます。可能であれば、単体テストから並列処理を制限または削除しようとします。

スケジューラのアプローチが機能するかどうかを確認したかったと言いました。http://code.msdn.microsoft.com/ParExtSamplesから変更された StaTaskScheduler を使用した実装を次に示します。

    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using Xunit;

    namespace Example
    {
      public class Foo
      {
        private TaskScheduler _scheduler;

    public int Bar { get; set; }

    private void SomethingLong()
    {
      Thread.SpinWait(10000);
    }

    public Foo()
      : this(TaskScheduler.Default)
    {
    }

    public Foo(TaskScheduler scheduler)
    {
      _scheduler = scheduler;
    }

    public void DoWork()
    {
      var factory = new TaskFactory(_scheduler);

      factory.StartNew(() => SomethingLong())
      .ContinueWith(a => Bar = 1, _scheduler);
    }
  }

  public class FooTests
  {
    [Fact]
    public void Foo_should_set_Bar_to_1()
    {
      var sch = new StaTaskScheduler(3);
      var target = new Foo(sch);
      target.DoWork();

      sch.Dispose();
      Assert.Equal(1, target.Bar);
    }
  }

  public sealed class StaTaskScheduler : TaskScheduler, IDisposable
  {
    /// <summary>Stores the queued tasks to be executed by our pool of STA threads.</summary>
    private BlockingCollection<Task> _tasks;
    /// <summary>The STA threads used by the scheduler.</summary>
    private readonly List<Thread> _threads;

    /// <summary>Initializes a new instance of the StaTaskScheduler class with the specified concurrency level.</summary>
    /// <param name="numberOfThreads">The number of threads that should be created and used by this scheduler.</param>
    public StaTaskScheduler(int numberOfThreads)
    {
      // Validate arguments
      if (numberOfThreads < 1) throw new ArgumentOutOfRangeException("concurrencyLevel");

      // Initialize the tasks collection
      _tasks = new BlockingCollection<Task>();

      // Create the threads to be used by this scheduler
      _threads = Enumerable.Range(0, numberOfThreads).Select(i =>
      {
        var thread = new Thread(() =>
        {
          // Continually get the next task and try to execute it.
          // This will continue until the scheduler is disposed and no more tasks remain.
          foreach (var t in _tasks.GetConsumingEnumerable())
          {
            TryExecuteTask(t);
          }
        });
        thread.IsBackground = true;
        // NO STA REQUIREMENT!
        // thread.SetApartmentState(ApartmentState.STA);
        return thread;
      }).ToList();

      // Start all of the threads
      _threads.ForEach(t => t.Start());
    }

    /// <summary>Queues a Task to be executed by this scheduler.</summary>
    /// <param name="task">The task to be executed.</param>
    protected override void QueueTask(Task task)
    {
      // Push it into the blocking collection of tasks
      _tasks.Add(task);
    }

    /// <summary>Provides a list of the scheduled tasks for the debugger to consume.</summary>
    /// <returns>An enumerable of all tasks currently scheduled.</returns>
    protected override IEnumerable<Task> GetScheduledTasks()
    {
      // Serialize the contents of the blocking collection of tasks for the debugger
      return _tasks.ToArray();
    }

    /// <summary>Determines whether a Task may be inlined.</summary>
    /// <param name="task">The task to be executed.</param>
    /// <param name="taskWasPreviouslyQueued">Whether the task was previously queued.</param>
    /// <returns>true if the task was successfully inlined; otherwise, false.</returns>
    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
      // Try to inline if the current thread is STA
      return
      Thread.CurrentThread.GetApartmentState() == ApartmentState.STA &&
      TryExecuteTask(task);
    }

    /// <summary>Gets the maximum concurrency level supported by this scheduler.</summary>
    public override int MaximumConcurrencyLevel
    {
      get { return _threads.Count; }
    }

    /// <summary>
    /// Cleans up the scheduler by indicating that no more tasks will be queued.
    /// This method blocks until all threads successfully shutdown.
    /// </summary>
    public void Dispose()
    {
      if (_tasks != null)
      {
        // Indicate that no new tasks will be coming in
        _tasks.CompleteAdding();

        // Wait for all threads to finish processing tasks
        foreach (var thread in _threads) thread.Join();

        // Cleanup
        _tasks.Dispose();
        _tasks = null;
      }
    }
  }
}
于 2010-09-02T07:21:54.627 に答える
1

コンストラクターをオーバーロードする必要をなくしたい場合は、単体テスト コードを Task.Factory.ContinueWhenAll(...) でラップできます。

public class Foo
{
    public Foo( )
    {
        Task.Factory.StartNew( () => somethingLong( ) )
            .ContinueWith( a => Bar = 1 ) ;
    }
}

[Test] public void Foo_should_set_Bar_to_1( )
{
    Foo foo;
    Task.Factory.ContinueWhenAll(
        new [] {
            new Task(() => {
                foo = new Foo();
            })
        },
        asserts => { 
            Assert.Equal(1, foo.Bar ) ;
        }
    ).Wait;
}

このアプローチに関するフィードバックをお待ちしております。

于 2010-10-11T03:04:44.423 に答える