출력이 불확실한 단위 테스트 방법


37

임의 길이의 임의 암호를 생성하지만 정의 된 최소 길이와 최대 길이 사이로 제한되는 클래스가 있습니다.

나는 단위 테스트를 만들고 있는데,이 수업에서 재미있는 작은 걸림돌에 부딪쳤다. 단위 테스트의 기본 개념은 반복 가능해야한다는 것입니다. 테스트를 백 번 실행하면 동일한 결과가 백 번 나옵니다. 예상되거나 초기 상태에 있거나 없을 수있는 일부 리소스에 의존하는 경우 테스트가 실제로 항상 반복 가능하도록 해당 리소스를 조롱해야합니다.

그러나 SUT가 불확실한 출력을 생성해야하는 경우는 어떻습니까?

최소 및 최대 길이를 동일한 값으로 고정하면 생성 된 비밀번호가 예상 길이인지 쉽게 확인할 수 있습니다. 그러나 허용 가능한 길이 (예 : 15-20 문자) 범위를 지정하면 테스트를 100 번 실행하고 100 패스를 얻을 수 있지만 101 번째 실행에서는 9 문자열을 다시 얻을 수 있다는 문제가 있습니다.

핵심적으로 상당히 간단한 암호 클래스의 경우 큰 문제를 나타내지 않아야합니다. 그러나 그것은 일반적인 경우에 대해 생각하게했습니다. 설계 상 불확실한 출력을 생성하는 SUT를 처리 할 때 일반적으로 가장 적합한 전략은 무엇입니까?


9
마감 투표가 왜 필요한가요? 나는 그것이 완전히 유효한 질문이라고 생각합니다.
Mark Baker

허, 댓글 주셔서 감사합니다. 눈치 채지 못했지만 지금은 똑같은 것을 궁금합니다. 내가 생각할 수있는 것은 특정 사례가 아닌 일반적인 사례에 관한 것이지만 위에서 언급 한 비밀번호 클래스의 소스를 게시하고 "어떻게 클래스를 테스트합니까?" 대신 "불확실한 클래스를 어떻게 테스트합니까?"
GordonM

1
@MarkBaker 대부분의 단위 테스팅 질문은 programmers.se에 있기 때문입니다. 질문을 끝내지 않고 이주에 대한 투표입니다.
Ikke

답변:


20

"비결정론 적"결과는 단위 테스트 목적으로 결정론적인 방법이 있어야합니다. 임의성을 처리하는 한 가지 방법은 임의 엔진을 교체하는 것입니다. 다음은 예입니다 (PHP 5.3+).

function DoSomethingRandom($getRandomIntLessThan)
{
    if ($getRandomIntLessThan(2) == 0)
    {
        // Do action 1
    }
    else
    {
        // Do action 2
    }
}

// For testing purposes, always return 1
$alwaysReturnsOne = function($n) { return 1; };
DoSomethingRandom($alwaysReturnsOne);

테스트가 완전히 반복 가능한지 확인하려는 숫자 시퀀스를 반환하는 함수의 특수 테스트 버전을 만들 수 있습니다. 실제 프로그램에서 재정의되지 않으면 대체가 될 수있는 기본 구현을 가질 수 있습니다.


1
주어진 모든 대답에는 내가 사용한 좋은 제안이 있었지만 이것이 핵심 문제를 해결하여 동의를 얻는 것으로 생각되는 것입니다.
GordonM

1
머리에 못을 박습니다. 비 결정적이지만 여전히 경계가 있습니다.
surfasb

21

메소드가 실행될 때마다 실제 출력 비밀번호가 결정되지 않을 수 있지만, 최소 길이, 결정 문자 세트에 포함 된 문자 등과 같이 테스트 할 수있는 결정 기능이 여전히 있습니다.

암호 생성기를 매번 같은 값으로 시드하여 루틴이 결정 결과를 반환하는지 테스트 할 수도 있습니다.


PW 클래스는 암호를 생성해야하는 문자 풀인 상수를 유지합니다. 서브 클래 싱하고 단일 문자로 상수를 재정의함으로써 테스트 목적으로 비결 정성의 한 영역을 제거했습니다. 감사합니다.
GordonM

14

"계약"에 대해 테스트하십시오. 메소드가 "z를 사용하여 15-20 자 길이의 비밀번호 생성"으로 정의 된 경우 다음과 같이 테스트하십시오.

