ฉันควรทดสอบวิธีที่สืบทอดมาหรือไม่


30

สมมติว่าฉันได้เรียนผู้จัดการมาจากชั้นฐานของพนักงานและลูกจ้างมีวิธีgetEmail ()ที่สืบเชื้อสายมาจากผู้จัดการ ฉันควรทดสอบว่าพฤติกรรมของเมธอด getEmail ()ของผู้จัดการจริงๆแล้วเหมือนกับพนักงานหรือไม่?

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

(โปรดทราบว่าวิธีการทดสอบManager :: getEmail ()ไม่ได้ปรับปรุงการครอบคลุมโค้ด (หรือแน่นอนการวัดคุณภาพโค้ดอื่น ๆ (?)) จนกระทั่งManager :: getEmail ()ถูกสร้าง / เขียนทับ)

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

การกำหนดคำถามที่เทียบเท่า:

หากคลาสที่ได้รับสืบทอดวิธีการจากคลาสพื้นฐานคุณจะแสดง (ทดสอบ) อย่างไรว่าคุณคาดหวังให้วิธีการสืบทอดมา:

  1. ประพฤติในแบบเดียวกับที่ฐานทำในตอนนี้ (ถ้าพฤติกรรมของการเปลี่ยนแปลงฐานพฤติกรรมของวิธีการที่ได้รับไม่ได้)
  2. ประพฤติตนในลักษณะเดียวกับฐานตลอดเวลา (หากพฤติกรรมของคลาสฐานเปลี่ยนไปพฤติกรรมของคลาสที่ได้รับก็เปลี่ยนไปเช่นกัน) หรือ
  3. ประพฤติ แต่มันต้องการ (คุณไม่สนใจเกี่ยวกับพฤติกรรมของวิธีนี้เพราะคุณไม่เคยเรียกมันว่า)

1
IMO ที่ได้รับManagerคลาสEmployeeนั้นเป็นความผิดพลาดครั้งแรกที่สำคัญ
CodesInChaos

4
@CodesInChaos ใช่ว่าอาจเป็นตัวอย่างที่ไม่ดี แต่ปัญหาเดียวกันจะเกิดขึ้นทุกครั้งที่คุณมีมรดก
mjs

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

คำตอบ:


23

ฉันจะใช้วิธีการปฏิบัติที่นี่: หากใครบางคนในอนาคตแทนที่ผู้จัดการ :: getMail แล้วมันเป็นความรับผิดชอบของนักพัฒนาที่จะให้รหัสทดสอบสำหรับวิธีการใหม่

แน่นอนว่าจะใช้ได้ก็ต่อเมื่อManager::getEmailมีเส้นทางรหัสเดียวกับEmployee::getEmail! แม้ว่าวิธีนี้จะไม่ถูกแทนที่ แต่มันก็อาจทำงานแตกต่างกัน:

  • Employee::getEmailสามารถโทรบางส่วนได้รับการคุ้มครองเสมือนgetInternalEmailซึ่งจะถูกManagerแทนที่ใน
  • Employee::getEmailสามารถเข้าถึงสถานะภายในบางอย่าง (เช่นบางฟิลด์_email) ซึ่งอาจแตกต่างกันในการนำไปใช้งานสองแบบ: ตัวอย่างเช่นการใช้งานเริ่มต้นของEmployeeสามารถทำให้มั่นใจได้ว่า_emailเป็นเสมอfirstname.lastname@example.comแต่Managerมีความยืดหยุ่นในการกำหนดที่อยู่อีเมล

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


14

ฉันจะ

หากคุณคิดว่า "ดีมันแค่โทรหาพนักงาน :: getEmail () เพราะฉันไม่ได้แทนที่มันดังนั้นฉันไม่จำเป็นต้องทดสอบ Manager :: getEmail ()" ดังนั้นคุณไม่ได้ทดสอบพฤติกรรมจริงๆ ของ Manager :: getEmail ()

