2024년 6월 17일 월요일

Laravel : Test 하기

Laravel : Test 하기

Laravel 테스트 작성

Laravel에서는 UnitTest와 FeatureTest 의 두가지가 있다.

UnitTest 는 laravel 프레임워크에 의존하지 않는 순수한 php 소스에 대해 테스트한다.
FeatureTest 는 Laravel 프레임워크를 사용한 기능 테스트가 가능하다.

테스트 환경설정

테스트관련 설정값을 phpunit.xml 에서 설정한다.
기본적으로 아래와 같은 형태이다.

    <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
      xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"  
      bootstrap="vendor/autoload.php"  
      colors="true"  
    >  
     <testsuites> <testsuite name="Unit">  
     <directory suffix="Test.php">./tests/Unit</directory>  
     </testsuite> <testsuite name="Feature">  
     <directory suffix="Test.php">./tests/Feature</directory>  
     </testsuite> </testsuites> <coverage processUncoveredFiles="true">  
     <include> <directory suffix=".php">./app</directory>  
     </include> </coverage> <php> <server name="APP_ENV" value="testing"/>  
     <server name="BCRYPT_ROUNDS" value="4"/>  
     <server name="CACHE_DRIVER" value="array"/>  
      <!-- <server name="DB_CONNECTION" value="sqlite"/> -->  
     <!-- <server name="DB_DATABASE" value=":memory:"/> -->  <server name="MAIL_MAILER" value="array"/>  
     <server name="QUEUE_CONNECTION" value="sync"/>  
     <server name="SESSION_DRIVER" value="array"/>  
     <server name="TELESCOPE_ENABLED" value="false"/>  
     </php></phpunit>

testsuites 항목에 보면 Unit와 Feature 항목이 있고 각각 디렉토리와 테스트파일로 인식해서 테스트를 실행할 접미사suffix 가 있다.
이곳에 항목을 추가해서 자신만의 디렉토리에서도 테스트가 실행되도록 할수도 있다.
server name=“APP_ENV” value=“testing” 항목이 있는데 .env 파일을 테스트용으로 별도로 만들어 테스트할때는 다른 환경에서 실행되도록 할수도있다.

UnitTest

UnitTest, 단위테스트는 함수레벨에서 프로그램의 특정 부분을 검증하는것이다. 가능한 모든 클래스와 함수는 테스트가 가능해야 되며 테스트가 불가능 하거나 테스트하기 어려운 함수를 만들었다는 것은 잘못 코딩했다는 반증이기도하다.

PHP 에서는 PHPUnit 이라는 PHP 단위 테스트 패키지 를 제공한다. 이는 주로 단위 테스트에 사용된다. 즉, 가능한 가장 작은 구성 요소로 코드를 테스트할 수 있지만 매우 유연하며 단순한 단위 테스트 이상의 용도로 사용할 수 있다.

Laravel에서는 PHPUnit 테스트를 확장한 TestCase라는 클래스를 제공하여 사용자의 테스트 클래스에서 상속받아 테스트를 진행한다.

class ExampleTest extends TestCase  { 
	public function test_example()  
	{  
	    $this->assertTrue(true);  
	}
}

Laravel 설치시에 위와같은 테스트 샘플이 있으므로 한번 테스트를실행해보자.

    php artisan test 

결과
   PASS  Tests\Unit\ExampleTest
  ✓ example
   PASS  Tests\Feature\ExampleTest
  ✓ example

  Tests:  2 passed
  Time:   0.03s

artisan 의 test 명령을 이용하면 Unit 테스트 와 Feature 테스트 전부 실행된다.
Unit 만 실행하고 싶을때는 아래와 같이 한다.

php artisan test --testsuite=Unit

또는 하나의 파일만 테스트 할수도 있다.

php artisan test tests/Unit/ExampleTest.php

FeatureTest ( 애플리케이션 테스트)