$this->assertTrue ((bool) preg_match('^[a-z]{15,20}$', $password));

또한 생성을 추출 할 수 있으므로 생성 된 모든 것이 다른 "정적"생성기 클래스를 사용하여 테스트 할 수 있습니다.

class RandomGenerator implements PasswordGenerator {
  public function create() {
    // Create $rndPwd
    return $rndPwd;
  }
}

class StaticGenerator implements PasswordGenerator {
  private $pwd;
  public function __construct ($pwd) { $this->pwd = $pwd; }
  public function create      ()     { return $this->pwd; }
}

당신이 준 정규 표현식이 유용하다는 것을 증명했기 때문에 테스트에 수정 된 버전을 포함 시켰습니다. 감사.
GordonM

6

당신은 Password generator있고 당신은 임의의 소스가 필요합니다.

질문에 언급했듯이 전역 상태random 이므로 비 결정적 출력을 만듭니다 . 의미는 시스템 외부의 무언가에 액세스하여 값을 생성한다는 의미입니다.

모든 클래스에서 이와 같은 것을 제거 할 수는 없지만 임의의 값을 만들기 위해 암호 생성을 분리 할 수 ​​있습니다.

<?php
class PasswordGenerator {

    public function __construct(RandomSource $randomSource) {
        $this->randomSource = $randomSource
    }

    public function generatePassword() {
        $password = '';
        for($length = rand(10, 16); $length; $length--) {
            $password .= $this-toChar($this->randomSource->rand(1,26));
        }
    }

}

이와 같은 코드를 구성하면 RandomSource테스트를 위해 모형을 만들 수 있습니다 .

100 % 테스트 할 수는 RandomSource없지만이 질문의 값을 테스트하기 위해 얻은 제안을 적용 할 수 있습니다 (테스트는 rand->(1,26);항상 1에서 26 사이의 숫자를 반환합니다).


좋은 답변입니다.
Nick Hodges

3

입자 물리 몬테 카를로 (Monte Carlo)의 경우 , 미리 설정된 랜덤 시드 로 비 결정적 루틴을 호출하고 통계 횟수를 실행하고 제약 조건 위반 (에너지 수준)을 확인하는 "단위 테스트"{*}를 작성했습니다. 입력 에너지 이상으로 접근 할 수 없어야하며, 모든 패스는 일정 수준 등을 선택해야합니다.) 이전에 기록 된 결과에 대한 회귀.


{*} 이러한 테스트는 단위 테스트에 대한 "테스트를 빠르게"원칙을 위반하므로 다른 방법 (예 : 승인 테스트 또는 회귀 테스트)으로 더 나은 특성을 느낄 수 있습니다. 여전히 단위 테스트 프레임 워크를 사용했습니다.


3

다음 두 가지 이유로 허용되는 답변에 동의하지 않아야합니다.

  1. 과적 합
  2. 실행 불가능

(주의 사항은 그 전부는 아니지만, 많은 상황에서 좋은 답변, 그리고 아마 대부분입니다.)

그게 무슨 뜻입니까? 음,에 의해 overfitting 나는 통계 테스트의 일반적인 문제를 의미 : 당신은 데이터의 지나치게 제약 집합에 대해 확률 적 알고리즘을 테스트 할 때 overfitting가 발생합니다. 그런 다음 알고리즘을 다시 수정하면 암시 적으로 학습 데이터에 잘 맞도록 만들 수 있지만 (실수로 알고리즘을 테스트 데이터에 맞출 수는 있지만) 다른 모든 데이터는 전혀 테스트되지 않기 때문에 전혀 그렇지 않을 수 있습니다. .

(우연히, 이것은 항상 단위 테스트에 숨어있는 문제입니다. 이것이 좋은 테스트가 완료 되었거나 적어도 주어진 단위에 대해 대표적 이며 이것이 일반적으로 어려운 이유 입니다.)

난수 생성기를 플러그 가능하게하여 테스트를 결정 론적으로 만들면 항상 동일하고 대표적이지 않은 데이터 세트 에 대해 테스트해야 합니다. 이로 인해 데이터가 왜곡되어 기능이 편향 될 수 있습니다.

