PHPでのユニットテスト

PHPは非常に簡単に習得できます。 これは、「24時間で何かを取っておけ」という豊富な文献と同様に、質の悪いコードを大量に生成しました。 その結果、遅かれ早かれ、ゲストブックや名刺サイトの作成を超えたプログラマーは、「ここに少し追加しても、他のすべては落ちませんか?」という質問に直面します。テスト。

最初は予約したいです。ここではTDDとソフトウェア開発方法論については説明しません。 この記事では、PHPUnitフレームワークに基づいた単体テストの基本をPHP開発者に紹介します。


序文の代わりに


初めに、完全に合理的な疑問が生じます。 なぜ、すべてがそのように機能するのか?
ここではすべてが簡単です。 次の変更の後、何かが正常に機能しなくなります-それをお約束します。 そして、間違いを見つけることは非常に時間がかかります。 単体テストでは、このプロセスを数分に短縮できます。 別のプラットフォームに移動する可能性と関連する「落とし穴」を排除しません。 64ビットシステムと32ビットシステムでの計算の精度の違いは何ですか。 そして、あなたが最初のようなニュアンスに出くわしたとき

<?php
print ( int ) ( ( 0.1 + 0.7 ) * 10 ) ;
// 8? ...
// : " BC Math? !"



必要性の質問が消えたら、ツールの選択に進むことができます。 ここで、品揃えは一般的に小さい-PHPUnithttp://www.phpunit.de/ )およびSimpleTesthttp://www.simpletest.org/ )。 PHPUnitはすでにPHPアプリケーションのテストにおいて事実上の標準になっているため、ここで詳しく説明します。

最初の知り合い


インストールの問題は考慮されません。すべてがサイトに明確に記載されています 。 すべてがどのように機能するかをよりよく理解してみましょう。

特定のクラス「MyClass」があるとします。これは、数値の累乗を実装するメソッドの1つです。 (ここで謝罪する必要がありますが、一般的にすべての例は指から吸い出されます)

Myclass.php
<?php
class MyClass {

public function power ( $x , $y )
{
return pow ( $x , $y ) ;
}
}



正しく動作するかどうかを確認したい。 クラス全体についてはまだ話していません-メソッドは1つだけです。 検証のためにこのようなテストを作成します。

MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php' ;
require_once 'MyClass.php' ;

class MyClassTest extends PHPUnit_Framework_TestCase {
public function testPower ( )
{
$my = new MyClass ( ) ;
$this -> assertEquals ( 8 , $my -> power ( 2 , 3 ) ) ;
}
}



テストの形式に関する小さな余談。

テストを見ると、べき乗法をテストすることで、クラスのインスタンスを作成し、定義済みの値を使用して必要なメソッドを呼び出し、計算が正しく実行されたかどうかを確認できます。 このチェックでは、 assertEquals()メソッドが使用されました。これは、最初の必須パラメーターとして期待値を取り、2番目として実際の値を取り、それらのコンプライアンスをチェックします。 頭を伸ばして、九九の知識を更新したので、2 3 = 8と仮定しました。 このデータで、メソッドの動作を確認します。
テストを実行します。

$ phpunit MyClassTest
.
Time: 0 seconds
OK (1 test, 1 assertion)



テストの結果はOKです。 これについて詳しく説明することは可能だと思われますが、チェックするメソッドの複数のデータセットをチェックするとよい場合があります。 PHPUnitはそのような機会を提供してくれます-これらはデータプロバイダーです。 データプロバイダーはパブリックメソッド(名前は重要ではありません)でもあり、各反復に対してデータセットの配列を返します。 プロバイダーを使用するには、テストの@dataProviderタグでプロバイダーを指定する必要があります。

次のようにテストを変更します。

MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php' ;
require_once 'MyClass.php' ;

class MyClassTest extends PHPUnit_Framework_TestCase {

/**
* @dataProvider providerPower
*/

public function testPower ( $a , $b , $c )
{
$my = new MyClass ( ) ;
$this -> assertEquals ( $c , $my -> power ( $a , $b ) ) ;
}

public function providerPower ( )
{
return array (
array ( 2 , 2 , 4 ) ,
array ( 2 , 3 , 9 ) ,
array ( 3 , 5 , 243 )
) ;
}
}



起動後、次の画像が表示されます。

.F.
Time: 0 seconds
There was 1 failure:
1) testPower(MyClassTest) with data set #1 (2, 3, 9)
Failed asserting that <integer:8> matches expected value <integer:9>.
/home/user/unit/MyClassTest.php:14
FAILURES!
Tests: 3, Assertions: 3, Failures: 1.



