การเขียนโค้ดขั้นต่ำเพื่อผ่านการทดสอบหน่วย - โดยไม่ต้องโกง!


36

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

ตัวอย่างเช่น:
ฉันจะต้องคำนวณแฟคทอเรียลของตัวเลข ฉันเริ่มต้นด้วยการทดสอบหน่วย (ใช้ MSTest) สิ่งที่ชอบ:

[TestClass]
public class CalculateFactorialTests
{
    [TestMethod]
    public void CalculateFactorial_5_input_returns_120()
    {
        // Arrange
        var myMath = new MyMath();
        // Act
        long output = myMath.CalculateFactorial(5);
        // Assert
        Assert.AreEqual(120, output);
    }
}

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

สิ่งคือฉันอยากจะเขียนต่อไปนี้:

public class MyMath
{
    public long CalculateFactorial(long input)
    {
        return 120;
    }
}

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

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


4
มันเป็นเรื่องของมนุษย์: คุณต้องต่อต้านความอยากที่จะโกง ไม่มีอะไรมากไป คุณสามารถเพิ่มการทดสอบมากขึ้นและเขียนรหัสการทดสอบมากกว่ารหัสที่จะทดสอบ แต่ถ้าคุณไม่มีความหรูหรานั้นกว่าที่คุณจะต้องต้านทาน มีหลายที่ในการเขียนโปรแกรมที่เราต้องต่อต้านความอยากที่จะแฮ็คหรือโกงเพราะเรารู้ว่าในขณะที่มันอาจใช้งานได้ในวันนี้มันจะไม่ทำงานในภายหลัง
Dan Rosenstark

7
แน่นอนใน TDD การทำในทางตรงกันข้ามคือการโกง - เช่นการคืน 120 เป็นวิธีที่เหมาะสม ฉันคิดว่ามันค่อนข้างยากที่จะทำให้ตัวเองทำอย่างนั้นและไม่แข่งเร็วและเริ่มเขียนการคำนวณแบบแฟคทอเรียล
พอล Butcher

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

3
หากปรากฎว่ารหัสลูกค้ารหัสผ่านเพียงใน 5 ผลตอบแทน 120 ไม่ได้เป็นเพียงการโกง แต่เป็นทางออกที่ถูกต้องตามกฎหมาย
Kramii Reinstate Monica

ฉันเห็นด้วยกับ @ PaulButcher - อันที่จริงตัวอย่างการทดสอบหน่วยจำนวนมากในข้อความและบทความจะใช้วิธีนี้
HorusKol

คำตอบ:


45

มันถูกต้องสมบูรณ์แบบ สีแดงสีเขียว Refactor

การทดสอบครั้งแรกผ่านไป

เพิ่มการทดสอบที่สองด้วยอินพุตใหม่

ตอนนี้ให้เป็นสีเขียวอย่างรวดเร็วคุณสามารถเพิ่ม if-else ซึ่งทำงานได้ดี มันผ่านไปแล้ว แต่ยังไม่เสร็จ

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

ฉันไม่ได้พูดว่าอย่าเขียนมันอย่างถูกต้องในครั้งแรก ฉันแค่บอกว่ามันไม่ใช่การโกงถ้าคุณไม่ทำ


12
สิ่งนี้ทำให้เกิดคำถามขึ้นทำไมไม่เพียงเขียนฟังก์ชันอย่างถูกต้องตั้งแต่แรก
Robert Harvey

8
@ Robert เลขแฟกทอเรียลนั้นง่ายนิดเดียว ข้อได้เปรียบที่แท้จริงของ TDD คือเมื่อคุณเขียนไลบรารี่ที่ไม่สำคัญและการเขียนการทดสอบครั้งแรกบังคับให้คุณออกแบบ API ก่อนที่จะนำไปใช้ซึ่งในประสบการณ์ของฉันจะทำให้โค้ดดีขึ้น