확률론은 두 번째 요점은 확률 변수를 제어 할 수 없을 때 발생합니다. 이것은 일반적으로 난수 생성기에서 발생하지 않지만 (“실제”난수 소스가 필요하지 않는 한) 확률 론자들이 다른 방법으로 문제에 몰래 들어갈 때 발생할 수 있습니다. 예를 들어, 동시 코드를 테스트 할 때 : 경쟁 조건은 항상 확률 적이므로 결정하기 쉽게 만들 수 없습니다 .

이러한 경우에 대한 신뢰를 높이는 유일한 방법 은 많이 테스트 하는 것 입니다. 오히려 헹구고 반복하십시오. 이를 통해 특정 수준까지 신뢰를 높일 수 있습니다 (추가 테스트 실행에 대한 트레이드 오프는 무시할 수 있음).


2

실제로 여기에는 여러 가지 책임이 있습니다. 단위 테스트와 특히 TDD는 이런 종류의 것을 강조하는 데 좋습니다.

책임은 다음과 같습니다.

1) 난수 생성기. 2) 비밀번호 포맷터.

비밀번호 포맷터는 난수 생성기를 사용합니다. 생성자를 인터페이스로 통해 생성기를 포맷터에 삽입하십시오. 이제 난수 생성기 (통계 테스트)를 완전히 테스트 할 수 있으며 모의 난수 생성기를 주입하여 포맷터를 테스트 할 수 있습니다.

더 나은 코드를 얻을뿐만 아니라 더 나은 테스트를받을 수 있습니다.


2

다른 사람들이 이미 언급했듯이 임의성을 제거 하여이 코드 를 단위 테스트 합니다.

또한 난수 생성기를 그대로두고 계약 (암호 길이, 허용되는 문자 등) 만 테스트하고 실패시 시스템을 재현 할 수있는 충분한 정보를 덤프하는 상위 레벨 테스트를 원할 수도 있습니다. 무작위 테스트가 실패한 인스턴스의 상태.

테스트 자체가 한 번 실패한 이유를 찾을 수있는 한 테스트 자체는 반복 할 수 없습니다.


2

의존성을 줄이기 위해 코드를 리팩터링하면 많은 단위 테스트 어려움이 사소 해집니다. 데이터베이스, 파일 시스템, 사용자 또는 귀하의 경우 무작위 소스.

또 다른 방법은 단위 테스트가 "이 코드가 의도 한대로 작동합니까?"라는 질문에 대답해야한다는 것입니다. 귀하의 경우 코드가 결정적이지 않기 때문에 코드의 의도를 모릅니다.

이를 염두에두고 로직을 작고 이해하기 쉽고 테스트하기 쉬운 절연 부품으로 분리하십시오. 특히, 임의의 소스를 입력으로 사용하고 암호를 출력으로 생성하는 고유 한 메소드 (또는 클래스!)를 작성합니다. 그 코드는 분명히 결정적입니다.

단위 테스트에서는 매번 동일한 비 무작위 입력을 공급합니다. 아주 작은 랜덤 스트림의 경우 테스트 값을 하드 코딩하십시오. 그렇지 않으면 테스트에서 RNG에 일정한 시드를 제공하십시오.

더 높은 수준의 테스트 ( "수락"또는 "통합"등)에서는 코드를 실제 임의 소스로 실행하도록 할 수 있습니다.


이 대답은 나를 위해 그것을 못 박았다 : 나는 실제로 두 가지 기능을 가지고 있었다 : 난수 생성기와 그 난수로 무언가를 한 함수. 간단히 리팩토링하여 이제 코드의 비 결정적 부분을 쉽게 테스트하고 임의 부분에서 생성 한 매개 변수를 제공 할 수 있습니다. 좋은 점은 단위 테스트에서 고정 된 매개 변수 (다른 세트)를 공급할 수 있다는 것입니다 (표준 라이브러리의 난수 생성기를 사용하고 있으므로 단위 테스트는 아닙니다).
neuronet

1

위의 답변 중 대부분은 난수 생성기를 조롱하는 것이 좋습니다. 그러나 단순히 내장 mt_rand 함수를 사용하고있었습니다. 조롱을 허용한다는 것은 생성시에 난수 생성기가 주입되도록 클래스를 다시 작성하는 것을 의미했을 것입니다.

또는 나는 생각했다!

네임 스페이스 추가의 결과 중 하나는 PHP 함수에 내장 된 조롱이 엄청나게 단단해 졌다는 것입니다. SUT가 지정된 네임 스페이스에 있으면 해당 네임 스페이스 아래의 단위 테스트에서 고유 한 mt_rand 함수를 정의하기 만하면 테스트 기간 동안 내장 PHP 함수 대신 사용됩니다.

