เส้นแบ่งระหว่างตรรกะการทดสอบหน่วยแอปพลิเคชันกับการสร้างภาษาที่ไม่ไว้ใจอยู่ที่ไหน


87

พิจารณาฟังก์ชั่นเช่นนี้:

function savePeople(dataStore, people) {
    people.forEach(person => dataStore.savePerson(person));
}

มันอาจจะใช้แบบนี้:

myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);

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

ควรsavePeople()ทดสอบหน่วยหรือจำนวนการทดสอบดังกล่าวเพื่อทดสอบการสร้างforEachภาษาในตัว?

แน่นอนเราสามารถผ่านการเยาะเย้ยdataStoreและยืนยันที่dataStore.savePerson()เรียกว่าครั้งเดียวสำหรับแต่ละคน แน่นอนคุณสามารถโต้แย้งว่าการทดสอบดังกล่าวมีความปลอดภัยต่อการเปลี่ยนแปลงการใช้งานเช่นหากเราตัดสินใจแทนที่forEachด้วยการforวนซ้ำดั้งเดิมหรือวิธีการทำซ้ำอื่น ๆ ดังนั้นการทดสอบไม่ได้อย่างสิ้นเชิงจิ๊บจ๊อย และยังดูเหมือนว่าใกล้มาก ...


นี่เป็นอีกตัวอย่างที่อาจมีผลมากกว่า พิจารณาฟังก์ชั่นที่ไม่ทำอะไรเลยนอกจากประสานงานวัตถุหรือฟังก์ชั่นอื่น ๆ ตัวอย่างเช่น:

function bakeCookies(dough, pan, oven) {
    panWithRawCookies = pan.add(dough);
    oven.addPan(panWithRawCookies);
    oven.bakeCookies();
    oven.removePan();
}

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

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


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

เมื่อผู้ใช้สร้างบัญชีใหม่สิ่งต่าง ๆ ต้องเกิดขึ้น: 1) ต้องมีการสร้างบันทึกผู้ใช้ใหม่ในฐานข้อมูล 2) ต้องส่งอีเมลต้อนรับ 3) ที่อยู่ IP ของผู้ใช้ต้องถูกบันทึกไว้เนื่องจากการฉ้อโกง วัตถุประสงค์

ดังนั้นเราต้องการสร้างวิธีการที่เชื่อมโยงทุกขั้นตอน "ผู้ใช้ใหม่":

function createNewUser(validatedUserData, emailService, dataStore) {
  userId = dataStore.insertUserRecord(validateduserData);
  emailService.sendWelcomeEmail(validatedUserData);
  dataStore.recordIpAddress(userId, validatedUserData.ip);
}

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

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


44
หลังจากที่มันทำงาน คุณมีคุกกี้ไหม
Ewan

6
เกี่ยวกับการอัปเดตของคุณ: ทำไมคุณถึงอยากล้อเลียน? หรือแป้ง? เสียงเหล่านี้เหมือนกับวัตถุในหน่วยความจำง่าย ๆ ที่ควรสร้างขึ้นเล็กน้อยดังนั้นจึงไม่มีเหตุผลว่าทำไมคุณไม่ควรทดสอบมันเป็นหน่วยเดียว โปรดจำไว้ว่า "หน่วย" ใน "การทดสอบหน่วย" ไม่ได้หมายถึง "คลาสเดียว" มันหมายถึง "หน่วยที่เล็กที่สุดของรหัสที่ใช้ในการทำให้เสร็จ" กระทะอาจจะไม่มีอะไรมากไปกว่าภาชนะสำหรับวัตถุแป้งดังนั้นจึงควรวางแผนที่จะทดสอบแยกเดี่ยวแทนที่จะทดสอบวิธีการขับรถ bakeCookies จากด้านนอกใน
sara

11
ในตอนท้ายของวันหลักการพื้นฐานในการทำงานที่นี่คือคุณเขียนการทดสอบมากพอที่จะรับรองตัวเองว่ารหัสทำงานได้และมันก็เป็น "นกขมิ้นในเหมืองถ่านหิน" ที่เพียงพอเมื่อมีคนเปลี่ยนแปลงบางอย่าง แค่นั้นแหละ. ไม่มีคาถาเวทมนต์ suppositions สูตรหรือยืนยัน dogmatic ซึ่งเป็นเหตุผลที่ครอบคลุมรหัส 85% ถึง 90% (ไม่ 100%) ถือว่าเป็นที่ยอดเยี่ยม
Robert Harvey

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

