วิธีการทดสอบหน่วยที่มีเอาต์พุตไม่แน่นอน


37

ฉันมีคลาสที่ตั้งใจจะสร้างรหัสผ่านแบบสุ่มที่มีความยาวซึ่งก็สุ่ม แต่ จำกัด ให้อยู่ระหว่างระยะเวลาที่กำหนดและความยาวสูงสุด

ฉันกำลังสร้างการทดสอบหน่วยและวิ่งเข้าไปในอุปสรรค์เล็ก ๆ น้อย ๆ ที่น่าสนใจกับชั้นเรียนนี้ แนวคิดทั้งหมดที่อยู่เบื้องหลังการทดสอบหน่วยคือควรทำซ้ำได้ หากคุณใช้การทดสอบร้อยครั้งก็ควรให้ผลลัพธ์เดียวกันร้อยครั้ง หากคุณขึ้นอยู่กับทรัพยากรบางอย่างที่อาจมีหรือไม่มีอยู่หรืออาจจะอยู่หรืออาจไม่อยู่ในสถานะเริ่มต้นที่คุณคาดหวังจากนั้นคุณตั้งใจจะจำลองทรัพยากรที่มีปัญหาเพื่อให้แน่ใจว่าการทดสอบของคุณทำซ้ำได้เสมอ

แต่สิ่งที่เกี่ยวกับในกรณีที่ SUT ควรจะสร้างผลผลิตไม่แน่นอน?

หากฉันแก้ไขความยาวขั้นต่ำและความยาวสูงสุดเป็นค่าเดียวกันฉันสามารถตรวจสอบได้อย่างง่ายดายว่ารหัสผ่านที่สร้างขึ้นนั้นมีความยาวตามที่คาดหวัง แต่ถ้าฉันระบุช่วงของความยาวที่ยอมรับได้ (พูด 15 - 20 ตัวอักษร) ตอนนี้คุณมีปัญหาที่คุณสามารถรันการทดสอบได้ร้อยครั้งและได้รับ 100 ครั้ง แต่ในการวิ่งครั้งที่ 101 คุณอาจได้รับสตริงอักขระ 9 คืน

ในกรณีของคลาสรหัสผ่านซึ่งค่อนข้างง่ายที่แกนกลางของมันก็ไม่ควรพิสูจน์ปัญหาใหญ่ แต่มันทำให้ฉันคิดถึงกรณีทั่วไป กลยุทธ์ที่มักได้รับการยอมรับว่าเป็นกลยุทธ์ที่ดีที่สุดเมื่อจัดการกับ SUT ที่สร้างผลลัพธ์ไม่แน่นอนโดยการออกแบบคืออะไร


9
ทำไมการโหวตอย่างใกล้ชิด? ฉันคิดว่ามันเป็นคำถามที่ถูกต้องสมบูรณ์
มาร์คเบเกอร์

ขอบคุณสำหรับความคิดเห็น ไม่ได้สังเกตด้วยซ้ำ แต่ตอนนี้ฉันสงสัยในสิ่งเดียวกัน มีเพียงสิ่งเดียวที่ฉันคิดได้ว่าเป็นเรื่องเกี่ยวกับกรณีทั่วไปมากกว่าเฉพาะ แต่ฉันสามารถโพสต์แหล่งที่มาสำหรับคลาสรหัสผ่านที่กล่าวถึงข้างต้นและถามว่า "ฉันจะทดสอบคลาสนั้นได้อย่างไร" แทนที่จะเป็น "ฉันจะทดสอบชั้นที่ไม่แน่นอนได้อย่างไร"
GordonM

1
@ MarkBaker เนื่องจากคำถามที่ไม่ได้ทดสอบส่วนใหญ่อยู่ใน programmers.se เป็นการลงคะแนนสำหรับการย้ายถิ่นไม่ใช่เพื่อปิดคำถาม
Ikke

คำตอบ:


20

