Phương pháp thử nghiệm đơn vị với đầu ra không xác định


37

Tôi có một lớp có nghĩa là tạo một mật khẩu ngẫu nhiên có độ dài cũng ngẫu nhiên, nhưng giới hạn ở giữa độ dài tối thiểu và tối đa xác định.

Tôi đang xây dựng các bài kiểm tra đơn vị, và gặp phải một trở ngại nhỏ thú vị với lớp này. Toàn bộ ý tưởng đằng sau một bài kiểm tra đơn vị là nó phải được lặp lại. Nếu bạn chạy thử nghiệm một trăm lần, nó sẽ cho kết quả tương tự một trăm lần. Nếu bạn phụ thuộc vào một số tài nguyên có thể có hoặc không có hoặc có thể ở trạng thái ban đầu mà bạn mong đợi thì bạn có nghĩa là phải chế giễu tài nguyên được đề cập để đảm bảo rằng bài kiểm tra của bạn thực sự luôn lặp lại.

Nhưng những gì về trường hợp SUT được cho là tạo ra đầu ra không xác định?

Nếu tôi sửa độ dài tối thiểu và tối đa thành cùng một giá trị thì tôi có thể dễ dàng kiểm tra xem mật khẩu được tạo có độ dài dự kiến ​​hay không. Nhưng nếu tôi chỉ định một phạm vi độ dài chấp nhận được (giả sử 15 - 20 ký tự), thì bây giờ bạn có vấn đề là bạn có thể chạy thử nghiệm hàng trăm lần và nhận được 100 lượt nhưng trong lần chạy thứ 101, bạn có thể lấy lại chuỗi 9 ký tự.

Trong trường hợp lớp mật khẩu, khá đơn giản ở cốt lõi của nó, nó không nên chứng minh một vấn đề lớn. Nhưng nó khiến tôi suy nghĩ về trường hợp chung. Chiến lược thường được chấp nhận là chiến lược tốt nhất để thực hiện khi xử lý các SUT đang tạo ra sản lượng không xác định theo thiết kế là gì?


9
Tại sao phiếu bầu chặt chẽ? Tôi nghĩ đó là một câu hỏi hoàn toàn hợp lệ.
Mark Baker

Huh, cảm ơn vì nhận xét. Thậm chí không nhận thấy điều đó, nhưng bây giờ tôi đang tự hỏi điều tương tự. Điều duy nhất tôi có thể nghĩ là đó là về một trường hợp chung chứ không phải là một trường hợp cụ thể, nhưng tôi chỉ có thể đăng nguồn cho lớp mật khẩu được đề cập ở trên và hỏi "Làm thế nào để tôi kiểm tra lớp đó?" thay vì "Làm thế nào để tôi kiểm tra bất kỳ lớp không xác định?"
GordonM

1
@MarkBaker Bởi vì hầu hết các câu hỏi khó nhất là về lập trình viên. Đó là một cuộc bỏ phiếu cho di cư, không phải để đóng câu hỏi.
Ikke

Câu trả lời:


20

Đầu ra "không xác định" nên có cách trở thành xác định cho mục đích thử nghiệm đơn vị. Một cách để xử lý ngẫu nhiên là cho phép thay thế động cơ ngẫu nhiên. Đây là một ví dụ (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);

Bạn có thể tạo một phiên bản thử nghiệm chuyên biệt của hàm trả về bất kỳ chuỗi số nào bạn muốn để đảm bảo kiểm tra có thể lặp lại hoàn toàn. Trong chương trình thực, bạn có thể có một cài đặt mặc định có thể là dự phòng nếu không bị ghi đè.


1
Tất cả các câu trả lời được đưa ra đều có những gợi ý hay mà tôi đã sử dụng, nhưng đây là câu hỏi mà tôi nghĩ là vấn đề cốt lõi để nó được chấp nhận.
GordonM

1
Khá nhiều đinh nó trên đầu. Trong khi không xác định, vẫn có ranh giới.
Surfasb

21

Mật khẩu đầu ra thực tế có thể không được xác định mỗi lần phương thức được thực thi, nhưng nó vẫn sẽ có các tính năng xác định có thể được kiểm tra, chẳng hạn như độ dài tối thiểu, các ký tự nằm trong một bộ ký tự xác định, v.v.

