ความยากลำบากในการใช้ TDD และการสร้างใหม่ (หรือ - ทำไมจึงเจ็บปวดกว่าที่ควรจะเป็น)


20

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

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

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

  1. ผู้ใช้คลิกสร้างโครงการ
  2. ผู้ใช้พิมพ์ชื่อเรื่องของโครงการ
  3. ตรวจสอบว่าโครงการสร้างขึ้นอย่างถูกต้อง

ข้ามสิ่งที่ UI และการวางแผนตัวกลางฉันมาทดสอบหน่วยแรกของฉัน:

[TestMethod]
public void CreateProject_BasicParameters_ProjectIsValid()
{
    var testController = new Controller();
    Project newProject = testController(A.Dummy<String>());
    Assert.IsNotNull(newProject);
}

จนถึงตอนนี้ดีมาก สีแดงเขียว refactor ฯลฯ เอาล่ะตอนนี้มันต้องการบันทึกของจริง ตัดขั้นตอนที่นี่ฉันปิดท้ายด้วยสิ่งนี้

[TestMethod]
public void CreateProject_BasicParameters_ProjectMatchesExpected()
{
    var fakeDataStore = A.Fake<IDataStore>();
    var testController = new Controller(fakeDataStore);
    String expectedTitle = fixture.Create<String>("Title");
    Project newProject = testController(expectedTitle);

    Assert.AreEqual(expectedTitle, newProject.Title);
}

ฉันยังรู้สึกดีในตอนนี้ ฉันยังไม่มีแหล่งข้อมูลที่เป็นรูปธรรม แต่ฉันสร้างอินเทอร์เฟซตามที่ฉันคาดไว้

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

[TestMethod]
public void SaveNewProject_BasicParameters_RequestsNewPage()
{
    /* snip init code */
    testDataStore.SaveNewProject(A.Dummy<IProject>());
    A.CallTo(() => oneNoteInterop.SavePage()).MustHaveHappened();
}

สิ่งนี้ดีจนกระทั่งฉันพยายามใช้:

public String SaveNewProject(IProject project)
{
    Page projectPage = oneNoteInterop.CreatePage(...);
}

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

มีใครช่วยอธิบายให้ฉันหน่อยได้ไหมถ้าฉันทำอะไรผิดในขั้นตอนนี้? อย่างไรก็ตามมีการหลีกเลี่ยงการ refactoring ประเภทนี้หรือไม่? หรือเป็นเรื่องปกติ หากเป็นเรื่องปกติจะมีวิธีใดที่ทำให้เจ็บปวดมากขึ้นหรือไม่?

ขอบคุณทุกคน!


คุณจะได้รับความคิดเห็นที่ลึกซึ้งหากคุณโพสต์หัวข้อนี้ที่ฟอรัมการสนทนานี้: groups.google.com/forum/#!forum/#ซึ่งเป็นหัวข้อสำหรับ TDD โดยเฉพาะ
Chuck Krutsinger

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

คำตอบ:


19

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

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

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

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


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

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

@Landon I´ma "เชื่อมั่นคลาสสิค" ดังนั้นฉันไม่ได้มีประสบการณ์มากกับการทดสอบตาม interation ... แต่คุณไม่จำเป็นต้องยืนยันสำหรับ "MustHaveBeenCalled" หากคุณกำลังทดสอบการแทรกคุณสามารถใช้แบบสอบถามเพื่อดูว่าแทรกหรือไม่ PS: ฉันใช้สตับเนื่องจากข้อควรพิจารณาด้านประสิทธิภาพเมื่อทดสอบทุกอย่างยกเว้นเลเยอร์ฐานข้อมูล
Hbas

@ jhewlett นั่นคือข้อสรุปที่ฉันได้รับเช่นกัน ขอบคุณ!
แลนดอน

@Hbas ไม่มีฐานข้อมูลที่จะสืบค้น ฉันยอมรับว่าจะเป็นวิธีที่ตรงไปตรงมาที่สุดถ้าฉันมี แต่ฉันเพิ่มลงในสมุดบันทึก OneNote วิธีที่ดีที่สุดที่ฉันทำได้คือการเพิ่มเมธอด Get ให้กับคลาสตัวช่วย interop ของฉันเพื่อพยายามดึงหน้า ฉันสามารถเขียนแบบทดสอบเพื่อทำสิ่งนั้นได้ แต่ฉันรู้สึกว่าฉันจะทำการทดสอบสองสิ่งพร้อมกัน: ฉันบันทึกสิ่งนี้หรือไม่ และชั้นผู้ช่วยของฉันดึงหน้าถูกต้องหรือไม่ แม้ว่าฉันเดาว่าในบางจุดการทดสอบของคุณอาจต้องใช้รหัสอื่นที่ผ่านการทดสอบในที่อื่น ขอบคุณ!
แลนดอน

10

... ฉันอดไม่ได้ที่จะรู้สึกว่าฉันจะจับเร็วกว่านี้ถ้าฉันร่างการออกแบบล่วงหน้าแทนที่จะปล่อยให้มันถูกออกแบบในระหว่างกระบวนการ TDD ...

อาจจะอาจจะไม่

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

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

จากนั้นอีกครั้งบางทีคุณอาจจะไม่มี เป็นการทดลองที่ไม่สามารถทำซ้ำได้

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


0

https://frontendmasters.com/courses/angularjs-and-code-testability/ จากประมาณ 2:22:00 ถึงสิ้นสุด (ประมาณ 1 ชั่วโมง) ขออภัยวิดีโอไม่ฟรี แต่ฉันไม่พบวิดีโอที่อธิบายได้ดี

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

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

นอกจากนี้เขายังใช้เวลาเขียนสเป็คในรูปแบบของการยืนยันการทดสอบ

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