เอาท์พุท "Non-deterministic" ควรมีวิธีการกำหนดขึ้นเพื่อวัตถุประสงค์ในการทดสอบหน่วย วิธีหนึ่งในการจัดการแบบแผนคือการอนุญาตให้เปลี่ยนเครื่องยนต์แบบสุ่ม นี่คือตัวอย่าง (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 รักษาค่าคงที่ซึ่งเป็นกลุ่มของรหัสผ่านที่ควรสร้างขึ้น โดย subclassing มันและเอาชนะค่าคงที่ด้วยตัวอักษรตัวเดียวฉันจัดการเพื่อกำจัดหนึ่งในพื้นที่ที่ไม่ได้กำหนดสำหรับวัตถุประสงค์ของการทดสอบ ขอบคุณมาก
GordonM

14

ทดสอบกับ "สัญญา" เมื่อวิธีถูกกำหนดเป็น "สร้างรหัสผ่านที่มีความยาว 15 ถึง 20 อักขระด้วย az" ให้ทดสอบด้วยวิธีนี้

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

regex ที่คุณให้ไว้มีประโยชน์ดังนั้นฉันจึงรวมเวอร์ชันที่ปรับแต่งแล้วในการทดสอบของฉัน ขอบคุณ
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

ในกรณีของฟิสิกส์ของอนุภาคมอนติคาร์โลฉันได้เขียน "การทดสอบหน่วย" {*} ที่เรียกใช้รูทีนที่ไม่ได้กำหนดไว้ล่วงหน้าด้วยเมล็ดสุ่มที่กำหนดไว้ล่วงหน้าจากนั้นเรียกใช้จำนวนครั้งทางสถิติและตรวจสอบการละเมิดข้อ จำกัด เหนือพลังงานอินพุตจะต้องไม่สามารถเข้าถึงได้บัตรผ่านทั้งหมดจะต้องเลือกระดับและอื่น ๆ ) และการถดถอยกับผลลัพธ์ที่บันทึกไว้ก่อนหน้า ..


{*} การทดสอบดังกล่าวละเมิดหลักการ "ทำให้การทดสอบเร็ว" สำหรับการทดสอบหน่วยดังนั้นคุณอาจรู้สึกถึงลักษณะที่ดีขึ้นในลักษณะอื่น: การทดสอบการยอมรับหรือการทดสอบการถดถอยเป็นต้น ยังฉันใช้กรอบการทดสอบหน่วยของฉัน


3

ฉันต้องไม่เห็นด้วยกับคำตอบที่ได้รับการยอมรับด้วยเหตุผลสองประการ:

  1. overfitting
  2. การไม่สามารถทำได้

(โปรดสังเกตว่าอาจเป็นคำตอบที่ดีในหลาย ๆ สถานการณ์ แต่ไม่ใช่ในทุกกรณีและอาจไม่ได้ในทุกกรณี)

ดังนั้นฉันหมายความว่าอย่างไร โดยการ overfittingฉันหมายถึงปัญหาทั่วไปของการทดสอบทางสถิติ: การ overfitting เกิดขึ้นเมื่อคุณทดสอบอัลกอริทึมแบบสุ่มกับชุดข้อมูลที่ จำกัด มากเกินไป หากคุณย้อนกลับไปและปรับแต่งอัลกอริทึมของคุณคุณจะทำให้ข้อมูลการฝึกอบรมเป็นไปโดยปริยาย (คุณพอดีอัลกอริทึมของคุณกับข้อมูลทดสอบ) แต่ข้อมูลอื่น ๆ อาจไม่ได้เลย (เพราะคุณไม่เคยทดสอบกับมัน) .

(โดยบังเอิญนี่เป็นปัญหาที่มักเกิดกับการทดสอบหน่วยนี่คือเหตุผลที่การทดสอบที่ดีเสร็จสมบูรณ์หรืออย่างน้อยก็เป็นตัวแทนของหน่วยการเรียนรู้ที่กำหนดและนี่เป็นเรื่องยากโดยทั่วไป)

