การปลอมตัวเป็นส่วนหนึ่งของชั้นเรียนภายใต้การทดสอบหรือไม่?


22

สมมติว่าฉันมีชั้นเรียน (ยกโทษให้ตัวอย่างที่ออกแบบและการออกแบบที่ไม่ดีของมัน):

class MyProfit
{
  public decimal GetNewYorkRevenue();
  public decimal GetNewYorkExpenses();
  public decimal GetNewYorkProfit();

  public decimal GetMiamiRevenue();
  public decimal GetMiamiExpenses();
  public decimal GetMiamiProfit();

  public bool BothCitiesProfitable();

}

(หมายเหตุเมธอด GetxxxRevenue () และ GetxxxExpenses () มีการขึ้นต่อกันที่ถูกสตับออก)

ตอนนี้ฉันกำลังทดสอบหน่วย BothCitiesProfitable () ซึ่งขึ้นอยู่กับ GetNewYorkProfit () และ GetMiamiProfit () การรับ GetNewYorkProfit () และ GetMiamiProfit () ดีไหม

ดูเหมือนว่าถ้าฉันไม่ทำฉันก็ทดสอบ GetNewYorkProfit () และ GetMiamiProfit () พร้อมกับ BothCitiesProfitable () พร้อมกัน ฉันต้องตรวจสอบให้แน่ใจว่าฉันได้ทำการตั้งค่า stubbing สำหรับ GetxxxRevenue () และ GetxxxExpenses () เพื่อให้เมธอด GetxxxProfit () ส่งคืนค่าที่ถูกต้อง

จนถึงตอนนี้ฉันเห็นเพียงตัวอย่างของการขัดถูการพึ่งพาคลาสภายนอกไม่ใช่วิธีการภายใน

และถ้าไม่เป็นไรมีรูปแบบเฉพาะที่ฉันควรใช้ทำสิ่งนี้หรือไม่?

UPDATE

ฉันกังวลว่าเราอาจพลาดประเด็นหลักและนั่นอาจเป็นความผิดของตัวอย่างที่ไม่ดีของฉัน คำถามพื้นฐานคือ: หากวิธีการในชั้นเรียนมีการพึ่งพาวิธีการเปิดเผยต่อสาธารณะอื่นในชั้นเรียนเดียวกันนั้นมันโอเค (หรือแนะนำแม้) เพื่อตอวิธีอื่น ๆ ออกมา?

บางทีฉันอาจจะพลาดอะไรบางอย่าง แต่ฉันไม่แน่ใจว่าการแยกชั้นเรียนจะทำให้รู้สึกได้เสมอ บางทีอีกตัวอย่างที่ดีกว่าคือ:

class Person
{
 public string FirstName()
 public string LastName()
 public string FullName()
}

โดยที่ชื่อเต็มถูกกำหนดเป็น:

public string FullName()
{
  return FirstName() + " " + LastName();
}

มันจะโอเคกับชื่อต้นขั้ว () และนามสกุล () เมื่อทดสอบ FullName () หรือไม่?


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

เพิ่งทราบเกี่ยวกับการอัปเดตของคุณฉันได้อัปเดตคำตอบแล้ว
Winston Ewert

คำตอบ:


27

คุณควรเลิกชั้นเรียนด้วยคำถาม

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

ไม่สนใจความโง่ของการออกแบบนี้:

class NewYork
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}


class Miami
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}

class MyProfit
{
     MyProfit(NewYork new_york, Miami miami);
     boolean bothProfitable();
}

UPDATE

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

ความจริงที่ว่า FullName ใช้ FirstName และ LastName เป็นรายละเอียดการนำไปใช้งาน ไม่มีสิ่งใดนอกชั้นเรียนที่ควรใส่ใจว่าเป็นเรื่องจริง โดยการเยาะเย้ยวิธีการสาธารณะเพื่อทดสอบวัตถุคุณกำลังทำการสันนิษฐานเกี่ยวกับวัตถุนั้น

