.NET Core と .NET Standard での単体テストのベスト プラクティスUnit testing best practices with .NET Core and .NET Standard

単体テストの記述には多大な利点があります。回帰の防止に役立ち、ドキュメントを提供して、優れた設計を容易に行うことができます。There are numerous benefits to writing unit tests; they help with regression, provide documentation, and facilitate good design. ただし、読みにくくて不安定な単体テストは、コード ベースに打撃を与える可能性があります。However, hard to read and brittle unit tests can wreak havoc on your code base. この記事では、.NET Core プロジェクトと .NET Standard プロジェクトの単体テストの設計に関するベスト プラクティスについて説明します。This article describes some best practices regarding unit test design for your .NET Core and .NET Standard projects.

このガイドでは、テストの回復性とわかりやすさを維持するよう単体テストを記述する際のいくつかのベスト プラクティスについて説明します。In this guide, you'll learn some best practices when writing unit tests to keep your tests resilient and easy to understand.

著者: John Reese、協力者: Roy OsheroveBy John Reese with special thanks to Roy Osherove

単体テストを記述する理由Why unit test?

機能テストの実行時間を短縮Less time performing functional tests

機能テストはコストがかかります。Functional tests are expensive. 通常、これらのテストでは、アプリケーションを開き、想定される動作を検証するためにお客様 (または他のだれか) が従う必要のある一連の手順を実行します。They typically involve opening up the application and performing a series of steps that you (or someone else), must follow in order to validate the expected behavior. テスト担当者は、これらの手順を必ずしもが把握しているとは限りません。つまり、テストを実行するために、その領域に精通している他者に支援を求める必要があります。These steps may not always be known to the tester, which means they will have to reach out to someone more knowledgeable in the area in order to carry out the test. テスト自体は、小さな変更の場合は数秒かかり、大きな変更の場合は数分かかる可能性があります。Testing itself could take seconds for trivial changes, or minutes for larger changes. 最後に、システムで変更を行うたびにこのプロセスを繰り返す必要があります。Lastly, this process must be repeated for every change that you make in the system.

一方、単体テストは数ミリ秒しかかからず、ボタンを押すだけで実行でき、概してシステムの知識は必ずしも必要ありません。Unit tests, on the other hand, take milliseconds, can be run at the press of a button and do not necessarily require any knowledge of the system at large. テストに合格するか失敗するかは、個人ではなくテスト ランナーの責任となります。Whether or not the test passes or fails is up to the test runner, not the individual.

回帰の防止Protection against regression

回帰問題は、アプリケーションに変更が行われると発生する欠陥です。Regression defects are defects that are introduced when a change is made to the application. 一般的にテスト担当者は、新しい機能をテストするだけでなく、以前に実装されていた機能が今も想定どおりに機能していることを確認するために、事前に存在していた機能もテストします。It is common for testers to not only test their new feature but also features that existed beforehand in order to verify that previously implemented features still function as expected.

単体テストでは、ビルドを行うたびに、さらにはコード行を変更するたびに、テストのスイート全体を再実行できます。With unit testing, it's possible to rerun your entire suite of tests after every build or even after you change a line of code. これにより、新しいコードが既存の機能を損なわないことを確信できます。Giving you confidence that your new code does not break existing functionality.

実行可能なドキュメントExecutable documentation

特定の入力によって、特定のメソッドが何を実行するか、またはそのメソッドがどのように動作するかは、必ずしも明らかではありません。It may not always be obvious what a particular method does or how it behaves given a certain input. 空の文字列や Null を渡した場合にこのメソッドがどのように動作するか、自分自身わからないことがYou may ask yourself: How does this method behave if I pass it a blank string? あります。Null?

適切な名前の単体テストのスイートがある場合、各テストは、特定の入力に対して想定される出力を明確に説明できる必要があります。When you have a suite of well-named unit tests, each test should be able to clearly explain the expected output for a given input. さらに、実際に動作することを確認できる必要があります。In addition, it should be able to verify that it actually works.

疎結合コードLess coupled code