ถ้าคุณทำทดสอบของคุณกำหนดโดยการสุ่มเครื่องกำเนิดไฟฟ้าจำนวน pluggable คุณทดสอบกับขนาดเล็กมากและ (ปกติ) เดียวกันไม่ใช่ตัวแทนของชุดข้อมูล สิ่งนี้จะบิดเบือนข้อมูลของคุณและอาจทำให้เกิดอคติในการทำงานของคุณ

จุดที่สองความไม่สามารถทำได้เกิดขึ้นเมื่อคุณไม่สามารถควบคุมตัวแปรสุ่มได้ สิ่งนี้มักจะไม่เกิดขึ้นกับตัวสร้างตัวเลขสุ่ม (เว้นแต่ว่าคุณต้องการแหล่งที่มาของการสุ่ม "จริง") แต่มันอาจเกิดขึ้นเมื่อ stochastics แอบเข้าไปในปัญหาของคุณด้วยวิธีอื่น ตัวอย่างเช่นเมื่อทดสอบโค้ดที่เกิดขึ้นพร้อมกัน: เงื่อนไขการแข่งขันจะสุ่มเสมอคุณจะไม่สามารถกำหนดได้อย่างง่ายดาย

วิธีที่จะเพิ่มความเชื่อมั่นในกรณีเดียวคือการทดสอบเป็นจำนวนมาก นวดแล้วล้างซ้ำ สิ่งนี้ทำให้ความมั่นใจเพิ่มขึ้นถึงระดับหนึ่ง (ณ จุดที่การแลกเปลี่ยนสำหรับการทดสอบเพิ่มเติมนั้นไม่มีความสำคัญ)


2

คุณมีความรับผิดชอบหลายอย่างจริงๆ การทดสอบหน่วยและโดยเฉพาะอย่างยิ่ง TDD นั้นยอดเยี่ยมสำหรับการเน้นสิ่งนี้

ความรับผิดชอบคือ:

1) เครื่องกำเนิดจำนวนสุ่ม 2) ฟอร์แมตรหัสผ่าน

ตัวจัดรูปแบบรหัสผ่านใช้ตัวสร้างตัวเลขสุ่ม ฉีดเครื่องกำเนิดไฟฟ้าลงในฟอร์แมตเตอร์ของคุณผ่าน Constructor ของมันเป็นอินเตอร์เฟส ตอนนี้คุณสามารถทดสอบตัวสร้างตัวเลขสุ่มของคุณได้อย่างเต็มที่ (การทดสอบทางสถิติ) และคุณสามารถทดสอบตัวจัดรูปแบบโดยการฉีดตัวสร้างตัวเลขสุ่มจำลอง

ไม่เพียง แต่คุณจะได้รับโค้ดที่ดีขึ้นเท่านั้น


2

ดังที่คนอื่น ๆ พูดถึงแล้วคุณได้ทำการทดสอบรหัสนี้โดยการลบการสุ่ม

คุณอาจต้องการทดสอบในระดับที่สูงกว่าซึ่งปล่อยให้ตัวสร้างตัวเลขแบบสุ่มเข้ามาทดสอบเฉพาะสัญญา (ความยาวรหัสผ่านอักขระที่อนุญาต ... ) และหากล้มเหลวจะมีการทิ้งข้อมูลให้เพียงพอเพื่อให้คุณสามารถสร้างระบบขึ้นใหม่ได้ สถานะในอินสแตนซ์เดียวที่การทดสอบแบบสุ่มล้มเหลว

ไม่สำคัญว่าการทดสอบนั้นไม่สามารถทำซ้ำได้ - ตราบใดที่คุณสามารถหาสาเหตุที่ทำให้การทดสอบล้มเหลวในครั้งเดียว


2

ปัญหาการทดสอบหน่วยจำนวนมากกลายเป็นเรื่องไม่สำคัญเมื่อคุณปรับโครงสร้างรหัสของคุณใหม่เพื่อตัดการอ้างอิง ฐานข้อมูลระบบไฟล์ผู้ใช้หรือในกรณีของคุณแหล่งที่มาของการสุ่ม

อีกวิธีในการดูคือการทดสอบหน่วยควรตอบคำถาม "รหัสนี้ทำในสิ่งที่ฉันตั้งใจจะทำหรือไม่" ในกรณีของคุณคุณไม่รู้ว่าคุณตั้งใจให้โค้ดทำอะไรเพราะมันไม่ได้กำหนดค่าไว้