4
การทดสอบหน่วยเพื่อลดความซับซ้อนของวัฏจักร เชื่อฉันสิคุณจะหมดเวลาก่อนที่คุณจะเข้าฟังก์ชั่นนี้
Neil McGuigan

คำตอบ:


118

ควรsavePeople()จะทดสอบหน่วย? ใช่. คุณไม่ได้ทำการทดสอบที่ใช้dataStore.savePersonงานได้หรือการเชื่อมต่อฐานข้อมูลนั้นใช้foreachงานได้ คุณกำลังทดสอบที่savePeopleทำตามสัญญาที่ทำผ่านสัญญา

ลองนึกภาพสถานการณ์นี้: ใครบางคนทำ refactor ขนาดใหญ่ของฐานรหัสและลบforEachส่วนหนึ่งของการนำไปใช้โดยไม่ได้ตั้งใจเพื่อให้มันบันทึกเฉพาะรายการแรกเท่านั้น คุณไม่ต้องการทดสอบหน่วยที่จะจับที่?


20
@RobertHarvey: มีพื้นที่สีเทามากมายและทำให้ความแตกต่าง IMO ไม่สำคัญ คุณพูดถูก - มันไม่สำคัญเลยที่จะทดสอบว่า "มันเรียกฟังก์ชั่นที่เหมาะสม" แต่มันค่อนข้าง "ทำในสิ่งที่ถูกต้อง" ไม่ว่ามันจะทำงานอย่างไร สิ่งสำคัญคือการทดสอบว่ากำหนดชุดอินพุตเฉพาะให้กับฟังก์ชันที่คุณได้รับชุดเอาต์พุตเฉพาะ อย่างไรก็ตามฉันเห็นได้ว่าประโยคสุดท้ายนั้นอาจสร้างความสับสนดังนั้นฉันจึงลบออก
ไบรอัน Oakley

64
"คุณกำลังทดสอบว่า savePeople ปฏิบัติตามสัญญาที่ทำผ่านสัญญา" นี้. มากขนาดนี้
Lovis

2
ถ้าคุณไม่มีการทดสอบระบบ "จบสิ้น" ที่ครอบคลุม
เอียน

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

4
@VincentSavard แต่ค่าใช้จ่าย / ผลประโยชน์ของการทดสอบหน่วยจะลดลงหากความเสี่ยงมีการควบคุมในทางอับละอองเกสร
เอียน

36

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

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

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

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


4
ทำการทดสอบอินเตอร์เฟสโดยทั่วไปไม่ใช่การติดตั้ง
นูป

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

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

ขอบคุณสำหรับการอัพเดท. คุณจะทดสอบsavePeopleอย่างไร ตามที่อธิบายไว้ในย่อหน้าสุดท้ายของ OP หรือวิธีอื่น ๆ ?
Jonah

1
ขออภัยฉันไม่ได้อธิบายตัวเองอย่างชัดเจนกับส่วน "ไม่มี mocks ที่เกี่ยวข้อง" ฉันหมายถึงฉันจะไม่ใช้จำลองสำหรับฟังก์ชั่นเช่นคุณแนะนำแทนฉันต้องการทดสอบผ่านทั่วไปมากขึ้นsavePerson savePeopleการทดสอบหน่วยสำหรับStoreจะถูกเปลี่ยนให้ทำงานsavePeopleแทนการโทรโดยตรงsavePersonดังนั้นสิ่งนี้จึงไม่ใช้ mocks แต่ไม่ควรมีฐานข้อมูลของหลักสูตรเนื่องจากเราต้องการแยกปัญหาการเข้ารหัสจากปัญหาการรวมที่เกิดขึ้นกับฐานข้อมูลจริงดังนั้นที่นี่เรายังมีการเยาะเย้ย
MichelHenrich

21

ควร savePeople () ทดสอบเป็นหน่วย

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