コードが密結合されている場合、単体テストは難しくなる可能性があります。When code is tightly coupled, it can be difficult to unit test. 記述しているコードに対して単体テストを作成しないと、結合を明確に認識できません。Without creating unit tests for the code that you're writing, coupling may be less apparent.

したがって、コードに対するテストを記述する際には、必然的にコードを分離します。そうしないと、テストが困難になるからです。Writing tests for your code will naturally decouple your code, because it would be more difficult to test otherwise.

適切な単体テストの特性Characteristics of a good unit test

  • 高速Fast. 完成度の高いプロジェクトに数千もの単体テストが含まれることは、珍しくありません。It is not uncommon for mature projects to have thousands of unit tests. 単体テストの実行にかかる時間はごくわずかで、Unit tests should take very little time to run. 数ミリ秒です。Milliseconds.
  • 独立性Isolated. 単体テストはスタンドアロンであり、独立して実行でき、ファイル システムやデータベースなどの外部要因に対する依存関係がありません。Unit tests are standalone, can be run in isolation, and have no dependencies on any outside factors such as a file system or database.
  • 反復可能Repeatable. 単体テストの実行では、その結果に一貫性がある必要があります。つまり、実行と実行の間で何も変更しない場合、常に同じ結果を返す必要があります。Running a unit test should be consistent with its results, that is, it always returns the same result if you do not change anything in between runs.
  • 自己チェックSelf-Checking. テストは人の介入なしに、合格したか失敗したかを自動的に検出できる必要があります。The test should be able to automatically detect if it passed or failed without any human interaction.
  • タイムリーTimely. 単体テストは、テスト対象のコードの記述と比較して時間がかかりすぎないようにする必要があります。A unit test should not take a disproportionately long time to write compared to the code being tested. コードの記述と比較してコードのテストに時間がかかりすぎる場合は、よりテストしやすい設計を検討してください。If you find testing the code taking a large amount of time compared to writing the code, consider a design that is more testable.

用語を統一するLet's speak the same language

テストに関して、モックという用語はよく誤用されます。The term mock is unfortunately very misused when talking about testing. 単体テストの記述時における最も一般的な種類のフェイクの定義を次に示します。The following defines the most common types of fakes when writing unit tests:

フェイク - フェイクは、スタブまたはモック オブジェクトのいずれかを示すのに使用できる汎用的な用語です。Fake - A fake is a generic term which can be used to describe either a stub or a mock object. スタブとモックのどちらであるかは、使用されているコンテキストによって決まります。Whether it is a stub or a mock depends on the context in which it's used. つまり、フェイクはスタブとモックのどちらにもなりえます。So in other words, a fake can be a stub or a mock.

モック - モック オブジェクトは、単体テストに合格したか失敗したかを判断する、システム内のフェイク オブジェクトです。Mock - A mock object is a fake object in the system that decides whether or not a unit test has passed or failed. モックは、アサートされるまでフェイクとして開始します。A mock starts out as a Fake until it is asserted against.

スタブ - スタブは、システム内の既存の依存関係 (コラボレーター) の制御可能な置換です。Stub - A stub is a controllable replacement for an existing dependency (or collaborator) in the system. スタブを使用すると、依存関係を直接処理することなく、コードをテストできます。By using a stub, you can test your code without dealing with the dependency directly. 既定では、フェイクはスタブとして開始します。By default, a fake starts out as a stub.

次のコード スニペットを考えてみます。Consider the following code snippet:

var mockOrder = new MockOrder();
var purchase = new Purchase(mockOrder);

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

これは、モックとして参照されるスタブの例です。This would be an example of stub being referred to as a mock. ここでは、実際にはスタブです。In this case, it is a stub. Purchase (テスト対象のシステム) をインスタンス化する手段として、Order を渡しているだけです。You're just passing in the Order as a means to be able to instantiate Purchase (the system under test). Order はモックでないので、名前 MockOrder も非常に紛らわしいものです。The name MockOrder is also very misleading because again, the order is not a mock.

より適切なアプローチを以下に示します。A better approach would be

var stubOrder = new FakeOrder();
var purchase = new Purchase(stubOrder);

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