さらに詳しく説明します。 テストの最初の結論で多くの人がタイプミスと間違えた可能性があるという点は、実際には1つではなく、成功したテストです。 F(発作)-したがって、テストは失敗しました。 したがって、この場合、3つのテストが実施され、そのうち1つが失敗しました。 拡張された説明では、どのデータ、どの初期データセット、どのような実際の結果、どのような結果が期待されるのかを説明しました。 2 3が実際に9に等しい場合、この方法でシナリオにエラーが表示されます。

ここでは、いくつかの抽象的な慣行から脱出し、非常に具体的な理論に進むことが理にかなっています。 つまり、テストされたスクリプトの動作をチェックするために、どのアサートメソッドを記述できますか。

最も単純な2つはassertFalse()assertTrue()です。 結果の値がそれぞれfalseとtrueであるかどうかを確認します。 次に、すでに述べたassertEquals()とその逆のassertNotEquals()があります。 それらの使用には微妙な違いがあります。 そのため、浮動小数点数を比較する場合、比較の精度を指定することができます。 これらのメソッドは、DOMDocumentクラス、配列、および任意のオブジェクトのインスタンスの比較にも使用されます(後者の場合、オブジェクトの属性に同じ値が含まれる場合、同等性が確立されます)。 AssertNull()およびassertNotNull()も言及する必要があります。これらは、パラメーターがNULLデータ型と一致するかどうかを確認します(はい、PHPでは別のデータ型であることを忘れないでください)。 可能な比較はこれに限定されません。 この記事のフレームワークではドキュメントを転載することは意味がないので、可能であればすべての可能なメソッドの構造化されたリストを提供します。 もっと興味がある人はここで読むことができます

基本的な比較方法
assertTrue()/ assertFalse()
assertEquals()/ assertNotEquals()
assertGreaterThan()
assertGreaterThanOrEqual()
assertLessThan()
assertLessThanOrEqual()
assertNull()/ assertNotNull()
assertType()/ assertNotType()
assertSame()/ assertNotSame()
assertRegExp()/ assertNotRegExp()

配列比較メソッド
assertArrayHasKey()/ assertArrayNotHasKey()
assertContains()/ assertNotContains()
assertContainsOnly()/ assertNotContainsOnly()

OOP固有のメソッド
assertClassHasAttribute()/ assertClassNotHasAttribute()
assertClassHasStaticAttribute()/ assertClassNotHasStaticAttribute()
assertAttributeContains()/ assertAttributeNotContains()
assertObjectHasAttribute()/ assertObjectNotHasAttribute()
assertAttributeGreaterThan()
assertAttributeGreaterThanOrEqual()
assertAttributeLessThan()
assertAttributeLessThanOrEqual()

ファイル比較方法
assertFileEquals()/ assertFileNotEquals()
assertFileExists()/ assertFileNotExists()
assertStringEqualsFile()/ assertStringNotEqualsFile()

XML比較メソッド
assertEqualXMLStructure()
assertXmlFileEqualsXmlFile()/ assertXmlFileNotEqualsXmlFile()
assertXmlStringEqualsXmlFile()/ assertXmlStringNotEqualsXmlFile()
assertXmlStringEqualsXmlString()/ assertXmlStringNotEqualsXmlString()

雑多
assertTag()
assertThat()

例外


まだ疲れていない場合は、練習、つまり例外の処理に戻ります。 まず、テストしたクラスを変更します-この例外をスローするメソッドを導入します。

Myclass.php
<?php
class MathException extends Exception { } ;

class MyClass {

// ...

public function divide ( $x , $y )
{
if ( ! ( boolean ) $y )
{
throw new MathException ( 'Division by zero' ) ;
}
return $x / $y ;
}
}



ここで、特定のデータセットに対してこの例外が発生した場合に成功するテストを作成する必要があります。 必要な例外は、少なくとも2つの方法で設定できます。テストに@expectedExceptionを追加するか、テストでsetExpectedException()メソッドを呼び出します。

MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php' ;
require_once 'MyClass.php' ;

class MyClassTest extends PHPUnit_Framework_TestCase {

// ...

/**
* @expectedException MathException
*/

public function testDivision1 ( )
{
$my = new MyClass ( ) ;
$my -> divide ( 8 , 0 ) ;
}

public function testDivision2 ( )
{
$this -> setExpectedException ( 'MathException' ) ;
$my = new MyClass ( ) ;
$my -> divide ( 8 , 0 ) ;
}
}