function testSavePeople() {
    myDataStore = new Store('some connection string', 'password');
    myPeople = ['Joe', 'Maggie', 'John'];
    savePeople(myDataStore, myPeople);
    assert(myDataStore.containsPerson('Joe'));
    assert(myDataStore.containsPerson('Maggie'));
    assert(myDataStore.containsPerson('John'));
}

การทดสอบนี้ทำหลายสิ่ง:

  • มันตรวจสอบสัญญาของฟังก์ชั่น savePeople()
  • มันไม่สนใจเกี่ยวกับการดำเนินการของ savePeople()
  • มันบันทึกตัวอย่างการใช้งานของ savePeople()

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

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

หรือจำนวนการทดสอบดังกล่าวเพื่อทดสอบโครงสร้างภาษาสำหรับแต่ละภาษาหรือไม่

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

เกี่ยวกับการอัปเดตคำถามของคุณ:

ทดสอบการเปลี่ยนแปลงสถานะ! เช่นจะใช้แป้งบางส่วน ตามการใช้งานของคุณยืนยันว่าจำนวนการใช้งานที่doughพอดีpanหรือยืนยันว่าการdoughใช้หมดแล้ว ยืนยันว่าpanมีคุกกี้หลังจากการเรียกใช้ฟังก์ชัน ยืนยันว่าovenว่าง / อยู่ในสถานะเดียวกับเมื่อก่อน

สำหรับการทดสอบเพิ่มเติมตรวจสอบกรณีขอบ: จะเกิดอะไรขึ้นถ้าovenไม่ว่างเปล่าก่อนที่จะโทร? เกิดอะไรขึ้นถ้ามีอยู่ไม่เพียงพอdough? ถ้าpanเต็มแล้ว?

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

ในความเป็นจริงผู้ใช้ TDD ส่วนใหญ่เขียนการทดสอบก่อนที่พวกเขาจะเขียนฟังก์ชันดังนั้นพวกเขาจึงไม่ได้ขึ้นอยู่กับการใช้งานจริง


สำหรับการเพิ่มล่าสุดของคุณ:

เมื่อผู้ใช้สร้างบัญชีใหม่สิ่งต่าง ๆ ต้องเกิดขึ้น: 1) ต้องมีการสร้างบันทึกผู้ใช้ใหม่ในฐานข้อมูล 2) ต้องส่งอีเมลต้อนรับ 3) ที่อยู่ IP ของผู้ใช้ต้องถูกบันทึกไว้เนื่องจากการฉ้อโกง วัตถุประสงค์

ดังนั้นเราต้องการสร้างวิธีการที่เชื่อมโยงทุกขั้นตอน "ผู้ใช้ใหม่":

function createNewUser(validatedUserData, emailService, dataStore) {
    userId = dataStore.insertUserRecord(validateduserData);
    emailService.sendWelcomeEmail(validatedUserData);
    dataStore.recordIpAddress(userId, validatedUserData.ip);
}

สำหรับฟังก์ชั่นเช่นนี้ฉันจะเยาะเย้ย / ต้นขั้ว / ปลอม (สิ่งที่ดูเหมือนทั่วไปมากขึ้น) dataStoreและemailServiceพารามิเตอร์ ฟังก์ชั่นนี้ไม่ได้ทำการเปลี่ยนสถานะใด ๆ กับพารามิเตอร์ใด ๆ ด้วยตัวเองมันจะมอบหมายให้กับวิธีการบางอย่างของพวกเขา ฉันจะพยายามตรวจสอบว่าการเรียกใช้ฟังก์ชันทำ 4 สิ่ง:

  • มันแทรกผู้ใช้ลงในแหล่งข้อมูล
  • มันส่ง (หรืออย่างน้อยเรียกว่าวิธีการที่เกี่ยวข้อง) อีเมลต้อนรับ
  • มันบันทึก IP ของผู้ใช้ลงในแหล่งข้อมูล
  • มันมอบหมายข้อยกเว้น / ข้อผิดพลาดที่พบ (ถ้ามี)