ในบางจุดในอนาคตข้อสันนิษฐานนั้นอาจยุติลงอย่างถูกต้อง บางทีตรรกะชื่อทั้งหมดจะถูกย้ายไปยังวัตถุชื่อที่บุคคลเพียงแค่เรียก บางที FullName จะเข้าถึงตัวแปรสมาชิก first_name และ last_name โดยตรงจากนั้นเรียก FirstName และ LastName

คำถามที่สองคือสาเหตุที่คุณรู้สึกว่าจำเป็นต้องทำเช่นนั้น หลังจากทุกคลาสบุคคลของคุณสามารถทดสอบสิ่งที่ชอบ:

Person person = new Person("John", "Doe");
Test.AssertEquals(person.FullName(), "John Doe");

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

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

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

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

อัพเดทอีกครั้ง

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

ขณะนี้มีการทดสอบพฤติกรรมภายในเนื่องจากเป็นสิ่งที่ส่งผลให้เกิดพฤติกรรมภายนอก แต่ฉันไม่ได้เขียนการทดสอบโดยตรงเกี่ยวกับพฤติกรรมภายในเพียงทางอ้อมผ่านพฤติกรรมภายนอก

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

แต่มันแตกต่างกันอย่างไร ถ้า FirstName () และ LastName () เป็นสมาชิกของวัตถุอื่นจริง ๆ แล้วมันจะเปลี่ยนปัญหาของ FullName () หรือไม่? หากเราตัดสินใจว่าจำเป็นต้องจำลองชื่อจริงและนามสกุลจริงจะช่วยให้พวกเขาอยู่ในวัตถุอื่นได้หรือไม่

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

แก้ไข

ลองย้อนกลับไปถามว่าทำไมเราเลียนแบบวัตถุเมื่อเราทดสอบ

  1. ทำให้การทดสอบทำงานอย่างสม่ำเสมอ (หลีกเลี่ยงการเข้าถึงสิ่งที่เปลี่ยนแปลงจากการทำงานเป็นแบบเรียกใช้)
  2. หลีกเลี่ยงการเข้าถึงทรัพยากรที่มีราคาแพง (อย่ากดบริการของบุคคลที่สามเป็นต้น)
  3. ลดความซับซ้อนของระบบภายใต้การทดสอบ
  4. ทำให้ง่ายต่อการทดสอบสถานการณ์ที่เป็นไปได้ทั้งหมด (เช่นสิ่งต่าง ๆ เช่นการจำลองความล้มเหลว ฯลฯ )
  5. หลีกเลี่ยงการขึ้นอยู่กับรายละเอียดของรหัสส่วนอื่น ๆ เพื่อให้การเปลี่ยนแปลงในส่วนอื่น ๆ ของรหัสจะไม่ทำลายการทดสอบนี้

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

ฉันคิดว่าข้อกังวลของคุณคือเหตุผลข้อที่ 5 ข้อกังวลคือในบางจุดในอนาคตการเปลี่ยนแปลงการใช้งานของ FirstName และ LastName จะทำให้การทดสอบหยุดชะงัก ในอนาคตชื่อและนามสกุลอาจได้รับชื่อจากตำแหน่งที่ตั้งอื่นหรือแหล่งที่มา แต่ FullName FirstName() + " " + LastName()อาจจะเสมอ นั่นเป็นเหตุผลที่คุณต้องการทดสอบ FullName โดยการจำลองชื่อและนามสกุล

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

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

ฉันสงสัยว่าคุณอาจคัดแยกวัตถุของคุณ แต่ทำไม?

แก้ไข

ฉันผิดไป.

คุณควรแยกวัตถุค่อนข้างแนะนำ ad-hoc splits โดยเยาะเย้ยแต่ละวิธี อย่างไรก็ตามฉันมุ่งเน้นไปที่วิธีแยกวัตถุ อย่างไรก็ตาม OO มีวิธีการแยกวัตถุหลายวิธี

สิ่งที่ฉันเสนอ:

class PersonBase
{
      abstract sring FirstName();
      abstract string LastName();