一般に、テストはまったく同じです。 方法の選択はあなた次第です。 PHPUnitが直接提供するメカニズムに加えて、例外をテストするために、たとえば次のような標準のtry {...} catch()を使用できます。

MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php' ;
require_once 'MyClass.php' ;

class MyClassTest extends PHPUnit_Framework_TestCase {

// ...

public function testDivision3 ( )
{
$my = new MyClass ( ) ;
try {
$my -> divide ( 8 , 2 ) ;
} catch ( MathException $e ) {
return ;
}
$this -> fail ( 'Not raise an exception' ) ;
}
}



この例では、 fail()メソッドを呼び出してテストを終了する、これまでに検討されていない方法も確認できます。 テスト出力は次のようになります。

F
Time: 0 seconds
There was 1 failure:
1) testDivision3(MyClassTest)
Not raise an exception
/home/user/unit/MyClassTest.php:50



付属品


基本的なテスト方法を習得しました。 テストを改善できますか? はい この記事の最初から書かれたクラスは、いくつかのテストを実行します。それぞれのテストでは、テストされたクラスのインスタンスが作成されます。 各テストの開始前に1回呼び出されるprotected setUp()メソッドを使用して、それらをインストールできます。 テストが完了すると、 tearDown()メソッドが呼び出され 、ここで「ガベージコレクション」を実行できます。 したがって、修正されたテストは次のようになります。

MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php' ;
require_once 'MyClass.php' ;

class MyClassTest extends PHPUnit_Framework_TestCase {

protected $fixture ;

protected function setUp ( )
{
$this -> fixture = new MyClass ( ) ;
}

protected function tearDown ( )
{
$this -> fixture = NULL ;
}

/**
* @dataProvider providerPower
*/

public function testPower ( $a , $b , $c )
{
$this -> assertEquals ( $c , $this -> fixture -> power ( $a , $b ) ) ;
}

public function providerPower ( )
{
return array (
array ( 2 , 2 , 4 ) ,
array ( 2 , 3 , 8 ) ,
array ( 3 , 5 , 243 )
) ;
}

// …

}



テストスイート


いくつかのクラスのコードがテストでカバーされた後、各テストを個別に実行するのは非常に不便になります。 ここでテストスイートが役立つ場合があります。1つのタスクに関連する複数のテストを組み合わせてキットにし、それに応じて実行できます。 セットは、 PHPUnit_Framework_TestSuiteクラスによって実装されます。 このクラスのインスタンスを作成し、 addTestSuite()メソッドを使用して必要なテストを追加する必要があります。 addTest()メソッドを使用すると、別のセットを追加できます。

SpecificTests.php
<?php
require_once 'PHPUnit/Framework.php' ;
//
require_once 'MyClassTest.php' ;

class SpecificTests
{
public static function suite ( )
{
$suite = new PHPUnit_Framework_TestSuite ( 'MySuite' ) ;
//
$suite -> addTestSuite ( 'MyClassTest' ) ;
return $suite ;
}
}



AllTests.php
<?php
require_once 'PHPUnit/Framework.php' ;
//
require_once 'SpecificTests.php' ;

class AllTests
{
public static function suite ( )
{
$suite = new PHPUnit_Framework_TestSuite ( 'AllMySuite' ) ;
//
$suite -> addTest ( SpecificTests :: suite ( ) ) ;
return $suite ;
}
}



ここで、データベースで動作するスクリプトの一連のテストを想像してください。 すべてのテストでデータベースに接続する必要がありますか? いいえ、する必要はありません。 これを行うには、 PHPUnit_Framework_TestSuiteから継承した独自のクラスを作成し、そのsetUp()およびtearDown()メソッドを定義してデータベースインターフェイスを初期化し、 sharedFixture属性でテストに渡すだけです。 データベースは後で残しますが、今のところは、既存のクラス用に独自のテストセットを作成しようとします。

MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php' ;
require_once 'MyClass.php' ;

class MyClassTest extends PHPUnit_Framework_TestCase {

protected $fixture ;

protected function setUp ( )
{
$this -> fixture = $this -> sharedFixture ;
}

protected function tearDown ( )
{
$this -> fixture = NULL ;
}

/**
* @dataProvider providerPower
*/

public function testPower ( $a , $b , $c )
{
$this -> assertEquals ( $c , $this -> fixture -> power ( $a , $b ) ) ;
}

public function providerPower ( )
{
return array (
array ( 2 , 2 , 4 ) ,
array ( 2 , 3 , 8 ) ,
array ( 3 , 5 , 243 )
) ;
}

// …

}



MySuite.php
<?php
require_once 'MyClassTest.php' ;