ฉันคิดว่า Manager :: getEmail () ควรทำอย่างไรไม่ว่าจะสืบทอดมาหรือถูกแทนที่ หากพฤติกรรมของ Manager :: getEmail () ควรจะคืนค่า Employee ใดก็ตาม :: getMail () กลับมาแสดงว่านั่นคือการทดสอบ หากพฤติกรรมคือการส่งคืน "pink@unicorns.com" นั่นคือการทดสอบ มันไม่สำคัญว่าจะดำเนินการโดยการสืบทอดหรือแทนที่

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

บางคนอาจไม่เห็นด้วยกับความซ้ำซ้อนในการตรวจสอบ แต่ตัวนับของฉันที่คุณกำลังทดสอบพฤติกรรม Employee :: getMail () และ Manager :: getMail () เป็นวิธีการที่แตกต่างกันไม่ว่าพวกเขาจะได้รับมรดกหรือไม่ก็ตาม แทนที่ หากนักพัฒนาในอนาคตต้องเปลี่ยนพฤติกรรมของ Manager :: getMail () พวกเขาจำเป็นต้องอัพเดตการทดสอบด้วย

ความคิดเห็นอาจแตกต่างกันไปฉันคิดว่า Digger และ Heinzi ให้เหตุผลที่สมเหตุสมผลสำหรับสิ่งที่ตรงกันข้าม


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

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

2
คุณต้องการทดสอบคลาสผู้จัดการจริงๆ ความจริงที่ว่ามันเป็น subclass ของ Employee และ getMail () นั้นถูกนำไปใช้โดยอาศัยการสืบทอดเป็นเพียงรายละเอียดการนำไปปฏิบัติที่คุณควรละเว้นเมื่อสร้างการทดสอบหน่วย เดือนถัดไปคุณจะพบว่าการสืบทอดผู้จัดการจากพนักงานนั้นเป็นความคิดที่ไม่ดีและคุณได้แทนที่โครงสร้างการสืบทอดทั้งหมดและนำวิธีการทั้งหมดมาใช้ใหม่ การทดสอบหน่วยของคุณควรทดสอบโค้ดของคุณต่อไปโดยไม่มีปัญหา
gnasher729

5

อย่าทดสอบหน่วย ทำหน้าที่ทดสอบการยอมรับ

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

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


4

กฎของ Robert Martin สำหรับ TDDคือ:

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

หากคุณปฏิบัติตามกฎเหล่านี้คุณจะไม่สามารถเข้ามาถามคำถามนี้ได้ หากgetEmailวิธีการนั้นจะต้องมีการทดสอบก็จะได้รับการทดสอบ


3

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