Bạn cũng có thể kiểm tra rằng thường trình trả về một kết quả xác định mỗi lần bằng cách gieo hạt tạo mật khẩu của bạn với cùng một giá trị mỗi lần.


Lớp PW duy trì một hằng số về cơ bản là nhóm ký tự cần tạo mật khẩu. Bằng cách phân lớp nó và ghi đè hằng số với một ký tự duy nhất, tôi đã quản lý để loại bỏ một khu vực không xác định cho mục đích thử nghiệm. Cảm ơn
GordonM

14

Thử nghiệm đối với "hợp đồng". Khi các phương thức được định nghĩa là "tạo mật khẩu có độ dài từ 15 đến 20 ký tự bằng az", hãy kiểm tra theo cách này

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

Ngoài ra, bạn có thể trích xuất thế hệ, vì vậy mọi thứ, dựa vào nó, có thể được kiểm tra bằng cách sử dụng một lớp trình tạo "tĩnh" khác

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; }
}

Regex bạn đưa ra tỏ ra hữu ích vì vậy tôi đã đưa phiên bản tinh chỉnh vào thử nghiệm của mình. Cảm ơn.
GordonM

6

Bạn có một Password generatorvà bạn cần một nguồn ngẫu nhiên.

Như bạn đã nêu trong câu hỏi, randomlàm cho đầu ra không xác định vì nó là trạng thái toàn cầu . Có nghĩa là nó truy cập một cái gì đó bên ngoài hệ thống để tạo ra các giá trị.

Bạn không bao giờ có thể loại bỏ một cái gì đó như thế cho tất cả các lớp của mình nhưng bạn có thể tách việc tạo mật khẩu để tạo các giá trị ngẫu nhiên.

<?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));
        }
    }

}

Nếu bạn cấu trúc mã như thế này, bạn có thể giả định RandomSourcecho các thử nghiệm của mình.