여러 개의 코드 유닛이 함께 작동하여 시스템의 특정 기능이 예상대로 동작하는지 검증하는 테스트이다. 주로 컨트롤러, 라우팅, 데이터베이스 쿼리 등을 포함한 여러 시스템 요소를 통합적으로 테스트한다.

Http 나 데이터 베이스 접속 등을 이용한 테스트가 가능하다.

use RefreshDatabase; // use RefreshDatabase 는 테스트 할때 저장한 데이터를 테스트 종료와 함께 삭제해준다.

public function testUser()
{
    $user = User::factory()->create(); //$user 데이터 생성

    $response = $this->actingAs($user)  //actingAs 함수을 사용하여 사용자 인증을 처리
                     ->getJson('/api/users');  //getJson 함수을 사용하여 JSON 응답받음

    $response->assertStatus(200)
             ->assertJson(['data' => true]); //assertJson  추가 검증
}

Feature 만 실행하고 싶을때는 아래와 같이 한다.

php artisan test --testsuite=Feature

Mock 테스트

모의 테스트(Mock Testing) 은 테스트 대상이 되는 클래스나 함수에 영향을 줄만한 다른 함수의 결과치를 모의(Mocking) 해서 지정된 결과치가 나오도록 하여 실제로 테스트 대상 함수가 해당 결과치를 이용한 수행결과가 테스트결과 예상치와 같은 지 확인하기 위해서 사용한다.

즉, foo() 함수를 실행하는데 foo 함수내부에서는 bar 를 사용한 결과를 가지고 추가 처리를 하여 최종 결과를 리턴하는 기능을 할때, foo 함수를 테스트 할때 bar 결과치에 따라 달라지거나, 실패가 발생할때 원인이 bar 에 의한 건지 foo 에 의한 건지 모호할때가 있다.

이때 bar 함수를 테스트에 필요한 지정된 값만 출력되도록 모의(mocking) 하여 foo 함수가 해당값을 이용하여 처리한 결과가 성공인지 판단 할수 있도록 해준다.

샘플을 보자

namespace App\Services;

class FooBarService
{
    public function bar()
    {
        // 복잡한 로직이나 외부 서비스 호출 등
        return 'real result';
    }

    public function foo()
    {
        $barResult = $this->bar();
        // bar의 결과에 따라 foo의 동작이 달라짐
        if ($barResult === 'real result') {
            return 'foo with real result';
        }
        
        return 'foo with different result';
    }
}

테스트 해보자

    class FooBarServiceTest extends TestCase
{
    public function testFooWithMockedBar()
    {
        // FooBarService의 Mock 객체 생성
        $fooBarServiceMock = $this->getMockBuilder(FooBarService::class)
            ->onlyMethods(['bar'])
            ->getMock();

        // bar 메서드가 호출되면 'mocked result'를 반환하도록 설정
        $fooBarServiceMock->method('bar')->willReturn('mocked result');

        // foo 메서드 실행
        $result = $fooBarServiceMock->foo();

        // foo 메서드가 예상하는 결과를 반환하는지 확인
        $this->assertEquals('foo with different result', $result);
    }
}

실행해 보면 아래 처럼 테스트가 성공한다.

   PASS  Tests\Unit\FooBarServiceTest
  ✓ foo with mocked bar

  Tests:  1 passed
  Time:   0.35s

getMockBuilder 로 FooBarService 클래스의 bar() 함수만 모의로 생성한다음 willReturn 으로 ‘mock result’ 라는 고정된 결과가 나오도록 하여 우리가 테스트할 foo 에서 해당 값을 가지고 테스트 하다록 하였다.
foo 에서는 ‘mock result’ 라는 인수가 들어올때 'foo with different result’라는 결과를 출력하므로 참이 되어 테스트가 성공했다.

createMock & getMockBuilder

테스트대상 이외의 클래스나 함수에 대해 지정된 모의(mock)결과를 리턴하도록 도와주는 함수들이다.

createMock : 지정한 클래스에 대해 클래스내의 public 함수에 대해 무조건 null을 리턴하는 모의 class를 만들어준다.

$stub = $this->createMock(SomeClass::class);