      string FullName()
      {
            return FirstName() + " " + LastName();
      }
 }

 class Person extends PersonBase
 {
      string FirstName(); 
      string LastName();
 }

 class FakePerson extends PersonBase
 {
      void setFirstName(string);
      void setLastName(string);
      string getFirstName();
      string getLastName();
 }

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

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


8
พูดได้ดี. หากคุณกำลังพยายามหาวิธีแก้ปัญหาในการทดสอบคุณอาจจำเป็นต้องพิจารณาการออกแบบของคุณอีกครั้ง (ตามด้านข้างฉันตอนนี้เสียงของ Frank Sinatra ติดอยู่ในหัวของฉัน)
ปัญหา

2
"โดยการเยาะเย้ยวิธีการสาธารณะเพื่อทดสอบวัตถุคุณกำลังทำการสันนิษฐานว่าวัตถุนั้นถูกนำไปใช้อย่างไร แต่นั่นไม่ใช่กรณีที่ทุกครั้งที่คุณเริ่มต้นวัตถุ? ตัวอย่างเช่นสมมติว่าฉันรู้วิธีการของฉัน xyz () เรียกวัตถุอื่น ๆ เพื่อทดสอบ xyz () ในการแยกฉันต้อง stub วัตถุอื่น ดังนั้นการทดสอบของฉันต้องรู้เกี่ยวกับรายละเอียดการใช้งานของวิธี xyz () ของฉัน
ผู้ใช้

ในกรณีของฉันวิธีการ "FirstName ()" และ "LastName ()" นั้นง่าย แต่พวกเขาจะสอบถาม API ของบุคคลที่สามสำหรับผลลัพธ์ของพวกเขา
ผู้ใช้

@ ผู้ใช้อัปเดตคงคุณกำลังเยาะเย้ย API ของบุคคลที่สามเพื่อทดสอบชื่อและนามสกุล เกิดอะไรขึ้นกับการเยาะเย้ยแบบเดียวกันเมื่อทดสอบ FullName
Winston Ewert

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

10

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

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


1
ตกลง แต่เพียงเพื่อเน้นจุดสำคัญให้กับทุกคนที่อ่านสิ่งนี้การแก้ปัญหาที่คุณใช้ถ้าคุณไม่สามารถทำสิ่งที่ดีกว่าได้
Winston Ewert

5
@Winston และนี่คือวิธีการแก้ปัญหาที่คุณใช้ก่อนที่จะหาทางออกที่ดีกว่า เช่นมีรหัสลูกใหญ่คุณต้องครอบคลุมด้วยการทดสอบหน่วยก่อนที่คุณจะสามารถ refactor
PéterTörök

@ PéterTörökจุดดี แม้ว่าฉันจะคิดว่ามันอยู่ภายใต้ "ไม่สามารถแก้ปัญหาได้ดีกว่า" :)
Winston Ewert

@ การทดสอบเมธอด getProfit () จะเป็นเรื่องยากมากเนื่องจากสถานการณ์ที่แตกต่างกันสำหรับ getExpenses () และ getRevenues () จะคูณสถานการณ์จริง ๆ ที่จำเป็นสำหรับ getProfit () ถ้าเรากำลังทดสอบ getExpenses () และรับรายได้ () อย่างอิสระมันไม่ควรที่จะจำลองวิธีการเหล่านี้เพื่อทดสอบ getProfit () หรือไม่?
Mohd Farid

7

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

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

หากคุณกำลังทดสอบลักษณะการทำงานของทั้งระบบ (จากC()มุมมองของ) ฉันจะออกจากA()และB()ดำเนินการตามและออกจากการอ้างอิงของพวกเขา (ถ้ามันทำให้มันเป็นไปได้ที่จะทดสอบ) หรือตั้งค่าสภาพแวดล้อม Sandbox เช่นการทดสอบ ฐานข้อมูล ฉันจะเรียกสิ่งนี้ว่าเป็นการทดสอบการรวมระบบ

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

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