クラスの名前を FakeOrder に変更して、クラスをより汎用的な名前にし、クラスをモックまたはスタブとして使用できるようにしました。By renaming the class to FakeOrder, you've made the class a lot more generic, the class can be used as a mock or a stub. テスト ケースに適切な方をどちらでも使用できます。Whichever is better for the test case. 上記の例では、FakeOrder はスタブとして使用されています。In the above example, FakeOrder is used as a stub. アサート時には、どのような形状または形式でも FakeOrder を使用していません。You're not using the FakeOrder in any shape or form during the assert. FakeOrder は、コンストラクターの要件を満たすために Purchase クラスに渡されただけです。FakeOrder was just passed into the Purchase class to satisfy the requirements of the constructor.

これをモックとして使用するには、たとえば次のようにします。To use it as a Mock, you could do something like this

var mockOrder = new FakeOrder();
var purchase = new Purchase(mockOrder);

purchase.ValidateOrders();

Assert.True(mockOrder.Validated);

この場合は、フェイクのプロパティをチェック (フェイクに対してアサート) しているので、上記のコード スニペット内で mockOrder はモックです。In this case, you are checking a property on the Fake (asserting against it), so in the above code snippet, the mockOrder is a Mock.

重要

この用語を正確に理解することが重要です。It's important to get this terminology correct. スタブを "モック" と名付けた場合、他の開発者はあなたの意図を誤って解釈してしまいます。If you call your stubs "mocks", other developers are going to make false assumptions about your intent.

モックとスタブに関する重要な留意点として、モックとスタブはよく似ていますが、モック オブジェクトに対してはアサートを行うのに対し、スタブに対してはアサートを行いません。The main thing to remember about mocks versus stubs is that mocks are just like stubs, but you assert against the mock object, whereas you do not assert against a stub.

ベスト プラクティスBest practices

テストの名前付けNaming your tests

テストの名前は、次の 3 つの部分で構成される必要があります。The name of your test should consist of three parts:

  • テスト対象のメソッドの名前。The name of the method being tested.
  • それがテストされるシナリオ。The scenario under which it's being tested.
  • シナリオが呼び出されたときに想定される動作。The expected behavior when the scenario is invoked.

なぜでしょうか。Why?

  • 名前付け規則は、テストの目的を明示的に表すので重要です。Naming standards are important because they explicitly express the intent of the test.

テストは、コードが機能することを確認するだけでなく、ドキュメントも提供します。Tests are more than just making sure your code works, they also provide documentation. 単体テストのスイートを見るだけで、コード自体を見なくても、コードの動作を推測できます。Just by looking at the suite of unit tests, you should be able to infer the behavior of your code without even looking at the code itself. さらに、テストが失敗した際には、どのシナリオが想定を満たしていないかを正確に判断できます。Additionally, when tests fail, you can see exactly which scenarios do not meet your expectations.

不適切な例:Bad:

[Fact]
public void Test_Single()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

より適切な例:Better:

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();
    
    var actual = stringCalculator.Add("0");
    
    Assert.Equal(0, actual);
}

テストの配置Arranging your tests

Arrange、Act、Assert は、単体テスト時に共通するパターンです。Arrange, Act, Assert is a common pattern when unit testing. 名前が示すように、これは次の 3 つの主なアクションで構成されます。As the name implies, it consists of three main actions:

  • オブジェクトを配置 (Arrange) し、必要に応じて作成および設定します。Arrange your objects, creating and setting them up as necessary.
  • オブジェクトを操作 (Act) します。Act on an object.
  • 何かが想定どおりであることをアサート (Assert) します。Assert that something is as expected.

なぜでしょうか。Why?

  • 配置アサートの手順で、テストする対象を明確に区別します。Clearly separates what is being tested from the arrange and assert steps.
  • "Act" コードにより、アサーションが混合される可能性が低くなります。Less chance to intermix assertions with "Act" code.