Bạn sẽ không thể kiểm tra 100% RandomSourcenhưng những gợi ý bạn nhận được để kiểm tra các giá trị trong câu hỏi này có thể được áp dụng cho nó (Giống như kiểm tra rand->(1,26);luôn trả về một số từ 1 đến 26.


Đó là một câu trả lời tuyệt vời.
Nick Hodges

3

Trong trường hợp vật lý hạt Monte Carlo, tôi đã viết "bài kiểm tra đơn vị" {*} gọi thói quen không xác định bằng hạt ngẫu nhiên đặt trước , sau đó chạy số lần thống kê và kiểm tra vi phạm các ràng buộc (mức năng lượng trên mức năng lượng đầu vào phải không thể truy cập được, tất cả các lượt phải chọn một số mức, v.v.) và hồi quy so với kết quả đã ghi trước đó ..


{*} Thử nghiệm như vậy vi phạm nguyên tắc "làm cho thử nghiệm nhanh" đối với thử nghiệm đơn vị, do đó bạn có thể cảm thấy tốt hơn khi mô tả chúng theo một cách khác: ví dụ thử nghiệm chấp nhận hoặc thử nghiệm hồi quy. Tuy nhiên, tôi đã sử dụng khung thử nghiệm đơn vị của tôi.


3

Tôi phải không đồng ý với câu trả lời được chấp nhận , vì hai lý do:

  1. Quá mức
  2. Không thể tưởng tượng được

(Lưu ý rằng nó có thể là một câu trả lời tốt trong nhiều trường hợp, nhưng không phải trong tất cả, và có thể không phải trong hầu hết.)

Vậy ý của tôi là gì? Vâng, bằng cách quá mức, tôi có nghĩa là một vấn đề điển hình của kiểm tra thống kê: quá mức xảy ra khi bạn kiểm tra thuật toán ngẫu nhiên chống lại một tập hợp dữ liệu bị hạn chế quá mức. Nếu sau đó bạn quay lại và tinh chỉnh thuật toán của mình, bạn sẽ hoàn toàn làm cho nó phù hợp với dữ liệu huấn luyện (bạn vô tình khớp thuật toán của bạn với dữ liệu thử nghiệm), nhưng tất cả các dữ liệu khác có thể không hoàn toàn (vì bạn không bao giờ kiểm tra chống lại nó) .

(Ngẫu nhiên, đây luôn là vấn đề ẩn giấu trong quá trình kiểm tra đơn vị. Đây là lý do tại sao các bài kiểm tra tốt đã hoàn thành hoặc ít nhất là đại diện cho một đơn vị nhất định và nói chung điều này rất khó.)

Nếu bạn thực hiện các thử nghiệm của mình một cách xác định bằng cách làm cho trình tạo số ngẫu nhiên có thể cắm được, bạn luôn kiểm tra đối với cùng một tập dữ liệu không đại diện rất nhỏ và (thường) . Điều này làm lệch dữ liệu của bạn và có thể dẫn đến sai lệch trong chức năng của bạn.

Điểm thứ hai, tính không khả thi, phát sinh khi bạn không có quyền kiểm soát đối với biến ngẫu nhiên. Điều này thường không xảy ra với các trình tạo số ngẫu nhiên (trừ khi bạn cần một nguồn ngẫu nhiên thực sự của trực tuyến) nhưng nó có thể xảy ra khi các stochastic lẻn vào vấn đề của bạn bằng những cách khác. Chẳng hạn, khi kiểm tra mã đồng thời: điều kiện chủng tộc luôn luôn ngẫu nhiên, bạn không thể (dễ dàng) làm cho chúng có tính xác định.

Cách duy nhất để nâng cao sự tự tin trong những trường hợp đó là kiểm tra rất nhiều . Lót, rửa sạch, lặp lại. Điều này làm tăng sự tự tin, lên đến một mức nhất định (tại thời điểm đó, sự đánh đổi cho các lần chạy thử bổ sung trở nên không đáng kể).


2

Bạn thực sự có nhiều trách nhiệm ở đây. Kiểm thử đơn vị và đặc biệt là TDD là tuyệt vời để làm nổi bật loại điều này.

Trách nhiệm là:

1) Trình tạo số ngẫu nhiên. 2) Định dạng mật khẩu.

Trình định dạng mật khẩu sử dụng trình tạo số ngẫu nhiên. Tiêm trình tạo vào bộ định dạng của bạn thông qua hàm tạo của nó như một giao diện. Bây giờ bạn có thể kiểm tra đầy đủ trình tạo số ngẫu nhiên của mình (kiểm tra thống kê) và bạn có thể kiểm tra trình định dạng bằng cách tiêm một trình tạo số ngẫu nhiên giả.

Bạn không chỉ nhận được mã tốt hơn mà còn có các bài kiểm tra tốt hơn.


2

Như những người khác đã đề cập, bạn đơn vị kiểm tra mã này bằng cách loại bỏ tính ngẫu nhiên.

Bạn cũng có thể muốn có một bài kiểm tra cấp cao hơn để đặt trình tạo số ngẫu nhiên, chỉ kiểm tra hợp đồng (độ dài mật khẩu, ký tự được phép, ...) và, khi thất bại, bỏ đủ thông tin để cho phép bạn sao chép hệ thống trạng thái trong một trường hợp trong đó thử nghiệm ngẫu nhiên thất bại.

Không có vấn đề gì khi bản thân bài kiểm tra không lặp lại - miễn là bạn có thể tìm thấy lý do tại sao nó lại thất bại một lần này.


2

Nhiều khó khăn kiểm thử đơn vị trở nên tầm thường khi bạn cấu trúc lại mã của mình thành sever phụ thuộc. Một cơ sở dữ liệu, một hệ thống tệp, người dùng hoặc trong trường hợp của bạn, một nguồn ngẫu nhiên.

Một cách khác để xem xét là các bài kiểm tra đơn vị được cho là trả lời câu hỏi "mã này có làm những gì tôi dự định làm không?". Trong trường hợp của bạn, bạn không biết bạn dự định làm gì bởi vì nó không mang tính quyết định.