1
@ Robert มันเป็นคุณที่มีความกังวลเกี่ยวกับการแก้ปัญหาแทนที่จะผ่านการทดสอบ ฉันกำลังบอกคุณว่าสำหรับปัญหาที่ไม่สำคัญมันก็จะดีกว่าที่จะเลื่อนการออกแบบอย่างหนักจนกว่าคุณจะมีการทดสอบ

1
@ Thorbjørn Ravn Andersen ไม่ฉันไม่ได้บอกว่าคุณมีเพียงหนึ่งคืน มีเหตุผลที่ถูกต้องสำหรับหลาย ๆ (เช่นคำสั่งป้องกัน) ปัญหาคือข้อความสั่งคืนทั้งสองเป็น "เท่ากัน" พวกเขาทำสิ่งเดียวกัน พวกเขาเพิ่งจะมีค่าแตกต่างกัน TDD ไม่ได้เกี่ยวกับความแข็งแกร่งและเป็นไปตามขนาดเฉพาะของอัตราส่วนทดสอบ / รหัส มันเกี่ยวกับการสร้างระดับความสะดวกสบายภายในฐานรหัสของคุณ หากคุณสามารถเขียนการทดสอบที่ล้มเหลวได้ฟังก์ชันที่จะใช้สำหรับการทดสอบในอนาคตของฟังก์ชันนั้นยอดเยี่ยม ทำแล้วเขียนการทดสอบเคสของคุณเพื่อให้แน่ใจว่าฟังก์ชั่นของคุณยังคงทำงานได้
CaffGeek

3
จุดที่ไม่ได้เขียนการใช้งานเต็มรูปแบบ (แม้ว่าจะง่าย) ในครั้งเดียวก็คือคุณไม่มีการรับประกันเลยว่าการทดสอบของคุณจะไม่สามารถทำได้ จุดที่เห็นการทดสอบล้มเหลวก่อนที่จะผ่านก็คือว่าคุณมีหลักฐานที่แท้จริงว่าการเปลี่ยนแปลงรหัสของคุณคือสิ่งที่คุณพอใจในการยืนยัน นี่เป็นเหตุผลเดียวว่าทำไม TDD ถึงยอดเยี่ยมสำหรับการสร้างชุดการทดสอบการถดถอยและเช็ดพื้นอย่างสมบูรณ์ด้วยการ "ทดสอบหลัง" - พิสูจน์ในแง่นั้น
sara

25

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

TDD ไม่ใช่กระสุนวิเศษสำหรับการออกแบบ คุณยังต้องรู้วิธีแก้ปัญหาด้วยการใช้รหัสและคุณยังต้องรู้วิธีการทำในระดับที่สูงกว่ารหัสสองสามบรรทัดเพื่อทำการทดสอบ

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

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

ลุง Bob Martin กล่าวสิ่งนี้:

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

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

คุณไม่เพียงแค่ทำการทดสอบกลุ่มทั้งหมดเข้าด้วยกันและทำให้พวกเขาผ่านไปหลายสิบปีหลังจากทศวรรษหลังจากทศวรรษและคิดว่าคุณกำลังจะอยู่รอด เราไม่ต้องการที่จะพัฒนาตัวเองไปสู่นรก นักพัฒนาที่ขับเคลื่อนการทดสอบที่ดีจึงมีความตระหนักในการตัดสินใจทางสถาปัตยกรรมโดยคำนึงถึงภาพรวม


ไม่ใช่คำตอบสำหรับคำถามจริงๆ แต่เป็น 1+
ไม่มีใครใน

2
@rmx: เอ่อคำถามคือ: คุณจะได้รับความสมดุลระหว่าง "การเขียนรหัสขั้นต่ำที่จะผ่านการทดสอบ" ในขณะที่ยังคงทำงานได้และในจิตวิญญาณของสิ่งที่คุณพยายามที่จะบรรลุจริง? พวกเรากำลังอ่านคำถามเดียวกันหรือไม่?
Robert Harvey

