web-dev-qa-db-ja.com

簡単に操作できないローカル/ネストされた条件付き論理分岐と変数をテストする方法は?

予想されるパスをたどる条件付きブロックの単体テストを作成するのは簡単ですが、直接制御しないソース/オブジェクト(変更またはアクセスしたくないデータベース、環境変数、など)デバッグロジックを追加するためにソースコードを変更せずに(単体テストのみを関数として使用。以下の指定された制御ブロックをテストする単体テストをどのように構築しますか?

function(int x)
{
   if(x > 10)
   {
       if(system.day() == "Monday")
           print "Monday"
       else
           // TEST THIS SPACE (BUT ON A MONDAY :)
           print "Not Monday"
   }
   else
   {
       ....
   }
}

前の関数では、関数を記述してxに好きな値を渡すことができますが、(簡単に)変更できないSystem.date()呼び出しに依存するネストされた条件をテストするにはどうすればよいですか?

私が制御せず、アクセスできないデータベースを使用する別の例:

function(int x)
{
    try
    {
        if(x > 10)
        {
             query_result = database.query()
             if(query_results != NULL)
             {
                 print "QUERY NOT NULL"
             }
             else
             {
                 // TEST THIS SPACE 
                 print "QUERY IS NULL"
             }
        }
    }
}

明らかに、クエリを渡した場合は制御できましたが、この場合は制御できません。これらは単純で不自然な例です。これらの特定のケースと、私が考慮していない可能性がある関連シナリオを展開してください。

4
user58446

依存性注入を使用したい。 C#の例を紹介します。私はそれに慣れているからです。あなたの最初の例:

class YourClass
{
    ITimeService _timeService;
    IOutputService _outputService;

    public YourClass(ITimeService timeService, IOutputService outputService)
    {
        _timeService = timeService;
        _outputService = outputService;
    }    

    public function(int x)
    {
       if(x > 10)
       {
           if(_timeService.GetDayOfWeek() == "Monday")
               _outputService.Print("Monday")
           else
               // TEST THIS SPACE (BUT ON A MONDAY :)
               _outputService.Print("Not Monday")
       }
       else
       {
           ....
       }
    }

ここで、2つのサービスは次のように定義されています

interface ITimeService
{
    string GetDayOfWeek();
}

interface IOutputService 
{
    void Print(string text);
}

このように実装されています:

class RealTimeService : ITimeService
{
    public string GetDayOfWeek()
    {
        return system.day(); //not C#
    }
}

class RealOutputService : IOutputService
{
    public void Print(string text)
    {
        print text; //not C#
    }
}

次に、プログラムのメイン関数(依存関係注入の用語を使用した「コンポジションルート」とも呼ばれます)で、クラスを次のようにインスタンス化します。

void main()
{
     YourClass yourClass = new YourClass(new RealTimeService(), new RealOutputService());

     yourClass.function(11);
}

ただし、コードをテストする場合は、RealTimeServiceとRealOutputServiceを注入する代わりに、FakeTimeServiceとFakeOutputServiceを注入します。たとえば、次のFakeTimeServiceを使用すると、必要な曜日を返すことができます。

public class FakeTimeService : ITimeService
{
    private string _dayOfWeek;

    public FakeTimeService(string dayOfWeek)
    {
        _dayOfWeek = dayOfWeek;
    }

    public string GetDayOfWeek()
    {
        return _dayOfWeek;
    }
}

このFakeOutputServiceは、出力されたものをすべて格納するので、テストでそれが正しいことを確認できます。

public class FakeOutputService : IOutputService
{
    public string _printedText = "";

    public Print(string text)
    {
        _printedText += text;
    }
}

最後に、テストコード内で、次のようにクラスをインスタンス化してテストします。

[Test]
void When_It_Is_Monday_Output_Should_Be_Monday()
{
    FakeOutputService fakeOutputService = new FakeOutputService();
    YourClass yourClass = new YourClass(new FakeTimeService("MONDAY"), fakeOutputService);

    yourClass.function(11);

    Assert.AreEqual(fakeOutputService._printedText, "Monday");
}

[Test]
void When_It_Is_Tuesday_Output_Should_Be_Not_Monday()
{
    FakeOutputService fakeOutputService = new FakeOutputService();
    YourClass yourClass = new YourClass(new FakeTimeService("TUESDAY"), fakeOutputService);

    yourClass.function(11);

    Assert.AreEqual(fakeOutputService._printedText, "Not Monday");
}

当然のことながら、これは多くのタイピングのように思われます。そのため、ほとんどの場合、モックフレームワークを使用してそれを行います。たとえばC#では、 Moqフレームワーク を使用します。これにより、偽のオブジェクトが作成されます。

また、ユニットテストの例で別のDIが必要な場合は、数か月前の私の回答を確認してください here

5
Eternal21

まず、「ソースコードに変更を加えない」というポリシーと組み合わせてテスト可能な方法で記述されていない関数をテストしようとすることは、非常に効果的なアプローチではないと私は確信しています。多くの場合、ソースコードの一部の変更はわずかな労力で済み、何かを壊すリスクは非常に低くなりますが、単体テストを作成する労力は桁違いに減少します。

ただし、実際のプログラミング言語によっては、変数systemまたはdatabaseを「テストオブジェクト」または「モック」から初期化できるテスト環境にこれらの関数を埋め込むことができる場合があります。外側。これにより、テストする機能が「有効」であるソースコードファイルを変更せずに、サンプルの機能をテストできます。

たとえば、CまたはC++では、これにプリプロセッサを利用したり、テスト環境で開発環境とは異なるファイルを含めたりすることができます。 JavaまたはC#では、モック「データベース」とこのためのモック「システム」オブジェクトを含む特別なテストプロジェクトをセットアップし、「テスト中の関数」を参照できる場合がありますこの環境に。

ただし、呼び出し側が直接「システム」または関数system.dayを挿入できるようにコードを少し変更すると、これははるかに簡単になります。 Michael Feathersはこれを seam をコード内に作成すると呼びました。コードを編集することなく動作を変更できる場所です。

0
Doc Brown

私はPHP開発者なので、この言語でどのように行われるかを説明します。しかし、ほとんどの言語には同じようなものがあると確信しています。これは十分によくある問題だと確信しています。ほとんどのテストフレームワークでは、少なくとも問題を解決する方法がわかっているため、特定の言語/プラットフォームで検索するだけです。

PHPで示すことができる最良の例は、PHPUnitブリッジ(Symfonyフレームワークの一部)に実装されているClockMockです): https://github.com/ symfony/phpunit-bridge/blob/master/ClockMock.php

言語には、たとえばsleep()など、いくつかのネイティブの時間関連関数があります。

あなたのコードにこれがあるとしましょう:

class A
{
    public function pauseSystemForAWhile()
    {
        sleep(2);
    }
}

本番環境で実行すると、もちろんコードは約2000msの間スリープします。

ただし、単体テストの場合、上記のクラスはそれ自体を言語に「フック」し、その関数の定義をオーバーライドします。それがどのように実装されているかを見ると、ユニットテストの実行中に増加する内部カウンターを使用していることがわかります。

これを効果的に使用するには、特定のグループを使用して単体テストにマークを付けるだけです。

/**
 * @group time-sensitive
 */
class ATest extends \PHPUnit_Framework_TestCase
{
    public function testPauseSystemForAWhile()
    {
        $now = time();

        $a = new A();
        $a->pauseSystemForAWhile();

        $afterPause = time();

        //check that 2 seconds have passed.
        $this->assertEquals(
            2,
            $afterPause - $now
        );
    }
}

ご覧のとおり、内部での実装はそれほどきれいではなく、フレームワークの開発者が正しく理解するまでには時間がかかったと思います。しかし、それは本当にエレガントで使いやすいです。

残念ながら、他の言語で「モック」ネイティブ言語関数をどのように実行できるかは知りませんが、最初に言ったように、あなたの言語には確かに存在します。

0
Radu Murzea