จะเกิดอะไรขึ้นกับการทดสอบเมธอดเมื่อเมธอดนั้นเป็นไพรเวตหลังจากถูกออกแบบใหม่ใน TDD?


29

สมมติว่าฉันเริ่มพัฒนาเกมสวมบทบาทกับตัวละครที่โจมตีตัวละครอื่นและสิ่งต่าง ๆ

ใช้ TDD ฉันสร้างกรณีทดสอบเพื่อทดสอบตรรกะภายในCharacter.receiveAttack(Int)วิธี บางสิ่งเช่นนี้

@Test
fun healthIsReducedWhenCharacterIsAttacked() {
    val c = Character(100) //arg is the health
    c.receiveAttack(50) //arg is the suffered attack damage
    assertThat(c.health, is(50));
}

บอกว่าผมมี 10 วิธีการทดสอบreceiveAttackวิธีการ ตอนนี้ผมเพิ่มวิธีการCharacter.attack(Character)(ที่โทรreceiveAttackวิธี) และหลังจากที่บางรอบ TDD ทดสอบนั้นผมตัดสินใจ: ควรจะเป็นCharacter.receiveAttack(Int)private

จะเกิดอะไรขึ้นกับ 10 กรณีทดสอบก่อนหน้า? ฉันควรจะลบมันเหรอ? ฉันควรรักษาวิธีpublic(ฉันไม่คิดอย่างนั้น)?

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



10
หากเป็นส่วนตัวคุณไม่ได้ทดสอบมันง่ายมาก ลบและทำการเต้น refactor
kayess

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

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

2
@gnat แต่ฉันไม่เคยพูดอะไรเกี่ยวกับ "ไม่มีความคุ้มครอง"? ความคิดเห็นของฉันเกี่ยวกับ "ฉันชอบการทดสอบมากกว่าการทดสอบน้อยกว่า" ควรทำให้ชัดเจน ไม่แน่ใจว่าคุณได้รับอะไรอย่างแน่นอนฉันยังทดสอบรหัสที่ฉันแยก นั่นคือประเด็นทั้งหมด
user9993

คำตอบ:


52

ใน TDD การทดสอบทำหน้าที่เป็นเอกสารประกอบการออกแบบของคุณ การออกแบบของคุณเปลี่ยนไปอย่างชัดเจนเอกสารของคุณก็ต้องเหมือนกัน!

โปรดทราบว่าใน TDD วิธีเดียวที่attackวิธีนี้อาจปรากฏขึ้นนั้นเป็นผลมาจากการผ่านการทดสอบที่ล้มเหลว ซึ่งหมายความว่าattackกำลังถูกทดสอบโดยการทดสอบอื่น ซึ่งหมายความว่าทางอ้อม receiveAttackจะครอบคลุมโดยattackการทดสอบของ เป็นการดีที่การเปลี่ยนแปลงใด ๆ ที่receiveAttackควรทำลายattackการทดสอบอย่างน้อยหนึ่งครั้ง

และหากไม่เป็นเช่นนั้นก็มีฟังก์ชั่นการทำงานreceiveAttackที่ไม่จำเป็นอีกต่อไปและไม่ควรมีอยู่อีกต่อไป!

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


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

20
@DavidArno: ฉันไม่เห็นด้วยเพราะเหตุผลที่ว่าภายในไม่ควรมีการทดสอบโมดูล อย่างไรก็ตามภายในของโมดูลอาจมีความซับซ้อนมากดังนั้นการทดสอบหน่วยสำหรับแต่ละฟังก์ชั่นภายในของแต่ละบุคคลจึงมีค่า การทดสอบหน่วยจะใช้ในการตรวจสอบค่าคงที่ของชิ้นส่วนของการทำงานหากวิธีการส่วนตัวมีค่าคงที่ (pre-เงื่อนไข / หลังเงื่อนไข) แล้วการทดสอบหน่วยจะมีค่า
Matthieu M.

8
" ด้วยเหตุผลที่ว่า internals ของโมดูลไม่ควรถูกทดสอบ " ไม่ควรทำการทดสอบภายในเหล่านั้นโดยตรง การทดสอบทั้งหมดควรทดสอบ API สาธารณะเท่านั้น หากองค์ประกอบภายในไม่สามารถเข้าถึงได้ผ่าน API สาธารณะให้ลบออกโดยไม่ทำอะไรเลย
David Arno

28
@DavidArno โดยตรรกะนั้นถ้าคุณกำลังสร้างไฟล์ปฏิบัติการ (แทนที่จะเป็นไลบรารี) คุณก็ไม่ควรทำการทดสอบหน่วยใด ๆ เลย - "การเรียกใช้ฟังก์ชันไม่ได้เป็นส่วนหนึ่งของ API สาธารณะ! มีเพียงอาร์กิวเมนต์บรรทัดคำสั่งเท่านั้น! หากฟังก์ชันภายในของโปรแกรมของคุณไม่สามารถเข้าถึงได้ผ่านอาร์กิวเมนต์บรรทัดคำสั่งให้ลบออกโดยไม่ทำอะไรเลย" - ในขณะที่ฟังก์ชั่นส่วนตัวไม่ได้เป็นส่วนหนึ่งของ API สาธารณะของชั้นเรียน แต่เป็นส่วนหนึ่งของ API ภายในของชั้นเรียน และในขณะที่คุณไม่จำเป็นต้องทดสอบ API ภายในของคลาสคุณสามารถทำได้โดยใช้ตรรกะเดียวกันสำหรับการทดสอบ API ภายในของไฟล์เรียกทำงาน
RM

