การทดสอบหน่วยที่ควรคาดว่าผลลัพธ์จะ hardcoded?


29

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

ตัวอย่างเช่นรูปแบบใดในสองรูปแบบที่เชื่อถือได้มากขึ้น

[TestMethod]
public void GetPath_Hardcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

[TestMethod]
public void GetPath_Softcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

แก้ไข 1:เพื่อตอบสนองต่อคำตอบของ DXM ตัวเลือก 3 เป็นโซลูชันที่ต้องการหรือไม่

[TestMethod]
public void GetPath_Option3()
{
    string field1 = "fields";
    string field2 = "that later";
    string field3 = "determine";
    string field4 = "a folder";
    MyClass target = new MyClass(field1, field2, field3, field4);
    string expected = "C:\\Output Folder\\" + string.Join("\\", field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

2
ทำทั้งสองอย่าง อย่างจริงจัง. การทดสอบสามารถและควรทับซ้อนกัน ลองดูการทดสอบที่ขับเคลื่อนด้วยข้อมูลบางประเภทหากคุณพบว่าตัวเองกำลังจัดการกับค่าตายตัว
งาน

ฉันยอมรับตัวเลือกที่สามคือสิ่งที่ฉันต้องการใช้ ฉันไม่คิดว่าตัวเลือกที่ 1 จะเจ็บเพราะคุณกำจัดการจัดการในการรวบรวม
kwelch

ตัวเลือกทั้งสองของคุณใช้การ
เข้ารหัสแบบแข็ง

คำตอบ:


27

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

ต้องบอกว่าในตัวอย่างเฉพาะของคุณฉันจะไม่เชื่อถือวิธี "Softcoded" เพราะใช้ SUT ของคุณ (ระบบภายใต้การทดสอบ) เป็นข้อมูลป้อนเข้าสำหรับการคำนวณของคุณ หากมีข้อผิดพลาดใน MyClass ที่ไม่ได้จัดเก็บฟิลด์อย่างถูกต้องการทดสอบของคุณจะผ่านเพราะการคำนวณค่าที่คาดหวังของคุณจะใช้สตริงที่ไม่ถูกต้องเช่นเดียวกับ target.GetPath ()

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

ในการตอบสนองการอัปเดต OP ของการตอบสนองของฉัน:

ใช่ขึ้นอยู่กับความรู้ของฉัน แต่ประสบการณ์ที่ จำกัด ในการทำ TDD ฉันจะเลือกตัวเลือก # 3


1
จุดดี! อย่าพึ่งพาวัตถุที่ไม่ผ่านการตรวจสอบในการทดสอบ
Hand-E-Food

มันไม่ซ้ำกับรหัส SUT เหรอ?
Abyx

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

16

เกิดอะไรขึ้นถ้ารหัสเป็นดังนี้:

MyTarget() // constructor
{
   Field1 = Field2 = Field3 = Field4 = "";
}

ตัวอย่างที่สองของคุณจะไม่ดักจับข้อบกพร่อง แต่ตัวอย่างแรกจะ

โดยทั่วไปฉันแนะนำให้ต่อต้านการเข้ารหัสแบบนิ่มเพราะอาจซ่อนข้อบกพร่องได้ ตัวอย่างเช่น:

string expected = "C:\\Output Folder" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);

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

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


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

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

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

@ Hand-E-Food ถ้าฉันเข้าใจคุณถูกต้องการทดสอบ ConstructShouldCorrectlyInitialiseFields จะเรียกใช้ Constructor แล้วยืนยันว่าฟิลด์นั้นตั้งค่าไว้ถูกต้อง แต่คุณไม่ควรทำอย่างนั้น คุณไม่ควรสนใจว่าฟิลด์ภายในกำลังทำอะไร คุณควรยืนยันว่าพฤติกรรมภายนอกของวัตถุนั้นถูกต้อง มิฉะนั้นวันนั้นอาจมาถึงเมื่อคุณจำเป็นต้องเปลี่ยนการใช้งานภายใน หากคุณยืนยันเกี่ยวกับสถานะภายในการทดสอบทั้งหมดของคุณจะหยุด แต่ถ้าคุณยืนยันเกี่ยวกับพฤติกรรมภายนอกทุกอย่างจะยังคงทำงานอยู่
Winston Ewert

@ Winston - จริง ๆ แล้วฉันกำลังอยู่ในขั้นตอนของการไถผ่านหนังสือ xUnit Test Patterns และก่อนหน้านั้น The Art of Unit Testing เสร็จแล้ว ฉันจะไม่แกล้งฉันรู้ว่าสิ่งที่ฉันพูดถึง แต่ฉันอยากจะคิดว่าฉันหยิบอะไรจากหนังสือเหล่านั้น หนังสือทั้งสองเล่มแนะนำเป็นอย่างยิ่งว่าวิธีทดสอบแต่ละวิธีควรทดสอบขั้นต่ำที่แน่นอนและคุณควรมีกรณีทดสอบมากมายเพื่อทดสอบวัตถุทั้งหมดของคุณ ด้วยวิธีนี้เมื่ออินเทอร์เฟซหรือฟังก์ชันการทำงานเปลี่ยนไปคุณควรคาดหวังว่าจะแก้ไขวิธีทดสอบเพียงเล็กน้อยเท่านั้น และเนื่องจากมีขนาดเล็กการเปลี่ยนแปลงจึงควรง่ายขึ้น
DXM

4

ในความคิดของฉันคำแนะนำของคุณทั้งสองนั้นน้อยกว่าอุดมคติ วิธีที่เหมาะที่จะทำคือ:

[TestMethod]
public void GetPath_Hardcoded()
{
    const string f1 = "fields"; const string f2 = "that later"; 
    const string f3 = "determine"; const string f4 = "a folder";

    MyClass target = new MyClass( f1, f2, f3, f4 );
    string expected = "C:\\Output Folder\\" + string.Join("\\", f1, f2, f3, f4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

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


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

จากนั้นสถานะนั้นจะถูกพิจารณาว่าเป็นผลลัพธ์ของวิธีการ จุดประสงค์ของการทดสอบตัวอย่างนี้คือการตรวจสอบเมธอด GetPath () ไม่ใช่ตัวสร้างของ MyClass อ่านคำตอบของ @ DXM เขาให้เหตุผลที่ดีมากสำหรับแนวทางกล่องดำ
Mike Nakis

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

@MatthewFlynn เพียงเพื่อชี้แจงว่าเกี่ยวข้องกับตัวอย่างที่แสดงหรืออย่างอื่นที่ต้องพิจารณาสำหรับการทดสอบหน่วยอื่น ๆ ? ฉันสามารถเห็นสิ่งที่มีความสำคัญเช่นtarget.Dispose(); Assert.IsTrue(target.IsDisposed);(ตัวอย่างง่าย ๆ )
Hand-E-Food

แม้ในกรณีนี้คุณสมบัติ IsDisposed คือ (หรือควร) ส่วนที่ขาดไม่ได้ของส่วนต่อประสานสาธารณะของชั้นเรียนและไม่ใช่รายละเอียดการนำไปปฏิบัติ (อินเทอร์เฟซ IDispose ไม่มีคุณสมบัติดังกล่าว แต่โชคร้าย)
Mike Nakis

2

การสนทนามีสองด้าน:

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

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

เช่นนี่คือวิธีที่ฉันจะทดสอบต้นขั้ว

[TestMethod]
public void GetPath_Tested(int CaseId)
{
    testParams = GetTestConfig(caseID,"testConfig.txt"); // some wrapper that does read line and chops the field. 
    MyClass target = new MyClass(testParams.field1, testParams.field2);
    string expected = testParams.field5;
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

0

มีแนวคิดจำนวนมากที่เป็นไปได้ทำตัวอย่างเพื่อดูความแตกต่าง

[TestMethod]
public void GetPath_Softcoded()
{
    //Hardcoded since you want to see what you expect is most simple and clear
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";

    //If this test should also use a mocked filesystem it might be that you want to use
    //some base directory, which you could set in the setUp of your test class
    //that is usefull if you you need to run the same test on different environments
    string expected = this.outputPath + "fields\\that later\\determine\\a folder";


    //another readable way could be interesting if you have difficult variables needed to test
    string fields = "fields";
    string thatLater = "that later";
    string determine = "determine";
    string aFolder = "a folder";
    string expected = this.outputPath + fields + "\\" + thatLater + "\\" + determine + "\\" + aFolder;
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    //in general testing with real words is not needed, so code could be shorter on that
    //for testing difficult folder names you write a separate test anyway
    string f1 = "f1";
    string f2 = "f2";
    string f3 = "f3";
    string f4 = "f4";
    string expected = this.outputPath + f1 + "\\" + f2 + "\\" + f3 + "\\" + f4;
    MyClass target = new MyClass(f1, f2, f3, f4);

    //so here we start to see a structure, it looks more like an array of fields
    //so what would make testing more interesting with lots of variables is the use of a data provider
    //the data provider will re-use your test with many different kinds of inputs. That will reduce the amount of duplication of code for testing
    //http://msdn.microsoft.com/en-us/library/ms182527.aspx


    The part where you compare already seems correct
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

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

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


0

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

[TestCase("fields", "that later", "determine", "a folder", @"C:\Output Folder\fields\that later\determine\a folder")]
public void GetPathShouldReturnFullDirectoryPathBasedOnItsFields(
    string field1, string field2, string field3, string field,
    string expected)
{
    MyClass target = new MyClass(field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

มีข้อดีหลายประการในเรื่องนี้ในมุมมองของฉัน:

  1. นักพัฒนามักถูกล่อลวงให้คัดลอกส่วนของรหัสที่ดูเหมือนง่าย ๆ จาก SUT ไปยังการทดสอบหน่วย ในขณะที่วินสตันชี้ให้เห็นพวกเขายังสามารถมีข้อบกพร่องที่ซ่อนอยู่ในพวกเขา "การเข้ารหัสแบบยาก" ผลลัพธ์ที่คาดไว้ช่วยในการหลีกเลี่ยงสถานการณ์ที่รหัสทดสอบของคุณไม่ถูกต้องด้วยเหตุผลเดียวกับที่รหัสดั้งเดิมของคุณไม่ถูกต้อง แต่หากการเปลี่ยนแปลงข้อกำหนดบังคับให้คุณติดตามสตริงที่เข้ารหัสยากซึ่งฝังอยู่ในวิธีการทดสอบหลายสิบวิธีอาจเป็นเรื่องที่น่ารำคาญ การมีค่าฮาร์ดโค้ดทั้งหมดในที่เดียวนอกตรรกะการทดสอบของคุณจะมอบสิ่งที่ดีที่สุดให้กับคุณทั้งสองโลก
  2. คุณสามารถเพิ่มการทดสอบสำหรับอินพุตที่แตกต่างกันและเอาท์พุทที่คาดหวังด้วยรหัสบรรทัดเดียว สิ่งนี้กระตุ้นให้คุณเขียนการทดสอบเพิ่มเติมในขณะที่รักษารหัสการทดสอบ DRY และบำรุงรักษาง่าย ฉันพบว่าเนื่องจากราคาถูกกว่ามากเมื่อเพิ่มการทดสอบใจของฉันเปิดรับกรณีทดสอบใหม่ที่ฉันไม่คิดถ้าฉันต้องเขียนวิธีการใหม่ทั้งหมดสำหรับพวกเขา ตัวอย่างเช่นฉันจะคาดหวังว่าพฤติกรรมใดถ้าหนึ่งในอินพุตมีจุดอยู่ในนั้น แบ็กสแลช เกิดอะไรขึ้นถ้าใครว่างเปล่า หรือช่องว่าง? หรือเริ่มหรือจบลงด้วยช่องว่าง
  3. กรอบการทดสอบจะถือว่าแต่ละ TestCase เป็นการทดสอบของตัวเองแม้จะใส่อินพุตและเอาต์พุตที่ให้ไว้ในชื่อการทดสอบ ถ้า TestCases ทั้งหมดผ่านการทดสอบไปแล้วมันจะง่ายมากที่จะเห็นว่าอันไหนแตกได้และมันแตกต่างจาก TestCases อื่น ๆ อย่างไร
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.