読みやすさは、テストの作成時において最も重要な側面の 1 つです。Readability is one of the most important aspects when writing a test. テスト内でこれらの各アクションを分離することで、コードの呼び出しに必要な依存関係、コードを呼び出す方法、および何をアサートしようとしているかが明確に区別されます。Separating each of these actions within the test clearly highlight the dependencies required to call your code, how your code is being called, and what you are trying to assert. いくつかの手順を結合し、テストのサイズを小さくすることは可能ですが、主な目的はテストをできるだけ読みやすくすることにあります。While it may be possible to combine some steps and reduce the size of your test, the primary goal is to make the test as readable as possible.

不適切な例:Bad:

[Fact]
public void Add_EmptyString_ReturnsZero()
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Assert
    Assert.Equal(0, stringCalculator.Add(""));
}

より適切な例:Better:

[Fact]
public void Add_EmptyString_ReturnsZero()
{
    // Arrange
    var stringCalculator = new StringCalculator();
    
    // Act         
    var actual = stringCalculator.Add("");

    // Assert
    Assert.Equal(0, actual);
}

最小限の情報で合格するテストを記述するWrite minimally passing tests

単体テストで使用する入力は、現在テストしている動作を検証するために、できる限り単純である必要があります。The input to be used in a unit test should be the simplest possible in order to verify the behavior that you are currently testing.

なぜでしょうか。Why?

  • コードベースでの今後の変更に対して、テストの回復力が高くなります。Tests become more resilient to future changes in the codebase.
  • 実装よりもテストの動作に的が絞られます。Closer to testing behavior over implementation.

テストに合格するために必要以上の情報を含むテストは、テストでエラーが発生する可能性が高く、テストの目的が不明確になる可能性があります。Tests that include more information than required to pass the test have a higher chance of introducing errors into the test and can make the intent of the test less clear. テストの記述時には、動作に的を絞る必要があります。When writing tests you want to focus on the behavior. モデルに追加のプロパティを設定したり、不要な場合に 0 以外の値を使用すると、証明しようとしている内容が不明瞭になるだけです。Setting extra properties on models or using non-zero values when not required, only detracts from what you are trying to prove.

不適切な例:Bad:

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("42");

    Assert.Equal(42, actual);
}

より適切な例:Better:

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();
    
    var actual = stringCalculator.Add("0");
    
    Assert.Equal(0, actual);
}

マジック文字列を回避するAvoid magic strings

単体テストにおける変数の名前付けは、より重要というわけではなくとも、運用コードにおける変数の名前付けと同等に重要です。Naming variables in unit tests is as important, if not more important, than naming variables in production code. 単体テストにマジック文字列を含めることはできません。Unit tests should not contain magic strings.

なぜでしょうか。Why?

  • 特殊な値となっている原因を特定するために、テストを読む人が運用コードを調べる必要がなくなります。Prevents the need for the reader of the test to inspect the production code in order to figure out what makes the value special.
  • 実現しようとしている内容ではなく証明しようとしている内容が明示的に示されます。Explicitly shows what you're trying to prove rather than trying to accomplish.

マジック文字列は、テストを読む人に混乱をきたす可能性があります。Magic strings can cause confusion to the reader of your tests. 文字列が通常からかけ離れて見えれば、パラメーターや戻り値としてその特定の値がなぜ選択されたかが気になります。If a string looks out of the ordinary, they may wonder why a certain value was chosen for a parameter or return value. そのような場合、テストに集中するのではなく、実装の詳細を調べようとします。This may lead them to take a closer look at the implementation details, rather than focus on the test.

ヒント

テストの記述時には、その意図をできる限り表現することを目指す必要があります。When writing tests, you should aim to express as much intent as possible. マジック文字列の場合の適切なアプローチとしては、これらの値を定数に割り当てます。In the case of magic strings, a good approach is to assign these values to constants.

不適切な例:Bad:

[Fact]
public void Add_BigNumber_ThrowsException()
{
    var stringCalculator = new StringCalculator();

    Action actual = () => stringCalculator.Add("1001");

    Assert.Throws<OverflowException>(actual);
}

より適切な例:Better:

       [Fact]
       void Add_MaximumSumResult_ThrowsOverflowException()
       {
           var stringCalculator = new StringCalculator();
           const string MAXIMUM_RESULT = "1001";

           Action actual = () => stringCalculator.Add(MAXIMUM_RESULT);

           Assert.Throws<OverflowException>(actual);
       }