ด้วยความคิดนี้ให้แยกตรรกะของคุณเป็นชิ้นส่วนขนาดเล็กที่เข้าใจง่ายทดสอบได้อย่างง่ายดายและแยกได้ง่าย คุณสร้างวิธีที่แตกต่าง (หรือคลาส!) ที่นำแหล่งที่มาของการสุ่มเป็นอินพุตและสร้างรหัสผ่านเป็นเอาต์พุต รหัสนั้นชัดเจนอย่างชัดเจน

ในการทดสอบหน่วยของคุณคุณป้อนข้อมูลป้อนเข้าแบบสุ่มค่อนข้างไม่เหมือนกันทุกครั้ง สำหรับสตรีมแบบสุ่มขนาดเล็กมากเพียงแค่เขียนโค้ดค่าในการทดสอบของคุณ มิฉะนั้นให้เมล็ดพันธุ์คงที่แก่ RNG ในการทดสอบของคุณ

ในระดับการทดสอบที่สูงขึ้น (เรียกว่า "การยอมรับ" หรือ "การรวม" หรืออะไรก็ตาม) คุณจะปล่อยให้โค้ดทำงานกับแหล่งที่มาแบบสุ่มจริง


คำตอบนี้ถูกจับสำหรับฉัน: ฉันมีสองฟังก์ชันในหนึ่งเดียว: ตัวสร้างตัวเลขสุ่มและฟังก์ชันที่ทำบางสิ่งกับตัวเลขสุ่มนั้น ฉันได้รับการปรับโครงสร้างใหม่และตอนนี้สามารถทดสอบส่วน nondeterministic ของรหัสได้อย่างง่ายดายและป้อนพารามิเตอร์ที่สร้างขึ้นโดยส่วนที่สุ่ม สิ่งที่ดีคือฉันสามารถป้อนพารามิเตอร์คงที่ (ชุดต่าง ๆ ) ในการทดสอบหน่วยของฉัน (ฉันใช้ตัวสร้างตัวเลขสุ่มจากไลบรารีมาตรฐานดังนั้นไม่ต้องทดสอบหน่วยนั้น)
เซลล์ประสาท

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 ที่สำคัญคือการใช้ namespaces ที่ไม่ได้เกิดขึ้นกับฉัน ขอบคุณทุกคนสำหรับความช่วยเหลือในเรื่องนี้


0

มีการทดสอบเพิ่มเติมที่คุณควรรวมไว้ในสถานการณ์นี้และนี่คือการทดสอบเพื่อให้แน่ใจว่าการโทรซ้ำไปยังเครื่องกำเนิดรหัสผ่านซ้ำ ๆ จะสร้างรหัสผ่านที่แตกต่างกัน หากคุณต้องการเครื่องกำเนิดรหัสผ่านที่ปลอดภัยสำหรับเธรดคุณควรทดสอบการโทรพร้อมกันโดยใช้หลายเธรด

สิ่งนี้ทำให้แน่ใจได้ว่าคุณกำลังใช้ฟังก์ชั่นการสุ่มของคุณอย่างถูกต้องและไม่ได้ทำการรีสตาร์ตในทุกการโทร


ที่จริงแล้วคลาสได้รับการออกแบบเพื่อให้รหัสผ่านถูกสร้างขึ้นในการเรียกครั้งแรกไปที่ getPassword () จากนั้นแลตช์ดังนั้นมันจะส่งคืนรหัสผ่านเดิมตลอดอายุการใช้งานของวัตถุ ชุดทดสอบของฉันตรวจสอบแล้วว่าการโทรหลายครั้งไปยัง getPassword () บนอินสแตนซ์รหัสผ่านเดียวกันจะส่งคืนสตริงรหัสผ่านเดียวกันเสมอ ในฐานะที่เป็นด้ายความปลอดภัยที่ไม่ได้จริงๆกังวลใน PHP :)
GordonM
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.