การตรวจสอบ 3 ครั้งแรกสามารถทำได้ด้วย mocks, stubs หรือ fakes of dataStoreและemailService(คุณไม่ต้องการส่งอีเมลเมื่อทำการทดสอบ) เนื่องจากฉันต้องค้นหาความคิดเห็นบางส่วนสิ่งเหล่านี้จึงแตกต่าง:

  • ของปลอมเป็นวัตถุที่มีพฤติกรรมเหมือนกับต้นฉบับและอยู่ในขอบเขตที่แยกไม่ออก รหัสของมันสามารถนำกลับมาใช้ซ้ำได้ตลอดการทดสอบ เช่นนี้สามารถเป็นฐานข้อมูลในหน่วยความจำง่ายสำหรับ wrapper ฐานข้อมูล
  • ต้นขั้วเพียงดำเนินการเท่าที่จำเป็นเพื่อตอบสนองการดำเนินงานที่จำเป็นของการทดสอบนี้ ในกรณีส่วนใหญ่ต้นขั้วนั้นมีความเฉพาะเจาะจงกับการทดสอบหรือกลุ่มการทดสอบที่ต้องการวิธีการดั้งเดิมเพียงเล็กน้อย ในตัวอย่างนี้มันอาจจะเป็นdataStoreที่เพิ่งดำเนินการรุ่นที่เหมาะสมและinsertUserRecord()recordIpAddress()
  • การเยาะเย้ยเป็นวัตถุที่ช่วยให้คุณตรวจสอบวิธีการใช้งาน (ส่วนใหญ่มักให้คุณประเมินการโทรไปยังวิธีการ) ฉันพยายามใช้พวกเขาเท่าที่จำเป็นในการทดสอบหน่วยเนื่องจากใช้พวกคุณจริง ๆ แล้วลองทดสอบการใช้งานฟังก์ชั่นไม่ใช่การยึดติดกับส่วนต่อประสาน แต่มันก็ยังคงมีการใช้งานอยู่ เฟรมเวิร์กจำลองจำนวนมากมีอยู่เพื่อช่วยคุณสร้างเฉพาะจำลองที่คุณต้องการ

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

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

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

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

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

น่าเศร้าที่ในกรณีนั้นการทดสอบหน่วยสูญเสียความน่าเชื่อถือบางส่วน: เนื่องจากมีการเปลี่ยนแปลงจึงไม่ยืนยันฟังก์ชันหลังจากการเปลี่ยนแปลงนั้นทำแบบเดียวกับที่เคยเป็นมา

ยกตัวอย่างเช่นลองพิจารณาว่ามีคนเพิ่มการเรียกไปที่oven.preheat()(การเพิ่มประสิทธิภาพ!) ในตัวอย่างการอบคุกกี้ของคุณ:

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

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


1
ปัญหาคือทันทีที่คุณเขียนmyDataStore.containsPerson('Joe')คุณจะถือว่ามีฐานข้อมูลการทดสอบการทำงาน เมื่อคุณทำเช่นนั้นคุณกำลังเขียนการทดสอบการรวมและไม่ใช่การทดสอบหน่วย
Jonah

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

เพื่อชี้แจงหากคุณใช้การเยาะเย้ยทั้งหมดที่คุณสามารถทำได้คือยืนยันว่าวิธีการในการเยาะเย้ยนั้นเรียกว่าอาจจะมีพารามิเตอร์เฉพาะบางอย่าง คุณไม่สามารถยืนยันสถานะของการเยาะเย้ยในภายหลัง ดังนั้นหากคุณต้องการยืนยันสถานะของฐานข้อมูลหลังจากเรียกใช้เมธอดภายใต้การทดสอบเช่นเดียวกับในmyDataStore.containsPerson('Joe')คุณต้องใช้ฟังก์ชั่นฐานข้อมูลบางชนิด เมื่อคุณทำตามขั้นตอนนั้นจะไม่มีการทดสอบหน่วยอีกต่อไป
Jonah