class MySuite extends PHPUnit_Framework_TestSuite {

protected $sharedFixture ;

public static function suite ( )
{
$suite = new MySuite ( 'MyTests' ) ;
$suite -> addTestSuite ( 'MyClassTest' ) ;
return $suite ;
}

protected function setUp ( )
{
$this -> sharedFixture = new MyClass ( ) ;
}

protected function tearDown ( )
{
$this -> sharedFixture = NULL ;
}

}



ここでは、テスト対象のクラスのインスタンスをsharedFixtureに配置し 、テストで使用しました-ソリューションはあまり美しくありません(まったく美しいとは言えません)が、テストセットとテスト間のアクセサリの転送の一般的なアイデアを提供します。 メソッド呼び出しのシーケンスを視覚化すると、次のようになります。

MySuite::setUp()
MyClassTest::setUp()
MyClassTest::testPower()
MyClassTest::tearDown()
MyClassTest::setUp()
MyClassTest::testDivision()
MyClassTest::tearDown()
...
MySuite::tearDown()



追加機能


上記に加えて、スクリプトの計算と動作だけでなく、開発の結論と速度も確認する必要があります。 このために、 PHPUnit_Extensions_OutputTestCase拡張機能とPHPUnit_Extensions_PerformanceTestCase拡張機能がそれぞれ用意されています。 テストしたクラスに別のメソッドを追加し、正しく機能するかどうかを確認します。

Myclass.php
<?php
class MyClass {

// ...

public function square ( $x )
{
sleep ( 2 ) ;
print $x * $x ;
}

}



MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php' ;
require_once 'PHPUnit/Extensions/OutputTestCase.php' ;
require_once 'PHPUnit/Extensions/PerformanceTestCase.php' ;
require_once 'MyClass.php' ;

class MyClassOutputTest extends PHPUnit_Extensions_OutputTestCase {

protected $fixture ;

protected function setUp ( )
{
$this -> fixture = $this -> sharedFixture ;
}

protected function tearDown ( )
{
$this -> fixture = NULL ;
}

public function testSquare ( )
{
$this -> expectOutputString ( '4' ) ;
$this -> fixture -> square ( 2 ) ;
}
}

class MyClassPerformanceTest extends PHPUnit_Extensions_PerformanceTestCase {

protected $fixture ;

protected function setUp ( )
{
$this -> fixture = $this -> sharedFixture ;
}

protected function tearDown ( )
{
$this -> fixture = NULL ;
}

public function testPerformance ( )
{
$this -> setMaxRunningTime ( 1 ) ;
$this -> fixture -> square ( 4 ) ;
}
}

class MyClassTest extends PHPUnit_Framework_TestCase {

// …

}



expectOutputString()またはexpectOutputRegex()メソッドを使用して、期待されるスクリプト出力を指定できます。 また、 setMaxRunningTime()メソッドの場合、計画処理時間は秒単位で示されます。 これらのテストをすでに記述されているものと一緒に実行するには、セットに追加するだけです。

MySuite.php
<?php
require_once 'MyClassTest.php' ;

class MySuite extends PHPUnit_Framework_TestSuite {

protected $sharedFixture ;

public static function suite ( )
{
$suite = new MySuite ( 'MyTests' ) ;
$suite -> addTestSuite ( 'MyClassTest' ) ;
$suite -> addTestSuite ( 'MyClassOutputTest' ) ;
$suite -> addTestSuite ( 'MyClassPerformanceTest' ) ;
return $suite ;
}

// ...

}



テストをスキップする


最後に、何らかの理由でいくつかのテストをスキップする必要がある状況を考えてください。 たとえば、テスト対象のマシンにPHP拡張機能がない場合、次のコードを追加することで、PHP拡張機能が欠落していることを確認し、テストを欠落としてマークできます。

if ( ! extension_loaded ( 'someExtension' ) ) {
$this -> markTestSkipped ( 'Extension is not loaded.' ) ;
}



または、スクリプトにまだないコード(TDDのまれな状況ではない)に対してテストが記述されている場合、 markTestIncomplete()メソッドを使用して、実装されていないものとしてマークできます。

最後に


おそらく今のところここでやめることができます。 この記事による単体テストのトピックは完全ではありません。モックオブジェクトの使用、データベースの操作のテスト、コードカバレッジの分析などがまだあります。 しかし、PHPUnitの基本的な機能に初心者を慣れさせ、効率を高める手段の1つとして単体テストの使用を奨励するという目標セットが達成されたことを願っています。
幸運と安定したアプリケーション。

Source: https://habr.com/ru/post/J56289/


All Articles