ทางออกที่ดีที่สุดคืออัลกอริธึมและไม่เกี่ยวข้องกับสถาปัตยกรรม การทำ TDD จะไม่ทำให้คุณคิดค้นอัลกอริทึม ในบางจุดคุณต้องทำตามขั้นตอนในแง่ของอัลกอริทึม / โซลูชัน
Joppe

ฉันเห็นด้วยกับ @rmx สิ่งนี้ไม่ได้ตอบคำถามเฉพาะของฉันอย่างแท้จริง แต่มันก่อให้เกิดอาหารสำหรับความคิดว่า TDD โดยทั่วไปเข้ากับภาพรวมของกระบวนการพัฒนาซอฟต์แวร์โดยรวมอย่างไร ดังนั้นด้วยเหตุผลนั้น +1
CraigTP

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

16

เป็นคำถามที่ดีมาก ... และฉันต้องไม่เห็นด้วยกับเกือบทุกคนยกเว้น @Robert

การเขียน

return 120;

สำหรับฟังก์ชั่นปัจจัยที่ทำให้การทดสอบหนึ่งผ่านเป็นเสียเวลา มันไม่ใช่ "การโกง" และไม่เป็นไปตามสีแดง - เขียว - รีแฟคเตอร์อย่างแท้จริง มันเป็นความผิด

นี่คือเหตุผล:

  • คำนวณแฟคทอเรียลเป็นคุณลักษณะไม่ใช่ "คืนค่าคงที่" "return 120" ไม่ใช่การคำนวณ
  • อาร์กิวเมนต์ 'refactor' ถูกเข้าใจผิด หากคุณมีสองกรณีทดสอบสำหรับ 5 และ 6 รหัสนี้ยังคงผิดเพราะคุณไม่ได้คำนวณแฟคทอเรียลเลย :

    if (input == 5) { return 120; } //input=5 case
    else { return 720; }   //input=6 case
    
  • ถ้าเราทำตามอาร์กิวเมนต์ 'refactor' ตามตัวอักษรเมื่อเรามี 5 กรณีทดสอบเราจะเรียกใช้ YAGNI และใช้ฟังก์ชันโดยใช้ตารางการค้นหา:

    if (factorialDictionary.Contains(input)) {
        return factorialDictionary[input]; 
    }
    throw new Exception("Input failure");
    

ไม่มีของเหล่านี้เป็นจริงการคำนวณอะไรที่คุณอยู่ และนั่นไม่ใช่งาน!


1
@rmx: ไม่ไม่พลาด "refactor เพื่อลบการทำซ้ำ" สามารถพอใจกับตารางการค้นหา BTW เป็นหลักการที่หน่วยทดสอบความต้องการการเข้ารหัสไม่เฉพาะ BDD มันเป็นหลักการทั่วไปของ Agile / XP หากความต้องการคือ "ตอบคำถาม 'อะไรคือปัจจัยของ 5'" แล้ว 'คืนค่า 120;' จะเป็น legit ;-)
Steven A. Lowe

2
@Chad ทั้งหมดนี้เป็นงานที่ไม่จำเป็น - เพียงแค่เขียนฟังก์ชั่นในครั้งแรก ;-)
Steven A. Lowe

2
@Steven A.Lowe โดยตรรกะนั้นทำไมต้องทำการทดสอบใด ๆ ! "เพียงแค่เขียนใบสมัครในครั้งแรก!" จุดของ TDD คือการเปลี่ยนแปลงเล็ก ๆ น้อย ๆ ที่ปลอดภัยเพิ่มขึ้น
CaffGeek

1
@Chad: หมวกฟาง
Steven A. Lowe