テストで論理を回避するAvoid logic in tests

単体テストの記述時には、手動による文字列の連結、および ifwhileforswitch などの論理条件を回避します。When writing your unit tests avoid manual string concatenation and logical conditions such as if, while, for, switch, etc.

なぜでしょうか。Why?

  • テスト内にバグが発生する可能性が低くなります。Less chance to introduce a bug inside of your tests.
  • 実装の詳細ではなく、最終的な結果に的が絞られます。Focus on the end result, rather than implementation details.

テスト スイートに論理を導入すると、バグが発生する可能性が著しく高まります。When you introduce logic into your test suite, the chance of introducing a bug into it increases dramatically. テスト スイート内でバグが発見されることは避けなければなりません。The last place that you want to find a bug is within your test suite. テストが機能するという高レベルの確信が持てる必要があります。そうでなければ、テストを信頼することはできません。You should have a high level of confidence that your tests work, otherwise, you will not trust them. 信頼できないテストは、何の値も提供しません。Tests that you do not trust, do not provide any value. テストが失敗した場合は、コードに実際に問題があり、無視できないことを実感できる必要があります。When a test fails, you want to have a sense that something is actually wrong with your code and that it cannot be ignored.

ヒント

テスト内の論理を避けられない場合は、テストを 2 つ以上の別々のテストに分割することを検討してください。If logic in your test seems unavoidable, consider splitting the test up into two or more different tests.

不適切な例:Bad:

[Fact]
public void Add_MultipleNumbers_ReturnsCorrectResults()
{
    var stringCalculator = new StringCalculator();
    var expected = 0;
    var testCases = new[]
    {
        "0,0,0",
        "0,1,2",
        "1,2,3"
    };

    foreach (var test in testCases)
    {
        Assert.Equal(expected, stringCalculator.Add(test));
        expected += 3;
    }

}

より適切な例:Better:

[Theory]
[InlineData("0,0,0", 0)]
[InlineData("0,1,2", 3)]
[InlineData("1,2,3", 6)]
public void Add_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected)
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add(input);

    Assert.Equal(expected, actual);
}

設定と破棄よりもヘルパー メソッドを優先するPrefer helper methods to setup and teardown

テストに同様のオブジェクトまたは状態が必要な場合は、設定属性と破棄属性を利用する (存在する場合) よりもヘルパー メソッドを優先します。If you require a similar object or state for your tests, prefer a helper method than leveraging Setup and Teardown attributes if they exist.

なぜでしょうか。Why?

  • 各テスト内からコード全体を見ることができるので、テストを読むときに混乱が少なくなります。Less confusion when reading the tests since all of the code is visible from within each test.
  • 特定のテストに対する設定が多すぎたり少なすぎたりする可能性が低くなります。Less chance of setting up too much or too little for the given test.
  • テスト間で状態を共有して、テスト間に不要な依存関係が生じる可能性が低くなります。Less chance of sharing state between tests which creates unwanted dependencies between them.

単体テスト フレームワークでは、テスト スイート内のすべての各単体テスト前に Setup が呼び出されます。In unit testing frameworks, Setup is called before each and every unit test within your test suite. これは便利なツールとして見なされる場合もありますが、通常はテストが膨張して読みづらくなってしまいます。While some may see this as a useful tool, it generally ends up leading to bloated and hard to read tests. 各テストには、テストが稼働するためにさまざまな要件があります。Each test will generally have different requirements in order to get the test up and running. しかし、Setup は各テストに対してまったく同じ要件を使用することを強制します。Unfortunately, Setup forces you to use the exact same requirements for each test.

注意

xUnit では、バージョン 2.x の時点で SetUp と TearDown の両方が削除されています。xUnit has removed both SetUp and TearDown as of version 2.x

不適切な例:Bad:

private readonly StringCalculator stringCalculator;
public StringCalculatorTests()
{
    stringCalculator = new StringCalculator();
}
// more tests...
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var result = stringCalculator.Add("0,1");

    Assert.Equal(1, result);
}

より適切な例:Better:

[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var stringCalculator = CreateDefaultStringCalculator();

    var actual = stringCalculator.Add("0,1");

    Assert.Equal(1, actual);
}
// more tests...
private StringCalculator CreateDefaultStringCalculator()
{
    return new StringCalculator();
}

複数のアサートを回避するAvoid multiple asserts

テストを記述する際には、テストごとにアサートを 1 つだけ含めるようにしてください。When writing your tests, try to only include one Assert per test. アサートを 1 つだけ使用する一般的なアプローチには、次のものがあります。Common approaches to using only one assert include:

  • アサートごとに別々のテストを作成します。Create a separate test for each assert.
  • パラメーター化されたテストを使用します。Use parameterized tests.

なぜでしょうか。Why?

  • 1 つのアサートに失敗した場合、後続のアサートは評価されません。If one Assert fails, the subsequent Asserts will not be evaluated.
  • テストで複数のケースをアサートしないようにします。Ensures you are not asserting multiple cases in your tests.
  • テストが失敗した理由に関する全体像をとらえることができます。Gives you the entire picture as to why your tests are failing.

テスト ケースに複数のアサートを導入した場合、すべてのアサートが実行されることは保証されません。When introducing multiple asserts into a test case, it is not guaranteed that all of the asserts will be executed. ほとんどの単体テスト フレームワークでは、単体テストでアサーションが失敗すると、続行中のテストは自動的に失敗と見なされます。In most unit testing frameworks, once an assertion fails in a unit test, the proceeding tests are automatically considered to be failing. この際、実際には動作している機能が失敗として示されるので、混乱を招きます。This can be confusing as functionality that is actually working, will be shown as failing.

注意

この規則の一般的な例外は、オブジェクトに対してアサートを行う場合です。A common exception to this rule is when asserting against an object. この場合、オブジェクトが想定どおりの状態にあること保証するために、通常は各プロパティに対して複数のアサートを使用することが許容されます。In this case, it is generally acceptable to have multiple asserts against each property to ensure the object is in the state that you expect it to be in.

不適切な例:Bad:

[Fact]
public void Add_EdgeCases_ThrowsArgumentExceptions()
{
    Assert.Throws<ArgumentException>(() => stringCalculator.Add(null));
    Assert.Throws<ArgumentException>(() => stringCalculator.Add("a"));
}

より適切な例:Better:

    [Theory]
    [InlineData(null)]
    [InlineData("a")]
    public void Add_InputNullOrAlphabetic_ThrowsArgumentException(string input)
    {
        var stringCalculator = new StringCalculator();

        Action actual = () => stringCalculator.Add(input);

        Assert.Throws<ArgumentException>(actual);
    }

パブリック メソッドの単体テストを行うことでプライベート メソッドを検証するValidate private methods by unit testing public methods

ほとんどの場合、プライベート メソッドをテストする必要はありません。In most cases, there should not be a need to test a private method. プライベート メソッドは実装の詳細です。Private methods are an implementation detail. プライベート メソッドは独立して存在することはない、と考えることができます。You can think of it this way: private methods never exist in isolation. いずれかの時点で、実装の一部としてプライベート メソッドを呼び出す、パブリックに公開されたメソッドが存在することになります。At some point, there is going to be a public facing method that calls the private method as part of its implementation. 鍵となるのは、プライベート メソッドを呼び出すパブリック メソッドの最終結果です。What you should care about is the end result of the public method that calls into the private one.

次のケースを考えてみます。Consider the following case

public string ParseLogLine(string input)
{
    var sanitizedInput = TrimInput(input);
    return sanitizedInput;
}

private string TrimInput(string input)
{
    return input.Trim();
}

メソッドが想定どおりに動作していることを確認する必要があるので、まずは、TrimInput に対してテストを記述しようと考えるかもしれません。Your first reaction may be to start writing a test for TrimInput because you want to make sure that the method is working as expected. しかし、想定外の方法で ParseLogLinesanitizedInput を操作し、TrimInput に対するテストが無効になる可能性があります。However, it is entirely possible that ParseLogLine manipulates sanitizedInput in such a way that you do not expect, rendering a test against TrimInput useless.