7
@RM หากฉันต้องการสร้างไฟล์ที่ไม่ทำงานแบบแยกส่วนฉันจะถูกบังคับให้เลือกระหว่างการทดสอบแบบเปราะของ internals หรือใช้การทดสอบการรวมโดยใช้ executable และ runtime I / O ดังนั้นด้วยเหตุผลที่แท้จริงของฉันแทนที่จะเป็นคนทำฟางของฉันฉันจะสร้างมันขึ้นมาในรูปแบบโมดูลาร์ (เช่นผ่านชุดของห้องสมุด) API สาธารณะของโมดูลเหล่านั้นสามารถทดสอบได้ในแบบที่ไม่เปราะ
David Arno

23

หากวิธีการนั้นซับซ้อนพอที่จะต้องทำการทดสอบมันควรจะเปิดเผยในบางชั้นเรียน ดังนั้นคุณ refactor จาก:

public class X {
  private int complexity(...) {
    ...
  }
  public void somethingElse() {
    int c = complexity(...);
  }
}

ไปที่:

public class Complexity {
  public int calculate(...) {
    ...
  }
}

public class X {
  private Complexity complexity;
  public X(Complexity complexity) { // dependency injection happiness
    this.complexity = complexity;
  }

  public void something() {
    int c = complexity.calculate(...);
  }
}

ย้ายการทดสอบปัจจุบันสำหรับ X.complexity ไปที่ ComplexityTest จากนั้นส่งข้อความ X.something โดยการเยาะเย้ยความซับซ้อน

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


คำตอบของคุณอธิบายให้ชัดเจนยิ่งขึ้นถึงความคิดที่ฉันพยายามอธิบายในความคิดเห็นของฉันเกี่ยวกับคำถามของ OP คำตอบที่ดี
user9993

3
ขอบคุณสำหรับคำตอบ. ที่จริงแล้วเมธอด acceptAttack นั้นค่อนข้างง่าย ( this.health = this.health - attackDamage) บางทีการแยกไปเรียนอีกชั้นหนึ่งเป็นวิธีแก้ปัญหาที่ overengineered ในขณะนี้
หมอ

1
นี่เป็นวิธีที่เกินราคาแน่นอนสำหรับ OP - เขาต้องการขับรถไปที่ร้านไม่ใช่บินไปยังดวงจันทร์ - แต่เป็นทางออกที่ดีในกรณีทั่วไป

ถ้าฟังก์ชั่นนั้นง่ายมากบางทีมันอาจจะเกินความจริงที่ว่ามันถูกกำหนดให้เป็นฟังก์ชั่นในตอนแรก
David K

1
มันอาจจะ overkill วันนี้แต่ในเวลา 6 เดือนเมื่อมีการเปลี่ยนแปลงรหัสนี้ผลประโยชน์จะชัดเจน และใน IDE ที่เหมาะสมในทุกวันนี้การแยกรหัสบางส่วนไปยังคลาสที่แยกต่างหากน่าจะเป็นวิธีการกดแป้นพิมพ์สองสามครั้งที่แทบจะเป็นวิธีแก้ปัญหาที่ไม่ได้วางแผนทางวิศวกรรมมากนัก
Stephen Byrne

6

บอกว่าฉันมี 10 วิธีการทดสอบวิธี getAttack ตอนนี้ฉันเพิ่มเมธอด Character.attack (ตัวละคร) (ที่เรียกเมธอด getAttack) และหลังจากที่วงรอบ TDD ทำการทดสอบฉันตัดสินใจ: Character.receiveAttack (Int) ควรเป็นแบบส่วนตัว

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

  1. หากคุณไม่ต้องการลบออกให้ปล่อยไว้ใน API
  2. หากคุณไม่จำเป็นต้องเอามันออกไปเลยแล้วทำเครื่องหมายว่าเลิกใช้แล้วและหากเอกสารเป็นไปได้เมื่อสิ้นสุดของชีวิตที่จะเกิดขึ้น
  3. หากคุณต้องการลบมันแสดงว่าคุณมีการเปลี่ยนแปลงรุ่นใหญ่

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

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


3
การคัดค้านไม่ใช่เรื่องที่น่ากังวลเสมอไป จากคำถาม: "สมมุติว่าฉันเริ่มพัฒนา ... " หากซอฟต์แวร์ยังไม่วางจำหน่ายการเลิกใช้งานก็ไม่ใช่ประเด็น นอกจากนี้: "เกมสวมบทบาท" หมายถึงสิ่งนี้ไม่ใช่ห้องสมุดที่ใช้ซ้ำได้ แต่เป็นซอฟต์แวร์ไบนารีที่มุ่งเป้าไปที่ผู้ใช้ปลายทาง ในขณะที่ซอฟต์แวร์สำหรับผู้ใช้บางรายมี API สาธารณะ (เช่น MS Office) แต่ส่วนใหญ่ไม่มี แม้ซอฟต์แวร์ที่ไม่ได้มีประชาชน API เพียง แต่มีส่วนหนึ่งของมันสัมผัสสำหรับปลั๊กอินสคริปต์ (เช่นเกมที่มีนามสกุลลัวะ) หรือฟังก์ชั่นอื่น ๆ ถึงกระนั้นมันก็คุ้มค่าที่จะนำเสนอแนวคิดสำหรับกรณีทั่วไปที่ OP อธิบาย
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.