2
จุดที่ไม่ได้เขียนการใช้งานเต็มรูปแบบ (แม้ว่าจะง่าย) ในครั้งเดียวก็คือคุณไม่มีการรับประกันเลยว่าการทดสอบของคุณจะไม่สามารถทำได้ จุดที่เห็นการทดสอบล้มเหลวก่อนที่จะผ่านก็คือว่าคุณมีหลักฐานที่แท้จริงว่าการเปลี่ยนแปลงรหัสของคุณคือสิ่งที่คุณพอใจในการยืนยัน นี่เป็นเหตุผลเดียวว่าทำไม TDD ถึงยอดเยี่ยมสำหรับการสร้างชุดการทดสอบการถดถอยและเช็ดพื้นอย่างสมบูรณ์ด้วยการ "ทดสอบหลัง" - พิสูจน์ในแง่นั้น คุณไม่เคยเขียนแบบทดสอบที่ไม่สามารถล้มเหลวโดยไม่ตั้งใจ ยังดูที่ bobs ลุงปัจจัยสำคัญกะตะ
sara

10

เมื่อคุณเขียนการทดสอบหน่วยเดียวเท่านั้นการใช้งานแบบบรรทัดเดียว ( return 120;) นั้นถูกต้องตามกฎหมาย การเขียนลูปคำนวณค่า 120 - นั่นจะเป็นการโกง!

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

กฎง่ายๆที่อาจมีประโยชน์ในที่นี้คือ: ศูนย์หนึ่งอันมากๆ ศูนย์และหนึ่งเป็นกรณีขอบที่สำคัญสำหรับแฟคทอเรียล พวกเขาสามารถนำมาใช้กับหนึ่งสมุทร กรณีทดสอบ "หลายคน" (เช่น 5!) จะบังคับให้คุณเขียนลูป กรณีทดสอบ "ล็อต" (1,000 !?) อาจบังคับให้คุณใช้อัลกอริธึมทางเลือกเพื่อจัดการกับตัวเลขที่มีขนาดใหญ่มาก


2
กรณี "-1" น่าสนใจ เพราะมันไม่ได้กำหนดไว้อย่างดีดังนั้นทั้งคนที่เขียนแบบทดสอบและคนที่เขียนรหัสต้องเห็นด้วยก่อนว่าจะเกิดอะไรขึ้น
gnasher729

2
+1 สำหรับการชี้ให้เห็นว่าfactorial(5)เป็นการทดสอบครั้งแรกที่ไม่ดี เราเริ่มต้นจากกรณีที่ง่ายที่สุดเท่าที่จะเป็นไปได้และในแต่ละการวนซ้ำเราทำการทดสอบอีกครั้งโดยเฉพาะเจาะจงยิ่งขึ้น นี่คือสิ่งที่ลุงบ๊อบเรียกว่าการเปลี่ยนสถานที่ที่มีความสำคัญ ( blog.8thlight.com/uncle-bob/2013/05/27/… )
sara

5

ตราบใดที่คุณมีการทดสอบเพียงครั้งเดียวรหัสที่น้อยที่สุดที่จำเป็นในการผ่านการทดสอบนั้นจะเป็นจริงreturn 120;และคุณสามารถเก็บไว้ได้อย่างง่ายดายตราบใดที่คุณไม่มีการทดสอบอีกต่อไป

สิ่งนี้ช่วยให้คุณเลื่อนการออกแบบเพิ่มเติมจนกว่าคุณจะเขียนการทดสอบที่ออกกำลังกายค่าตอบแทนอื่น ๆ ของวิธีการนี้

โปรดจำไว้ว่าการทดสอบนั้นเป็นเวอร์ชั่นที่รันได้ของสเปคของคุณและถ้าสเปคทั้งหมดที่กล่าวคือ f (6) = 120 ดังนั้นมันจึงเหมาะกับบิล


อย่างจริงจัง? ด้วยตรรกะนี้คุณจะต้องเขียนรหัสใหม่ทุกครั้งที่มีคนป้อนข้อมูลใหม่
Robert Harvey

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