実際のテストは、パブリックに公開された ParseLogLine メソッドに対して行う必要があります。最終的にはこのメソッドが鍵となるためです。The real test should be done against the public facing method ParseLogLine because that is what you should ultimately care about.

public void ParseLogLine_ByDefault_ReturnsTrimmedResult()
{
    var parser = new Parser();

    var result = parser.ParseLogLine(" a ");

    Assert.Equals("a", result);
}

この観点で、プライベート メソッドがある場合は、パブリック メソッドを見つけて、そのメソッドに対してテストを記述します。With this viewpoint, if you see a private method, find the public method and write your tests against that method. プライベート メソッドが想定どおりの結果を返すからといって、最終的にプライベート メソッドを呼び出すシステムが、結果を正しく使用するとは限りません。Just because a private method returns the expected result, does not mean the system that eventually calls the private method uses the result correctly.

スタブの静的参照Stub static references

単体テストの原則の 1 つは、テスト対象システムに対してフル コントロールを持つ必要があることです。One of the principles of a unit test is that it must have full control of the system under test. このことは、運用コードに静的参照への呼び出しが含まれる場合に問題となります (例: DateTime.Now)。This can be problematic when production code includes calls to static references (e.g. DateTime.Now). 次のコードを考えてみます。Consider the following code

public int GetDiscountedPrice(int price)
{
    if(DateTime.Now.DayOfWeek == DayOfWeek.Tuesday) 
    {
        return price / 2;
    }
    else 
    {
        return price;
    }
}

このコードに対して単体テストを行うには、どのようにすればよいでしょうでしょうか。How can this code possibly be unit tested? 次のようなアプローチを試してみることができます。You may try an approach such as

public void GetDiscountedPrice_ByDefault_ReturnsFullPrice()
{
    var priceCalculator = new PriceCalculator();

    var actual = priceCalculator.GetDiscountedPrice(2);

    Assert.Equals(2, actual)
}

public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
    var priceCalculator = new PriceCalculator();

    var actual = priceCalculator.GetDiscountedPrice(2);

    Assert.Equals(1, actual);
}

ただし、このテストにはいくつかの問題があることがすぐにわかります。Unfortunately, you will quickly realize that there are a couple problems with your tests.

  • テスト スイートが火曜日に実行される場合、2 番目のテストには合格しますが、最初のテストには失敗します。If the test suite is run on a Tuesday, the second test will pass, but the first test will fail.
  • テスト スイートが他のいずれかの曜日に実行される場合、最初のテストには合格しますが、2 番目のテストには失敗します。If the test suite is run on any other day, the first test will pass, but the second test will fail.

こうした問題を解決するには、運用コードにシームを導入する必要があります。To solve these problems, you'll need to introduce a seam into your production code. 1 つのアプローチとしては、インターフェイス内で制御する必要のあるコードをラップし、運用コードがそのインターフェイスに依存するようにします。One approach is to wrap the code that you need to control in an interface and have the production code depend on that interface.

public interface IDateTimeProvider
{
    DayOfWeek DayOfWeek();
}

public int GetDiscountedPrice(int price, IDateTimeProvider dateTimeProvider)
{
    if(dateTimeProvider.DayOfWeek() == DayOfWeek.Tuesday) 
    {
        return price / 2;
    }
    else 
    {
        return price;
    }
}

この時点で、テスト スイートは次のようになります。Your test suite now becomes

public void GetDiscountedPrice_ByDefault_ReturnsFullPrice()
{
    var priceCalculator = new PriceCalculator();
    var dateTimeProviderStub = new Mock<IDateTimeProvider>();
    dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Monday);

    var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);

    Assert.Equals(2, actual);
}

public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
    var priceCalculator = new PriceCalculator();
    var dateTimeProviderStub = new Mock<IDateTimeProvider>();
    dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Tuesday);

    var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);

    Assert.Equals(1, actual);
}

これで、テスト スイートは DateTime.Now に対してフル コントロールを持ち、メソッドを呼び出すときに任意の値をスタブできるようになりました。Now the test suite has full control over DateTime.Now and can stub any value when calling into the method.