테스트 중심 개발 및 단위 테스트는 수정 및 주요 조정에도 불구하고 코드가 예상대로 계속 작동하는지 확인하는 최신 방법입니다. 이 문서에서는 모듈, 데이터베이스 및 사용자 인터페이스(UI) 계층에서 PHP 코드를 단위 테스트하는 방법을 알아봅니다.
새벽 3시입니다. 우리 코드가 여전히 작동하는지 어떻게 알 수 있습니까?
웹 애플리케이션은 연중무휴로 실행되므로 내 프로그램이 여전히 실행되고 있는지에 대한 질문이 밤에 나를 귀찮게 할 것입니다. 단위 테스트는 잠을 잘 수 있을 만큼 코드에 대한 자신감을 키우는 데 도움이 되었습니다.
단위 테스트는 코드에 대한 테스트 사례를 작성하고 이러한 테스트를 자동으로 실행하기 위한 프레임워크입니다. 테스트 중심 개발은 먼저 테스트를 작성하고 해당 테스트에서 오류를 찾을 수 있는지 확인한 다음 해당 테스트를 통과하는 데 필요한 코드 작성을 시작해야 한다는 아이디어에 기반한 단위 테스트 접근 방식입니다. 모든 테스트를 통과하면 우리가 개발한 기능이 완성됩니다. 이러한 단위 테스트의 가치는 코드를 체크인하기 전, 주요 변경 후 또는 실행 중인 시스템에 배포한 후 언제든지 실행할 수 있다는 것입니다.
PHP 단위 테스트
PHP의 단위 테스트 프레임워크는 PHPUnit2입니다. 이 시스템은 PEAR 명령줄을 사용하여 PEAR 모듈로 설치할 수 있습니다: % pear install PHPUnit2.
프레임워크를 설치한 후 PHPUnit2_Framework_TestCase에서 파생된 테스트 클래스를 생성하여 단위 테스트를 작성할 수 있습니다.
모듈 단위 테스트
단위 테스트를 시작하기 가장 좋은 곳은 애플리케이션의 비즈니스 로직 모듈이라는 것을 알았습니다. 저는 간단한 예를 사용하고 있습니다. 이것은 두 숫자를 합하는 함수입니다. 테스트를 시작하려면 먼저 아래와 같이 테스트 케이스를 작성합니다.
목록 1. TestAdd.php
<?phprequire_once 'Add.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestAdd는 PHPUnit2_Framework_TestCase{ function test1() { $this->assertTrue( add( 1, 2 ) == 3 ); } function test2() { $this->assertTrue( add( 1, 1 ) == 2 ); }}?>
이 TestAdd 클래스에는 두 가지 메서드가 있으며 둘 다 테스트 접두사를 사용합니다. 각 메소드는 Listing 1처럼 간단할 수도 있고 매우 복잡할 수도 있는 테스트를 정의합니다. 이 경우 첫 번째 테스트에서는 1 더하기 2가 3이고 두 번째 테스트에서는 1 더하기 1이 2라고 간단하게 주장합니다.
PHPUnit2 시스템은 매개변수에 포함된 조건 값이 true인지 테스트하는 데 사용되는 AssertTrue() 메서드를 정의합니다. 그런 다음 처음에는 잘못된 결과를 생성하는 Add.php 모듈을 작성했습니다.
목록 2. Add.php
<?phpfunction add( $a, $b ) { return 0 }?>
이제 단위 테스트를 실행하면 두 테스트가 모두 실패합니다.
목록 3. 테스트 실패
% phpunit TestAdd.phpPHPUnit 2.2.1 - Sebastian Bergmann.FFTime: 0.00312709808349612개의 실패가 있었습니다:1) test1(TestAdd)2) test2(TestAdd)FAILURES!!!테스트 실행: 2, 실패: 2, 오류: 0, 불완전한 테스트: 0.
이제 두 테스트가 모두 잘 작동한다는 것을 알았습니다. 따라서 add() 함수를 수정하여 실제 작업을 실제로 수행할 수 있습니다.
이제 두 테스트가 모두 통과되었습니다.
<?phpfunction add( $a, $b ) { return $a+$b; }?>
목록 4. 테스트가
Sebastian Bergmann의 % phpunit TestAdd.phpPHPUnit 2.2.1을 통과했습니다...시간: 0.0023679733276367OK(2개 테스트)
% 테스트 중심 개발의 이 예는 매우 간단하지만 그 아이디어를 이해할 수 있습니다. 먼저 테스트 케이스를 생성하고 테스트를 실행하기에 충분한 코드를 가지고 있었지만 결과가 잘못되었습니다. 그런 다음 테스트가 실제로 실패했는지 확인한 다음 테스트를 통과하도록 실제 코드를 구현합니다.
코드를 구현하면서 모든 코드 경로를 포괄하는 완전한 테스트가 완료될 때까지 코드를 계속 추가한다는 사실을 알게 되었습니다. 이 글의 마지막 부분에서는 어떤 테스트를 작성하고 어떻게 작성해야 하는지에 대한 조언을 찾을 수 있습니다.
데이터베이스 테스트
모듈 테스트 후에 데이터베이스 액세스 테스트를 수행할 수 있습니다. 데이터베이스 액세스 테스트에서는 두 가지 흥미로운 문제가 발생합니다. 먼저, 각 테스트 전에 알려진 지점으로 데이터베이스를 복원해야 합니다. 둘째, 이번 복구로 인해 기존 데이터베이스에 손상이 발생할 수 있다는 점을 유의하시기 바랍니다. 따라서 비프로덕션 데이터베이스에서 테스트하거나, 테스트 케이스 작성 시 기존 데이터베이스의 내용에 영향을 주지 않도록 주의해야 합니다.
데이터베이스 단위 테스트는 데이터베이스에서 시작됩니다. 이 문제를 설명하려면 다음과 같은 간단한 패턴을 사용해야 합니다.
목록 5. Schema.sql
DROP TABLE IF EXISTS Author;CREATE TABLE Authors (id MEDIUMINT NOT NULL AUTO_INCREMENT, 이름 TEXT NOT NULL, PRIMARY KEY (id))
목록 5는 작성자 테이블이며 각 레코드에는 연관된 ID가 있습니다.
다음으로 테스트 케이스를 작성할 수 있습니다.
목록 6. TestAuthors.php
<?phprequire_once 'dblib.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestAuthors extends PHPUnit2_Framework_TestCase{ function test_delete_all() { $this->assertTrue( Authors::delete_all() ); } function test_insert() { $this->assertTrue( Authors::delete_all() ); $this->assertTrue( Authors::insert( 'Jack' ) ) } function test_insert_and_get() { $this->assertTrue( 작성자 ::delete_all() ); $this->assertTrue( Authors::insert( 'Jack' ) ) $this->assertTrue( Authors::insert( 'Joe' ) ); ; $this->assertTrue( $found != null ); $this->assertTrue( count( $found ) == 2 );
이 테스트 세트는 테이블에서 작성자를 삭제하고 테이블에 작성자를 삽입하는 방법을 다룹니다. 저자가 존재하는지 확인하면서 저자를 삽입하는 등의 기능도 있습니다. 이것은 누적 테스트로, 버그를 찾는 데 매우 유용하다고 생각합니다. 어떤 테스트가 작동하고 어떤 테스트가 작동하지 않는지 관찰하면 무엇이 잘못되었는지 빠르게 파악하고 차이점을 더 깊이 이해할 수 있습니다.
원래 오류를 발생시킨 dblib.php PHP 데이터베이스 액세스 코드의 버전은 다음과 같습니다.
목록 7. dblib.php
<?phprequire_once('DB.php'); class Authors{ public static function get_db() { $dsn = 'mysql://root:password@localhost/unitdb'; $db: :Connect( $dsn, array() ); if (PEAR::isError($db)) { die($db->getMessage()) } return $db } public static function delete_all() { return false; } public static function insert( $name ) { return false; } public static function get_all() { return null }}?>
목록 8의 코드에서 단위 테스트를 실행하면 세 가지 테스트가 모두 실패함을 알 수 있습니다.
목록 8. dblib; php
% phpunit TestAuthors.phpPHPUnit 2.2.1 by Sebastian Bergmann.FFFTime: 0.0075001716613773개의 실패가 있었습니다:1) test_delete_all(TestAuthors)2) test_insert(TestAuthors)3) test_insert_and_get(TestAuthors)FAILURES!!!테스트 실행: 3, 실패: 3, 오류: 0, 불완전한 테스트: 0.%
이제 3가지 테스트가 모두 통과할 때까지 메서드별로 데이터베이스에 올바르게 액세스하기 위한 코드 추가를 시작할 수 있습니다. dblib.php 코드의 최종 버전은 아래와 같습니다.
목록 9. 전체 dblib.php
<?phprequire_once('DB.php');class Authors{ public static function get_db() { $dsn = 'mysql://root:password@localhost/unitdb'; $db =& DB ::Connect( $dsn, array() ); if (PEAR::isError($db)) { die($db->getMessage()) } return $db } public static function delete_all(); = Authors::get_db(); $sth = $db->prepare( 'DELETE FROM Authors' ); $db->execute( $sth ) } public static function insert( $name ) { $db = Authors::get_db(); $sth = $db->prepare( 'INSERT INTO Authors (null,?)' ) $db->execute( $sth, array( $name ) ) return true; static function get_all() { $db = Authors::get_db(); $res = $db->query( "SELECT * FROM Authors" ) $rows = array() while( $res->fetchInto( $ row) ) ) { $rows []= $row; } return $rows; }}?>
HTML 테스트
전체 PHP 애플리케이션을 테스트하는 다음 단계는 프런트 엔드 HTML(Hypertext Markup Language) 인터페이스를 테스트하는 것입니다. 이 테스트를 수행하려면 아래와 같은 웹 페이지가 필요합니다.
목록 10. TestPage.php
<?phprequire_once 'HTTP/Client.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestPage는 PHPUnit2_Framework_TestCase{ function get_page( $url ) { $client = new HTTP_Client(); ->get( $url ); $resp = $client->currentResponse(); return $resp['body'] } function test_get() { $page = TestPage::get_page( 'http://localhost/unit /add.php' ); $this->assertTrue( strlen( $page ) > 0 ); $this->assertTrue( preg_match( '/<html>/', $page ) == 1 ); ) { $page = TestPage::get_page( 'http://localhost/unit/add.php?a=10&b=20' ) $this->assertTrue( strlen( $page ) > 0 ); 주장True( preg_match( '/<html>/', $page ) == 1 ); preg_match( '/<span id="result">(.*?)</span>/', $page, $out ); $this->assertTrue( $out[1]=='30' ); }}?>
이 테스트는 PEAR에서 제공하는 HTTP 클라이언트 모듈을 사용합니다. 내장된 PHP 클라이언트 URL 라이브러리(CURL)보다 조금 더 간단하지만 후자도 사용할 수 있습니다.
반환된 페이지를 확인하고 HTML이 포함되어 있는지 확인하는 테스트가 있습니다. 두 번째 테스트에서는 요청한 URL에 값을 넣어 10과 20의 합을 요청한 후 반환된 페이지에서 결과를 확인합니다.
이 페이지의 코드는 아래와 같습니다.
목록 11. TestPage.php
<html><body><form><input type="text" name="a" value="<?php echo($_REQUEST['a']); ?>" /> + <input type="text" name="b" value="<?php echo($_REQUEST['b']); ?>" /> =<span id="result"><?php echo($_REQUEST ['a']+$_REQUEST['b']) ?></span><br/><input type="submit" value="추가" /></form></body></html >
이 페이지는 매우 간단합니다. 두 개의 입력 필드에는 요청에 제공된 현재 값이 표시됩니다. 결과 범위에는 이 두 값의 합계가 표시됩니다. 마크업은 모든 차이점을 표시합니다. 사용자에게는 표시되지 않지만 단위 테스트에서는 표시됩니다. 따라서 단위 테스트에는 이 값을 찾기 위해 복잡한 논리가 필요하지 않습니다. 대신 특정 태그의 값을 검색합니다. 이러한 방식으로 인터페이스가 변경되면 범위가 존재하는 한 테스트가 통과됩니다.
이전과 마찬가지로 테스트 사례를 먼저 작성한 다음 페이지의 실패한 버전을 만듭니다. 우리는 실패 여부를 테스트한 다음 페이지 내용을 수정하여 작동하도록 합니다. 결과는 다음과 같습니다.
목록 12. 테스트 실패 후 페이지 수정
% phpunit TestPage.phpPHPUnit 2.2.1 by Sebastian Bergmann...Time: 0.25711488723755OK (2 테스트)%
두 테스트 모두 통과할 수 있습니다. 코드 잘 작동합니다.
이 코드에 대해 테스트를 실행하면 모든 테스트가 문제 없이 실행되므로 코드가 올바르게 작동한다는 것을 알 수 있습니다.
그러나 HTML 프런트 엔드 테스트에는 JavaScript라는 결함이 있습니다. HTTP(Hypertext Transfer Protocol) 클라이언트 코드가 페이지를 검색하지만 JavaScript를 실행하지는 않습니다. 따라서 JavaScript에 코드가 많으면 사용자 에이전트 수준 단위 테스트를 만들어야 합니다. 이 기능을 달성하기 위해 제가 찾은 가장 좋은 방법은 Microsoft® Internet Explorer®에 내장된 자동화 계층 기능을 사용하는 것입니다. PHP로 작성된 Microsoft Windows® 스크립트를 사용하면 COM(구성 요소 개체 모델) 인터페이스를 사용하여 Internet Explorer를 제어하여 페이지 간을 탐색한 다음 DOM(문서 개체 모델) 메서드를 사용하여 특정 사용자 작업을 수행한 후 페이지를 찾을 수 있습니다. .
이것이 제가 아는 프런트엔드 JavaScript 코드 단위 테스트에 대한 유일한 방법입니다. 작성하고 유지 관리하는 것이 쉽지 않으며 페이지에 약간의 변경만 있어도 이러한 테스트가 쉽게 중단된다는 점을 인정합니다.
어떤 테스트를 작성하고 어떻게 작성하는지
테스트를 작성할 때 다음 시나리오를 다루고 싶습니다.
모든 긍정적인 테스트
이 테스트 세트는 모든 것이 예상대로 작동하는지 확인합니다.
모든 네거티브 테스트에서는
이러한 테스트를 하나씩 사용하여 모든 오류나 이상이 테스트되었는지 확인합니다.
긍정적인 시퀀스 테스트
이 테스트 세트는 올바른 시퀀스의 호출이 예상대로 작동하는지 확인합니다.
네거티브 시퀀스 테스트
이 테스트 세트는 호출이 올바른 순서로 이루어지지 않을 때 호출이 실패하는지 확인합니다.
부하 테스트
적절한 경우 소규모 테스트를 수행하여 이러한 테스트의 성능이 예상 범위 내에 있는지 확인할 수 있습니다. 예를 들어 2,000건의 호출은 2초 이내에 완료되어야 합니다.
리소스 테스트
이러한 테스트는 API(응용 프로그래밍 인터페이스)가 리소스를 올바르게 할당하고 해제하는지 확인합니다. 예를 들어 파일 기반 API 열기, 쓰기 및 닫기를 연속해서 여러 번 호출하여 아직 열려 있는 파일이 없는지 확인합니다.
콜백 테스트
콜백 메소드가 있는 API의 경우 이러한 테스트는 콜백 함수가 정의되지 않은 경우 코드가 정상적으로 실행되는지 확인합니다. 또한 이러한 테스트를 통해 콜백 함수가 정의되었지만 이러한 콜백 함수가 잘못 작동하거나 예외를 생성할 때 코드가 계속 정상적으로 실행될 수 있는지 확인할 수도 있습니다.
단위 테스트에 대한 몇 가지 생각은 다음과 같습니다. 단위 테스트 작성 방법에 대한 몇 가지 제안 사항이 있습니다:
무작위 데이터를 사용하지 마십시오
인터페이스에서 무작위 데이터를 생성하는 것이 좋은 생각처럼 보일 수도 있지만 이 데이터는 디버그하기가 매우 어려울 수 있으므로 이를 피하고 싶습니다. 호출할 때마다 데이터가 무작위로 생성되면 한 테스트에서는 오류가 발생하고 다른 테스트에서는 오류가 발생하지 않을 수 있습니다. 테스트에 임의의 데이터가 필요한 경우 이를 파일로 생성하고 실행할 때마다 해당 파일을 사용할 수 있습니다. 이 접근 방식을 사용하면 "시끄러운" 데이터를 얻을 수 있지만 여전히 오류를 디버깅할 수 있습니다.
그룹 테스트
실행하는 데 몇 시간이 걸리는 수천 개의 테스트를 쉽게 축적할 수 있습니다. 아무런 문제가 없지만 이러한 테스트를 그룹화하면 일련의 테스트를 신속하게 실행하고 주요 문제를 확인한 다음 밤에 전체 테스트 세트를 실행할 수 있습니다.
강력한 API 및 강력한 테스트 작성
새로운 기능이 추가되거나 기존 기능이 수정될 때 쉽게 중단되지 않도록 API 및 테스트를 작성하는 것이 중요합니다. 보편적인 마법의 총알은 없지만 경험상 "진동"(가끔 실패하고 때로는 성공하고 계속해서 반복되는) 테스트는 신속하게 폐기되어야 한다는 것입니다.
결론
단위 테스트는 엔지니어에게 매우 중요합니다. 이는 민첩한 개발 프로세스의 기초입니다(문서에는 코드가 사양에 따라 작동하고 있다는 증거가 필요하기 때문에 코딩에 중점을 둡니다). 단위 테스트는 이러한 증거를 제공합니다. 프로세스는 코드가 구현해야 하지만 현재 구현하지 않는 기능을 정의하는 단위 테스트로 시작됩니다. 따라서 처음에는 모든 테스트가 실패합니다. 그런 다음 코드가 거의 완성되면 테스트가 통과됩니다. 모든 테스트를 통과하면 코드가 매우 완성됩니다.
나는 단위 테스트를 사용하지 않고 큰 코드를 작성하거나 크거나 복잡한 코드 블록을 수정한 적이 없습니다. 나는 보통 코드를 수정하기 전에 기존 코드에 대한 단위 테스트를 작성합니다. 코드를 수정할 때 무엇을 깨뜨리는지(또는 깨지지 않는지) 확인하기 위해서입니다. 이를 통해 제가 고객에게 제공한 코드가 새벽 3시에도 올바르게 실행되고 있다는 확신을 갖게 되었습니다.