วิธีที่ฉันทดสอบนี้คือการสร้างคลาสนามธรรมเพื่อทดสอบคลาสพื้นฐานหรืออินเทอร์เฟซ (อาจเป็นนามธรรม) เช่นนี้ (ใน C # โดยใช้ NUnit):

public abstract class EmployeeTests
{
    protected abstract Employee CreateInstance(string name, int age);

    [Test]
    public void GetEmail_ReturnsValidEmailAddress()
    {
        // Given
        var sut = CreateInstance("John Doe", 20);

        // When
        string email = sut.GetEmail();

        // Then
        Assert.IsTrue(Helper.IsValidEmail(email));
    }
}

จากนั้นฉันมีชั้นเรียนที่มีการทดสอบเฉพาะManagerและรวมการทดสอบของพนักงานเช่นนี้:

[TestFixture]
public class ManagerTests
{
    // Other tests.

    [TestFixture]
    public class ManagerEmployeeTests : EmployeeTests
    {
        protected override Employee CreateInstance(string name, int age);
        {
            return new Manager(name, age);
        }
    }
}

เหตุผลที่ฉันสามารถดังนั้นนี่คือหลักการทดแทน Liskov: การทดสอบของEmployeeยังควรจะผ่านเมื่อผ่านวัตถุเพราะมันมาจากManager Employeeดังนั้นฉันจึงต้องเขียนการทดสอบของฉันเพียงครั้งเดียวและสามารถตรวจสอบว่าพวกเขาทำงานสำหรับ implementstions ที่เป็นไปได้ทั้งหมดของอินเทอร์เฟซหรือคลาสฐาน


ข้อดีของหลักการแทน Liskov คุณพูดถูกถ้าชั้นนี้คลาสที่ได้รับจะผ่านการทดสอบคลาสพื้นฐานทั้งหมด อย่างไรก็ตาม LSP นั้นถูกละเมิดอยู่บ่อยครั้งรวมถึงsetUp()วิธีของ xUnit เอง! และเฟรมเวิร์ค MVC ของเว็บที่เกี่ยวข้องกับการแทนที่เมธอด "index" นั้นก็ทำลาย LSP ซึ่งก็คือทั้งหมด
mjs

นี่เป็นคำตอบที่ถูกต้องอย่างแน่นอน - ฉันได้ยินว่ามันเรียกว่า "รูปแบบทดสอบนามธรรม" จากความอยากรู้ว่าทำไมใช้ชั้นซ้อนกันมากกว่าแค่ผสมในการทดสอบผ่านclass ManagerTests : ExployeeTests? (เช่นการทำมิเรอร์การสืบทอดของคลาสที่อยู่ภายใต้การทดสอบ) มันเป็นความสวยงามเป็นหลักเพื่อช่วยในการดูผลลัพธ์หรือไม่?
ลุค Usherwood

2

ฉันควรทดสอบว่าพฤติกรรมของเมธอด getEmail () ของผู้จัดการจริงๆแล้วเหมือนกับพนักงานหรือไม่?

ฉันจะบอกว่าไม่ได้เพราะมันเป็นการทดสอบซ้ำในความคิดของฉันฉันจะทดสอบหนึ่งครั้งในการทดสอบพนักงานและนั่นจะเป็น

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

หากวิธีการถูกแทนที่แล้วคุณจะต้องมีการทดสอบใหม่เพื่อตรวจสอบพฤติกรรมการแทนที่ นั่นคืองานของบุคคลที่ใช้getEmail()วิธีการ เอาชนะ


1

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

นึกถึงสถานการณ์ต่อไปนี้: ที่อยู่อีเมลถูกรวมเป็น Firstname.Lastname@example.com:

class Employee{
    String firstname, lastname;
    String getEmail() { 
        return firstname + "." + lastname + "@example.com";
    }
}

Employeeคุณมีหน่วยทดสอบนี้และจะทำงานที่ดีสำหรับคุณ คุณได้สร้างชั้นเรียนด้วยManager:

class Manager extends Employee { /* nothing different in email generation */ }

ตอนนี้คุณมีคลาสManagerSortที่เรียงลำดับผู้จัดการในรายการตามที่อยู่อีเมลของพวกเขา คุณคิดว่าการสร้างอีเมลนั้นเหมือนกับในEmployee:

class ManagerSort {
    void sortManagers(Manager[] managerArrayToBeSorted)
        // sort based on email address omitted
    }
}

คุณเขียนทดสอบเพื่อManagerSort:

void testManagerSort() {
    Manager[] managers = ... // build random manager list
    ManagerSort.sortManagers(managers);

    Manager[] expected = ... // expected result
    assertEquals(expected, managers); // check the result
}

ทุกอย่างทำงานได้ดี ตอนนี้มีคนมาและแทนที่getEmail()วิธี:

class Manager extends Employee {
    String getEmail(){
        // managers should have their lastname and firstname order changed
        return lastname + "." + firstname + "@example.com";
    }
}

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

ดังนั้นคุณไม่จำเป็นต้องทดสอบวิธีการที่สืบทอดมา

มิฉะนั้นเช่นใน Java คุณจะต้องทดสอบวิธีการทั้งหมดที่สืบทอดจากObjectlike toString()และequals()อื่น ๆ ในทุก ๆ คลาส


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

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