1
@ Thorbjørn Ravn Andersen ซึ่งเป็นส่วนที่สำคัญที่สุดของ Red-Green-Refactor คือการสร้างใหม่
CaffGeek

+1: นี่เป็นแนวคิดทั่วไปจากความรู้ของฉันเช่นกัน แต่สิ่งที่ต้องพูดเกี่ยวกับการปฏิบัติตามสัญญาโดยนัย (เช่นชื่อเมธอดแฟคทอเรียล ) หากคุณเคย spec (เช่นการทดสอบ) f (6) = 120 คุณจะต้อง 'return 120' เท่านั้น เมื่อคุณเริ่มเพิ่มการทดสอบเพื่อให้แน่ใจว่า f (x) == x * x-1 ... * xx-1: upperBound> = x> = 0 จากนั้นคุณจะได้รับฟังก์ชันที่ตรงกับสมการแฟกทอเรียล
Steven Evers

1
@SnOrfus สถานที่สำหรับ "สัญญาโดยนัย" ที่จะอยู่ในกรณีทดสอบ หากสัญญาของคุณมีไว้สำหรับแฟ็กทอรีคุณจะทดสอบว่าแฟ็กทอเรียลที่รู้จักนั้นเป็นอย่างไรและหากไม่ใช่แฟ็กทอเรียลที่รู้จัก มากมายของพวกเขา ใช้เวลาไม่นานในการแปลงรายการของแฟคทอเรียลสิบอันดับแรกเป็นการทดสอบแบบวนรอบทุกหมายเลขจนถึงแฟกทอเรียลที่สิบ

4

หากคุณสามารถ "โกง" ในลักษณะดังกล่าวแสดงว่าการทดสอบหน่วยของคุณมีข้อบกพร่อง

แทนที่จะทดสอบวิธีแฟกทอเรียลด้วยค่าเดียวลองทดสอบว่าเป็นช่วงของค่า การทดสอบที่ขับเคลื่อนด้วยข้อมูลสามารถช่วยได้ที่นี่

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

ดังนั้นถามตัวเอง - ถ้ามีใครบางคนเปลี่ยนการใช้งานไปเป็นสิ่งที่ไม่ถูกต้องการทดสอบของคุณจะยังคงผ่านหรือพวกเขาจะพูดว่า "รอสักครู่!"?

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


คุณสามารถเพิ่มชุดcaseคำสั่งที่ไม่มีที่สิ้นสุดลงใน a switchและคุณไม่สามารถเขียนการทดสอบสำหรับอินพุทและเอาท์พุทที่เป็นไปได้ทั้งหมดสำหรับตัวอย่างของ OP
Robert Harvey

คุณสามารถทดสอบทางเทคนิคค่าจากไปInt64.MinValue Int64.MaxValueใช้เวลานานในการรัน แต่จะกำหนดข้อกำหนดอย่างชัดเจนโดยไม่มีช่องว่างสำหรับข้อผิดพลาด ด้วยเทคโนโลยีในปัจจุบันสิ่งนี้ไม่สามารถทำได้ (ฉันสงสัยว่ามันอาจจะกลายเป็นเรื่องธรรมดาในอนาคต) และฉันเห็นด้วยคุณสามารถโกง แต่ฉันคิดว่าคำถาม OPs ไม่ใช่คำถามที่ปฏิบัติได้จริง (ไม่มีใครจะโกงในลักษณะนี้ ในทางปฏิบัติ) แต่เป็นทฤษฎี
ไม่มีใคร

@rmx: หากคุณสามารถทำได้การทดสอบจะเป็นอัลกอริทึมและคุณไม่จำเป็นต้องเขียนอัลกอริทึมอีกต่อไป
Robert Harvey

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

นอกจากนี้เราไม่ได้เป็นมนุษย์เหมือนที่จะทำผิดพลาดในการทดสอบหน่วยตามที่เราอยู่ในรหัสการติดตั้งหรือไม่? เหตุใดจึงต้องทำการทดสอบหน่วย
ไม่มีใคร