1
มันไม่จำเป็นต้องเป็นฐานข้อมูลจริง - เพียงแค่วัตถุที่ใช้อินเทอร์เฟซเดียวกับแหล่งข้อมูลจริง (อ่าน: มันผ่านการทดสอบหน่วยที่เกี่ยวข้องสำหรับอินเทอร์เฟซแหล่งข้อมูล) ฉันยังคิดว่านี่เป็นเรื่องจำลอง ให้ mock เก็บทุกอย่างที่เพิ่มเข้ามาด้วยวิธีการใด ๆ ในอาร์เรย์และตรวจสอบว่าข้อมูลการทดสอบ (องค์ประกอบของmyPeople) อยู่ในอาร์เรย์หรือไม่ IMHO การเยาะเย้ยควรยังคงมีพฤติกรรมที่สังเกตได้เช่นเดียวกับที่คาดไว้จากวัตถุจริงมิฉะนั้นคุณกำลังทดสอบความสอดคล้องกับส่วนต่อประสานจำลองไม่ใช่ส่วนต่อประสานจริง
hoffmale

"ให้ mock เก็บทุกอย่างที่เพิ่มโดยวิธีการใด ๆ ในอาร์เรย์และตรวจสอบว่าข้อมูลการทดสอบ (องค์ประกอบของ myPeople) อยู่ในอาร์เรย์" - ยังคงเป็นฐานข้อมูล "ของจริง" เพียงแค่เฉพาะกิจ ในหน่วยความจำที่คุณสร้าง "IMHO คนเยาะเย้ยควรยังคงมีพฤติกรรมที่สังเกตได้เหมือนที่คาดหวังจากวัตถุจริง" - ฉันคิดว่าคุณสามารถสนับสนุนสิ่งนั้นได้ แต่นั่นไม่ใช่สิ่งที่ "เยาะเย้ย" หมายถึงในการทดสอบวรรณกรรมหรือในห้องสมุดที่เป็นที่นิยมของฉัน ได้เห็น เยาะเย้ยเพียงตรวจสอบว่าวิธีการที่คาดว่าจะเรียกว่ามีพารามิเตอร์ที่คาดหวัง
Jonah

13

ค่าหลักเช่นการทดสอบให้คือมันทำให้การใช้งานของคุณ refactorable

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

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

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

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

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

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

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


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

1
@ jpmc26 แล้วการทดสอบที่ทดสอบว่าผู้คนได้รับการช่วยให้รอด ... ?
immibis

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

1
@immibis ฉันไม่คิดว่าการทดสอบที่มีประโยชน์ การใช้แหล่งข้อมูลปลอมไม่ได้ให้ความมั่นใจเลยว่ามันจะใช้งานได้จริง ฉันจะรู้ได้อย่างไรว่างานปลอมของฉันเหมือนของจริง? ฉันอยากปล่อยให้ชุดการทดสอบการรวมครอบคลุม (ฉันควรจะชี้แจงว่าฉันหมายถึง " การทดสอบหน่วยใด ๆ" ในความคิดเห็นแรกของฉันที่นี่)
jpmc26

1
@immibis จริง ๆ แล้วฉันพิจารณาตำแหน่งของฉัน ฉันสงสัยเกี่ยวกับคุณค่าของการทดสอบหน่วยเพราะปัญหาเช่นนี้ แต่บางทีฉันประเมินค่าต่ำกว่าแม้ว่าคุณจะแกล้งทำอินพุต ผมทำรู้ว่าการทดสอบเข้า / ส่งออกมีแนวโน้มที่จะมากมีประโยชน์มากกว่าการทดสอบหนักเยาะเย้ย แต่อาจปฏิเสธที่จะเข้ามาแทนที่การป้อนข้อมูลปลอมเป็นจริงส่วนหนึ่งของปัญหาที่นี่
jpmc26

6

ควรbakeCookies()จะทดสอบ ใช่.

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

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

ในคำอื่น ๆที่คุณควรยืนยันว่าbakeCookies()อบคุกกี้

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

การทดสอบหน่วยทำหน้าที่สองฟังก์ชั่น:

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

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

อย่าทดสอบโค้ดภายในฟังก์ชั่น แทนที่จะทดสอบว่าฟังก์ชั่นทำในสิ่งที่มันบอกว่าทำ เมื่อคุณดูการทดสอบหน่วยด้วยวิธีนี้ (ทดสอบฟังก์ชั่นไม่ใช่รหัส) คุณจะรู้ว่าคุณไม่เคยทดสอบการสร้างภาษาหรือแม้แต่ตรรกะการใช้งาน คุณกำลังทดสอบ API


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

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