최종 테스트 스위트는 다음과 같습니다.

namespace gordian\reefknot\util;

/**
 * The following function will take the place of mt_rand for the duration of 
 * the test.  It always returns the number exactly half way between the min 
 * and the max.
 */
function mt_rand ($min = 42, $max = NULL)
{
    $min    = intval ($min);
    $max    = intval ($max);

    $max    = $max < $min? $min: $max;
    $ret    = round (($max - $min) / 2) + $min;

    //fwrite (STDOUT, PHP_EOL . PHP_EOL . $ret . PHP_EOL . PHP_EOL);
    return ($ret);
}

/**
 * Override the password character pool for the test 
 */
class PasswordSubclass extends Password
{
    const CHARLIST  = 'AAAAAAAAAA';
}

/**
 * Test class for Password.
 * Generated by PHPUnit on 2011-12-17 at 18:10:33.
 */
class PasswordTest extends \PHPUnit_Framework_TestCase
{

    /**
     * @var gordian\reefknot\util\Password
     */
    protected $object;

    const PWMIN = 15;
    const PWMAX = 20;

    /**
     * Sets up the fixture, for example, opens a network connection.
     * This method is called before a test is executed.
     */
    protected function setUp ()
    {
    }

    /**
     * Tears down the fixture, for example, closes a network connection.
     * This method is called after a test is executed.
     */
    protected function tearDown ()
    {

    }

    public function testGetPassword ()
    {
        $this -> object = new PasswordSubclass (self::PWMIN, self::PWMAX);
        $pw = $this -> object -> getPassword ();
        $this -> assertTrue ((bool) preg_match ('/^A{' . self::PWMIN . ',' . self::PWMAX . '}$/', $pw));
        $this -> assertTrue (strlen ($pw) >= self::PWMIN);
        $this -> assertTrue (strlen ($pw) <= self::PWMAX);
        $this -> assertTrue ($pw === $this -> object -> getPassword ());
    }

    public function testGetPasswordFixedLen ()
    {
        $this -> object = new PasswordSubclass (self::PWMIN, self::PWMIN);
        $pw = $this -> object -> getPassword ();
        $this -> assertTrue ($pw === 'AAAAAAAAAAAAAAA');
        $this -> assertTrue ($pw === $this -> object -> getPassword ());
    }

    public function testGetPasswordFixedLen2 ()
    {
        $this -> object = new PasswordSubclass (self::PWMAX, self::PWMAX);
        $pw = $this -> object -> getPassword ();
        $this -> assertTrue ($pw === 'AAAAAAAAAAAAAAAAAAAA');
        $this -> assertTrue ($pw === $this -> object -> getPassword ());
    }

    public function testInvalidLenThrowsException ()
    {
        $exception  = NULL;
        try
        {
            $this -> object = new PasswordSubclass (self::PWMAX, self::PWMIN);
        }
        catch (\Exception $e)
        {
            $exception  = $e;
        }
        $this -> assertTrue ($exception instanceof \InvalidArgumentException);
    }
}

PHP 내부 함수를 재정의하는 것이 단순히 나에게 발생하지 않은 네임 스페이스의 또 다른 용도이기 때문에 이것을 언급한다고 생각했습니다. 도움을 주신 모든 분들께 감사드립니다.


0

이 상황에 포함시켜야 할 추가 테스트가 있으며 이는 암호 생성기에 대한 반복 호출이 실제로 다른 암호를 생성하는지 확인하는 것입니다. 스레드 안전 암호 생성기가 필요한 경우 여러 스레드를 사용하여 동시 호출을 테스트해야합니다.

이것은 기본적으로 임의의 함수를 올바르게 사용하고 모든 호출에서 다시 시드하지 않도록합니다.


실제로 클래스는 getPassword ()에 대한 첫 번째 호출에서 비밀번호가 생성 된 후 래치되도록 설계되어 오브젝트의 수명 동안 항상 동일한 비밀번호를 리턴합니다. 내 테스트 스위트는 이미 동일한 비밀번호 인스턴스에서 getPassword ()에 대한 여러 호출이 항상 동일한 비밀번호 문자열을 리턴하는지 확인합니다. 스레드 안전성에 관해서는 PHP에서 실제로 문제가되지 않습니다 :)
GordonM
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.