3

เพียงแค่เขียนแบบทดสอบเพิ่มเติม ในที่สุดมันจะสั้นกว่าที่จะเขียน

public long CalculateFactorial(long input)
{
    return input <= 1 ? 1 : CalculateFactorial(input-1)*input;
}

กว่า

public long CalculateFactorial(long input)
{
    switch (input) {
       case 0: return 1;
       case 1: return 1;
       case 2: return 2;
       case 3: return 6;
       case 4: return 24;
       case 5: return 120;
    }
}

:-)


3
ไม่เพียงเขียนอัลกอริทึมอย่างถูกต้องตั้งแต่แรก?
Robert Harvey

3
@ Robert มันเป็นขั้นตอนวิธีการที่ถูกต้องสำหรับการคำนวณ factorial ของตัวเลขตั้งแต่ 0 ถึง 5 นอกจากนี้สิ่งที่ไม่ "ถูกต้อง" หมายความว่าอย่างไร นี่เป็นตัวอย่างที่ง่ายมาก แต่เมื่อมันมีความซับซ้อนมากขึ้นจะมีการไล่ระดับของความหมายที่ถูกต้อง โปรแกรมที่ต้องการรูท "ถูกต้อง" เพียงพอหรือไม่ ใช้ XML "ถูกต้อง" แทนที่จะใช้ CSV หรือไม่ คุณไม่สามารถตอบคำถามนี้ อัลกอริธึมใด ๆ ที่ถูกต้องตราบใดที่มันเป็นไปตามข้อกำหนดทางธุรกิจบางอย่างซึ่งจัดทำขึ้นเป็นการทดสอบใน TDD
P Shved

3
ควรสังเกตว่าเนื่องจากประเภทเอาต์พุตยาวมีค่าอินพุตเพียงเล็กน้อย (20 หรือดังนั้น) ที่ฟังก์ชันสามารถจัดการได้อย่างถูกต้องดังนั้นคำสั่งสวิตช์ขนาดใหญ่จึงไม่จำเป็นต้องเป็นการใช้งานที่เลวร้ายที่สุด - ถ้าความเร็วมากกว่า สำคัญกว่าขนาดรหัสคำสั่ง switch อาจเป็นหนทางไปขึ้นอยู่กับลำดับความสำคัญของคุณ
281377

3

การเขียนการทดสอบ "cheat" นั้นใช้ได้สำหรับค่าที่พอเพียงของ "OK" แต่การเรียกคืน - หน่วยทดสอบเสร็จสมบูรณ์ก็ต่อเมื่อการทดสอบทั้งหมดผ่านและไม่มีการทดสอบใหม่สามารถเขียนได้ที่จะล้มเหลว หากคุณต้องการมีวิธี CalculateFactorial ที่มีประโยคif (หรือดีกว่านั้นคือคำสั่งswitch / caseขนาดใหญ่:-) คุณสามารถทำได้และเนื่องจากคุณต้องจัดการกับหมายเลขที่มีความแม่นยำคงที่ซึ่งจำเป็นต้องใช้รหัส การใช้งานนี้มีขอบเขต จำกัด (แม้ว่าอาจจะค่อนข้างใหญ่และน่าเกลียดและอาจถูก จำกัด โดยข้อ จำกัด ของคอมไพเลอร์หรือระบบในขนาดสูงสุดของโค้ดของโพรซีเดอร์) ณ จุดนี้ถ้าคุณจริงๆยืนยันว่าการพัฒนาทั้งหมดจะต้องขับเคลื่อนด้วยการทดสอบหน่วยคุณสามารถเขียนการทดสอบที่ต้องใช้รหัสในการคำนวณผลลัพธ์ในจำนวนเวลาที่สั้นกว่าที่สามารถทำได้โดยทำตามกิ่งไม้ทั้งหมดของคำสั่งif