@James_pic แน่นอน และใช่นั่นคือคำจำกัดความจำลองที่ฉันใช้ เมื่อได้รับความคิดเห็นของคุณคุณจะทำอย่างไรในกรณีเช่นนี้? ยกเลิกการทดสอบ? เขียนการทดสอบการใช้งานซ้ำแล้วซ้ำอีกหรือไม่? อื่น ๆ อีก?
Jonah

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

3

ควร savePeople () ทำการทดสอบหน่วยหรือจำนวนการทดสอบดังกล่าวเพื่อทดสอบโครงสร้างภาษา forEach หรือไม่

ใช่. แต่คุณสามารถทำได้ในวิธีที่จะทดสอบการก่อสร้างอีกครั้ง

สิ่งที่ควรทราบที่นี่คือฟังก์ชั่นนี้ทำงานอย่างไรเมื่อsavePersonล้มเหลวไปครึ่งทาง? มันควรจะทำงานอย่างไร

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


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

@ โจนาห์: ถ้าคุณจะยืนยันในการ จำกัด การทดสอบหน่วยของคุณเพียงเพื่อforeachสร้างและไม่เงื่อนไขใด ๆ ผลข้างเคียงหรือพฤติกรรมนอกมันคุณก็ถูก; การทดสอบหน่วยใหม่นั้นไม่ใช่สิ่งที่น่าสนใจจริงๆ
Robert Harvey

1
@ jonah - ควรวนซ้ำและบันทึกให้มากที่สุดหรือหยุดข้อผิดพลาดหรือไม่ การบันทึกเดี่ยวไม่สามารถตัดสินใจได้เนื่องจากไม่สามารถทราบได้ว่าจะใช้งานอย่างไร
Telastyn

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

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

3

กุญแจสำคัญในที่นี้คือมุมมองของคุณเกี่ยวกับฟังก์ชั่นบางอย่างที่ไม่สำคัญ การเขียนโปรแกรมส่วนใหญ่เป็นเรื่องไม่สำคัญ: กำหนดค่าทำคณิตศาสตร์ทำในการตัดสินใจ: ถ้าเป็นเช่นนั้นให้ทำต่อไปเรื่อย ๆ จนกว่าจะถึง ... คุณเพิ่งผ่าน 5 บทแรกของหนังสือใด ๆ ที่สอนภาษาการเขียนโปรแกรม

ความจริงที่ว่าการเขียนการทดสอบนั้นง่ายมากควรเป็นสัญญาณว่าการออกแบบของคุณนั้นไม่เลว คุณต้องการออกแบบที่ไม่ง่ายต่อการทดสอบหรือไม่?

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

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


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

1

ควร savePeople () ทำการทดสอบหน่วยหรือจำนวนการทดสอบดังกล่าวเพื่อทดสอบโครงสร้างภาษา forEach หรือไม่

@BryanOakley ได้รับคำตอบนี้แล้ว แต่ฉันมีข้อโต้แย้งเพิ่มเติม (ฉันเดา):

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

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

ประการที่สามมีคุณค่าในการใช้การทดสอบเล็กน้อยทั้งในวิธีการ TDD (ซึ่งสั่งมัน) และนอกมัน

เมื่อเขียน C ++ สำหรับชั้นเรียนของฉันฉันมักจะเขียนแบบทดสอบเล็กน้อยที่ยกตัวอย่างวัตถุและตรวจสอบค่าคงที่ (กำหนดได้ปกติและอื่น ๆ ) ฉันพบว่ามันน่าประหลาดใจที่การทดสอบนี้ขาดระหว่างการพัฒนา (เช่น - โดยการเพิ่มสมาชิกที่ไม่สามารถเคลื่อนที่ได้ในชั้นเรียนโดยไม่ได้ตั้งใจ)


1

ฉันคิดว่าคำถามของคุณเดือดลงไปที่:

ฉันจะทดสอบฟังก์ชั่นโมฆะโดยไม่ต้องทดสอบการรวมได้อย่างไร