getMockBuilder 는 여러가지 설정사항을 직접 선택하여 모의객체를 생성할수있다.

        $mock = $this->getMockBuilder(SomeClass::class)
                 ->onlyMethods(['method1'])
                 ->disableOriginalConstructor() 
                 ->setConstructorArgs(['arg1', 'arg2'])
                 ->disableOriginalClone()
                 ->disableAutoload()
                 ->enableArgumentCloning()
                 ->getMock();

생성자실행안함, 특정함수만 이용,인수지정 등을 사용할수있다.

will

모의객체나 함수에 대해 리턴값을 지정할수있다. 리턴값을 지정함으로서 실제로 테스트 대상이 되는 함수에 대해 신뢰성있는 테스트 결과를 얻을수있다.

$mock->method('fuga')->will($this->returnValue('foo'));

wil 은
will( this>returnValue(this->returnValue(value)) : 반환값을 설정
will( this>returnArgument(this->returnArgument(num)) : 메서드에 전달된 $num 번째 인수를 반환 값으로 설정
will( $this->returnSelf()) : 반환 값에 모의객체 자체를 설정
will( $this->throwException(예외)) : 반환값에 예외값를 설정
등으로 사용하는데 좀더 사용하기 편리하도록 함수화 한것들도 있다.

willReturn : 특정 값을 반환하도록 설정

$mock->method('methodName')->willReturn('value');

willReturnMap :인자 값에 따라 다른 반환 값을 설정

$map = [
    ['param1', 'result1'],
    ['param2', 'result2']
];
$mock->method('methodName')->willReturnMap($map);

willReturnArgument : 특정 인자 값을 그대로 반환하도록 설정

$mock->method('methodName')->willReturnArgument(0); // 첫 번째 인자 값을 반환

willReturnCallback : 콜백 함수를 사용하여 반환 값을 동적으로 설정

$mock->method('methodName')->willReturnCallback(function ($arg) {
    return $arg . ' processed';
});

willReturnSelf
설명: 호출된 객체 자체를 반환하도록 설정. 메서드 체이닝에 유용.

$mock->method('methodName')->willReturnSelf();

willReturnOnConsecutiveCalls 설명: 메서드가 연속적으로 호출될 때 각각 다른 값을 반환하도록 설정

$mock->method('methodName')
     ->willReturnOnConsecutiveCalls('first', 'second', 'third');

Assert

테스트 대상 함수가 예상결과와 같은지 판단하는 함수가 Assert 함수이다. 테스트 대상 함수의 결과치가 여러가지 인 만큼 Assert 도 다양하다.

  • assertEquals() : 값이 같은지 확인. assertEquals(5, $actual);
  • assertSame() : 값과 형식도 같은지 확인. assertSame(5, $actual);
  • assertNotSame() : 값과 형식도 다른지 확인. assertNotSame(5, $actual);
  • assertTrue() / assertFalse() : 불린값이 같은지 확인. assertTrue($actual);
  • assertInstanceOf() : 인스턴스가 같은지 확인. assertInstanceOf(Foo::class, $actual);
  • assertNull(): 값이 null 인지 확인 assertNull($actual);
  • assertNotNull(): 값이 null 이 아닌지 확인 assertNotNull($actual);
  • assertStringStartsWith() / assertStringEndsWith() / assertStringContainsString() : 값의 문자열중에 시작문자, 종료문자, 포함문자가 있는지 확인. assertStringStartsWith(‘foo’, $actual);
  • assertCount() : 배열인수의 개수가 같은지 확인. assertCount(1, $actual)
  • assertEmpty() : 배열이 빈 배열인지 확인. assertEmpty($actual)
  • assertContains() : 배열에 해당 아이템이 포함되었는지 확인. assertContains(‘foo’, $actual)

Laravel에서는 아래와 같은 Assert 도 추가되어 있다.

  • assertPageLoaded($uri, $message = null) :최신 페이지가 로드되었는지 확인한다… URL/메시지가 존재하지 않으면 오류가 보고된다.
  • assertResponseOk() : 페이지 응답이 정상인지 확인
  • assertReponseStatus($code): 지정된 코드에 응답한지 여부
  • assertViewHas($key, $value = null): 지정된 데이터가 뷰에 존재하는지 여부
  • assertViewHasAll($bindings):지정된 데이터 계열이 뷰에 존재하는지 여부
  • assertViewMissing($key):이 데이터가 뷰에 존재하지 않는지 여부를 지정
  • assertRedirectedTo($uri, $with = []):지정된 URI로 리디렉션할지 확인
  • assertRedirectedToRoute($name, $parameters = [], $with = []):클라이언트가 지정된 경로로 리디렉션되는지 여부
  • assertRedirectedToAction($name, $parameters = [], $with = []):작업으로 리디렉션할지 여부
  • assertSessionHas($key, $value = null):세션에 키/값이 존재하는지 여부
  • assertSessionHasAll($bindings):지정된 kv가 세션에 존재하는지 여부
  • assertSessionHasErrors($bindings = []):세션에 오류가 있는 걸까요?
  • assertHasOldInput():세션에 이전 데이터가 있는지 여부
    등등 많이 있다.

테스트 함수의 맨 마지막에 적어준다.
실패의 경우에는 예상치과 실제값이 어떻게 다른가 표시해준다.

  $this->assertEquals('foo with different result', $result);

어노테이션 기능

PHPUnit 에서는 여러가지 어노테이션을 제공해서 테스트 함수에 주석으로 표기하면 PHPUnit이 읽어들여 적절한 액션을 취해준다.

@depends : 현재 테스트가 다른 테스트의 결과를 받아서 처리해야할 필요가 있을때 지정한다.

class ExampleTest extends TestCase
{
   public function testEmpty()
   {
       $stack = array();
       $this->assertEmpty($stack);
       return $stack;
   }
   /**
    * @depends testEmpty
    */
   public function testPush(array $stack)
   {
       array_push($stack, 'foo');
       $this->assertEquals('foo', $stack[count($stack)-1]);
       $this->assertNotEmpty($stack);
       return $stack;
   }
   /**
    * @depends testPush
    */
   public function testPop(array $stack)
   {
       $this->assertEquals('foo', array_pop($stack));
       $this->assertEmpty($stack);
   }
}

@depends 의 대상이 되는 함수에는 리턴값이 있고 ,이를 depends하는 함수가 인수로서 받아서 테스트에 이용하는 방식이다.

@dataProvider : 테스트 대상 함수에 인수를 전달 하는 것이 가능하다. 테스트 함수에 인수를 다양하게 지정하여 테스트 대상함수가 해당 인수로 성공하는 지 알아 볼때 편리하다.

class ExampleTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAppend($a, $b, $expected)
    {
        $result = $a . $b;
        $this->assertEquals($expected, $result);
    }

    public function additionProvider()
    {
        return [
            ['Hello, ', 'World!', 'Hello, World!'],
            ['This ', 'is a ', 'This is a '],
            ['PHP ', 'unit test', 'PHP unit test'],
        ];
    }
}

@testWith : dataProvider와 비슷하지만 좀더 간단하게 지정할수 있다.

class ExampleTest extends TestCase
{
    /**
     * @testWith ["Hello", "World", "HelloWorld"]
     *           ["This", " is a ", "This is a "]
     *           ["PHP", " unit test", "PHP unit test"]
     */
    public function testConcatenateStrings($a, $b, $expected)
    {
        $result = $a . $b;
        $this->assertEquals($expected, $result);
    }
}

@group : 그룹명을 지정하여 테스트 시에 선택적으로 해당 그룹만 실행할수있다.

 /**  
 * @group mytest  
 */
 public function test_1(){}
 
 public function test_2(){}
 
 /**  
 * @group mytest  
 */
 public function test_3(){}

php artisan test --group mytest 을 실행하면 test_1(), test_3() 만 실행된다.

0 comments:

댓글 쓰기