Với tâm trí này, hãy phân tách logic của bạn thành các phần nhỏ, dễ hiểu, dễ kiểm tra-cách ly. Cụ thể, bạn tạo một phương thức riêng biệt (hoặc lớp!) Lấy nguồn ngẫu nhiên làm đầu vào của nó và tạo mật khẩu làm đầu ra. Mã đó rõ ràng là xác định.

Trong bài kiểm tra đơn vị của bạn, bạn cung cấp cho nó cùng một đầu vào không hoàn toàn ngẫu nhiên mỗi lần. Đối với các luồng ngẫu nhiên rất nhỏ, chỉ cần mã cứng các giá trị trong thử nghiệm của bạn. Mặt khác, cung cấp một hạt giống liên tục cho RNG trong thử nghiệm của bạn.

Ở cấp độ thử nghiệm cao hơn (gọi là "chấp nhận" hoặc "tích hợp" hoặc bất cứ điều gì), bạn sẽ để mã chạy với một nguồn ngẫu nhiên thực sự.


Câu trả lời này đóng đinh nó cho tôi: Tôi thực sự có hai hàm trong một: trình tạo số ngẫu nhiên và hàm đã làm một cái gì đó với số ngẫu nhiên đó. Tôi chỉ đơn giản là tái cấu trúc, và bây giờ có thể dễ dàng kiểm tra phần không xác định của mã và cung cấp cho nó các tham số được tạo bởi phần ngẫu nhiên. Điều tuyệt vời là sau đó tôi có thể cung cấp cho nó (các bộ khác nhau) các tham số cố định trong kiểm tra đơn vị của mình (tôi đang sử dụng trình tạo số ngẫu nhiên từ thư viện chuẩn, vì vậy không phải kiểm tra đơn vị nào).

1

Hầu hết các câu trả lời ở trên chỉ ra rằng việc chế tạo bộ tạo số ngẫu nhiên là cách tốt nhất, tuy nhiên tôi chỉ đơn giản là sử dụng hàm mt_rand tích hợp. Cho phép chế nhạo có nghĩa là viết lại lớp để yêu cầu một bộ tạo số ngẫu nhiên được đưa vào khi xây dựng.

Hay là tôi nghĩ vậy!

Một trong những hậu quả của việc bổ sung các không gian tên là việc chế tạo các hàm PHP được xây dựng đã đi từ cực kỳ khó khăn đến đơn giản tầm thường. Nếu SUT nằm trong một không gian tên nhất định thì tất cả những gì bạn cần làm là xác định hàm mt_rand của riêng bạn trong kiểm tra đơn vị trong không gian tên đó và nó sẽ được sử dụng thay cho hàm PHP tích hợp trong thời gian thử nghiệm.

Dưới đây là bộ kiểm tra hoàn thiện:

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);
    }
}

Tôi nghĩ rằng tôi đã đề cập đến điều này, bởi vì ghi đè các hàm bên trong PHP là một cách sử dụng khác cho các không gian tên đơn giản đã không xảy ra với tôi. Cảm ơn tất cả mọi người vì sự giúp đỡ này.


0

Có một thử nghiệm bổ sung mà bạn nên đưa vào trong tình huống này và đó là một thử nghiệm để đảm bảo rằng các cuộc gọi lặp lại đến trình tạo mật khẩu thực sự tạo ra các mật khẩu khác nhau. Nếu bạn cần một trình tạo mật khẩu an toàn luồng, bạn cũng nên kiểm tra các cuộc gọi đồng thời bằng nhiều luồng.

Điều này về cơ bản đảm bảo rằng bạn đang sử dụng đúng chức năng ngẫu nhiên của mình và không phải gieo lại mỗi lần gọi.


Trên thực tế, lớp được thiết kế sao cho mật khẩu được tạo trong lệnh gọi đầu tiên tới getPassword () và sau đó chốt, vì vậy nó luôn trả về cùng một mật khẩu trong suốt vòng đời của đối tượng. Bộ kiểm tra của tôi đã kiểm tra rằng nhiều cuộc gọi đến getPassword () trên cùng một thể hiện mật khẩu luôn trả về cùng một chuỗi mật khẩu. Về an toàn luồng, điều đó không thực sự đáng lo ngại trong PHP :)
GordonM
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.