หากเราเปลี่ยนฟังก์ชั่นการอบคุกกี้ของคุณเพื่อส่งคืนคุกกี้ตัวอย่างเช่นจะเห็นได้ชัดทันทีว่าการทดสอบควรเป็นอย่างไร

ถ้าเราต้องเรียกใช้ pan.GetCookies หลังจากเรียกฟังก์ชั่นแม้ว่าเราจะสามารถตั้งคำถามว่า 'การทดสอบการรวมระบบ' หรือ 'แต่เราไม่ได้ทดสอบวัตถุแพนหรือไม่'

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

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

--- อัพเดตสำหรับตัวอย่าง createNewUser

  • จำเป็นต้องสร้างเรคคอร์ดผู้ใช้ใหม่ในฐานข้อมูล
  • จะต้องส่งอีเมลต้อนรับ
  • ต้องบันทึกที่อยู่ IP ของผู้ใช้เพื่อวัตถุประสงค์ในการฉ้อโกง

ตกลงดังนั้นในเวลานี้ผลลัพธ์ของฟังก์ชันจะไม่ถูกส่งคืนได้อย่างง่ายดาย เราต้องการเปลี่ยนสถานะของพารามิเตอร์

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

ได้โปรดผู้อ่านที่รักลองและควบคุมความโกรธของคุณ!

ดังนั้น...

var validatedUserData = new UserData(); //we can use the real object for this
var emailService = new MockEmailService(); //a simple mock which saves sentEmails to a List<string>
var dataStore = new MockDataStore(); //a simple mock which saves ips to a List<string>

//run the test
target.createNewUser(validatedUserData, emailService, dataStore);

//check the results
Assert.AreEqual(1, emailService.EmailsSent.Count());
Assert.AreEqual(1, dataStore.IpsRecorded.Count());
Assert.AreEqual(1, dataStore.UsersSaved.Count());

วิธีนี้จะแยกรายละเอียดการใช้งานของวิธีการทดสอบออกจากพฤติกรรมที่ต้องการ การใช้งานทางเลือก:

function createNewUser(validatedUserData, emailService, dataStore) {
  userId = dataStore.bulkInsedrtUserRecords(new [] {validateduserData});
  emailService.addEmailToQueue(validatedUserData);
  emailService.ProcessQueue();
  dataStore.recordIpAddress(userId, validatedUserData.ip);
}

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


ไม่ใช่การทดสอบการรวมเพียงเพราะคุณพูดถึงชื่อของคลาสคอนกรีต 2 ชั้น ... การทดสอบการรวมเป็นการทดสอบการรวมกับระบบภายนอกเช่นดิสก์ IO, DB, บริการเว็บภายนอกและอื่น ๆ ในการเรียก pan.getCookies () - หน่วยความจำรวดเร็วตรวจสอบสิ่งที่เราสนใจ ฯลฯ ฉันยอมรับว่าการใช้วิธีการคืนคุกกี้จะให้ความรู้สึกโดยตรงกับการออกแบบที่ดีกว่า
sara

3
รอ. สำหรับทุกอย่างที่เรารู้ pan.getcookies ส่งอีเมลไปยังพ่อครัวที่ขอให้พวกเขานำคุกกี้ออกจากเตาอบเมื่อพวกเขามีโอกาส
Ewan

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

2
มันเป็นคำถามเชิงโวหาร แต่: "ใครเคยได้ยินอุปกรณ์เตาอบที่ส่งอีเมล?" capitalbeat.com/2016/03/08/…
clacke

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

0

คุณควรทดสอบด้วยเช่นกันbakeCookies- จะbakeCookies(egg, pan, oven)ให้ผลตอบแทนอะไรบ้าง? ไข่ดาวหรือข้อยกเว้น? ของตัวเองค่าpanมิได้ovenจะดูแลเกี่ยวกับส่วนผสมที่เกิดขึ้นจริงเนื่องจากไม่มีของพวกเขาควรจะ แต่bakeCookiesมักจะควรผลผลิตคุกกี้ โดยทั่วไปก็สามารถขึ้นอยู่กับวิธีการที่doughจะได้รับและไม่ว่าจะมีเป็นโอกาสใด ๆ ของมันกลายเป็นเพียงeggหรือเช่นwaterแทน

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