โดยพื้นฐานแล้ว TDD สามารถช่วยคุณในการเขียนโค้ดที่ต้องการได้อย่างถูกต้องแต่มันไม่สามารถบังคับให้คุณเขียนโค้ดที่ดีได้ ขึ้นอยู่กับคุณ

แบ่งปันและเพลิดเพลิน


+1 สำหรับ "การทดสอบหน่วยจะเสร็จสมบูรณ์ก็ต่อเมื่อการทดสอบทั้งหมดผ่านและไม่สามารถเขียนการทดสอบใหม่ที่จะล้มเหลว" หลายคนกำลังบอกว่ามันถูกต้องตามกฎหมายเพื่อคืนค่าคงที่ แต่ไม่ตามด้วย "ในระยะสั้น" หรือ " หากข้อกำหนดโดยรวมต้องการเฉพาะกรณีเหล่านั้นเท่านั้น "
Thymine

1

ฉันเห็นด้วย 100% กับข้อเสนอแนะของ Robert Harveys ที่นี่ไม่ใช่แค่การทำข้อสอบคุณต้องคำนึงถึงเป้าหมายโดยรวมด้วยเช่นกัน

ในฐานะที่เป็นวิธีแก้ปัญหาของคุณ "มันได้รับการตรวจสอบแล้วว่าสามารถทำงานกับอินพุตที่กำหนด" ฉันขอเสนอโดยใช้การทดสอบที่ขับเคลื่อนด้วยข้อมูลเช่นทฤษฎี xunit พลังที่อยู่เบื้องหลังแนวคิดนี้คือมันช่วยให้คุณสร้างข้อมูลจำเพาะของอินพุตได้อย่างง่ายดาย

สำหรับ Factorials การทดสอบจะมีลักษณะเช่นนี้:

    [Theory]
    [InlineData(0, 1)]
    [InlineData( 1, 1 )]
    [InlineData( 2, 2 )]
    [InlineData( 3, 6 )]
    [InlineData( 4, 24 )]
    public void Test_Factorial(int input, int expected)
    {
        int result = Factorial( input );
        Assert.Equal( result, expected);
    }

คุณสามารถนำเสนอการทดสอบข้อมูล (ที่ส่งคืนIEnumerable<Tuple<xxx>>) และเข้ารหัสค่าคงที่ทางคณิตศาสตร์เช่นการหารซ้ำ ๆ โดย n จะให้ผลตอบแทน n-1)

ฉันพบว่า TP นี้เป็นวิธีทดสอบที่ทรงพลังมาก


1

หากคุณยังคงสามารถโกงการทดสอบนั้นไม่เพียงพอ เขียนแบบทดสอบเพิ่มเติม! สำหรับตัวอย่างของคุณฉันจะพยายามเพิ่มการทดสอบด้วยอินพุต 1, -1, -1000, 0, 10, 200

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

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


1
ดูเหมือนว่าคุณกำลังบอกว่าเพียงแค่เขียนรหัสเพียงพอสำหรับการทดสอบเพื่อส่งผ่าน (ตามที่ผู้สนับสนุน TDD) นั้นไม่เพียงพอ คุณต้องคำนึงถึงหลักการออกแบบซอฟต์แวร์เสียงด้วย ฉันเห็นด้วยกับคุณ BTW
Robert Harvey

0

ฉันขอแนะนำให้คุณเลือกการทดสอบไม่ใช่การทดสอบที่ดีที่สุด

ฉันจะเริ่มด้วย:

แฟกทอเรียล (1) เป็นการทดสอบครั้งแรก

แฟกทอเรียล (0) เป็นตัวที่สอง

แฟกทอเรียล (-ve) เป็นอันดับสาม

แล้วดำเนินการต่อกับกรณีที่ไม่น่ารำคาญ

และจบลงด้วยกรณีล้น


คืออะไร-ve??
Robert Harvey

ค่าลบ
Chris Cudmore
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.