คุณจัดโครงสร้างหน่วยการทดสอบสำหรับวัตถุหลายรายการที่มีพฤติกรรมเดียวกันได้อย่างไร


9

ในหลายกรณีฉันอาจมีคลาสที่มีอยู่แล้วซึ่งมีพฤติกรรมบางอย่าง:

class Lion
{
    public void Eat(Herbivore herbivore) { ... }
}

... และฉันมีการทดสอบหน่วย ...

[TestMethod]
public void Lion_can_eat_herbivore()
{
    var herbivore = buildHerbivoreForEating();
    var test = BuildLionForTest();
    test.Eat(herbivore);
    Assert.IsEaten(herbivore);
}

ตอนนี้สิ่งที่เกิดขึ้นคือฉันต้องสร้างคลาส Tiger ที่มีพฤติกรรมเฉพาะตัวของ Lion:

class Tiger
{
    public void Eat(Herbivore herbivore) { ... }
}

... และเนื่องจากฉันต้องการพฤติกรรมเดียวกันฉันจึงต้องทำการทดสอบเดียวกันฉันจึงทำสิ่งนี้:

interface IHerbivoreEater
{
    void Eat(Herbivore herbivore);
}

... และฉันได้ทำการทดสอบอีกครั้ง:

[TestMethod]
public void Lion_can_eat_herbivore()
{
    IHerbivoreEater_can_eat_herbivore(BuildLionForTest);
}


public void IHerbivoreEater_can_eat_herbivore(Func<IHerbivoreEater> builder)
{
    var herbivore = buildHerbivoreForEating();
    var test = builder();
    test.Eat(herbivore);
    Assert.IsEaten(herbivore);
}

... และจากนั้นฉันเพิ่มการทดสอบอื่นสำหรับTigerคลาสใหม่ของฉัน:

[TestMethod]
public void Tiger_can_eat_herbivore()
{
    IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}

... และจากนั้นฉัน refactor ของฉันLionและTigerชั้นเรียน (โดยปกติจะเป็นมรดก แต่บางครั้งก็เรียงตาม):

class Lion : HerbivoreEater { }
class Tiger : HerbivoreEater { }

abstract class HerbivoreEater : IHerbivoreEater
{
    public void Eat(Herbivore herbivore) { ... }
}

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

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

คนอื่นทำอะไรในสถานการณ์นี้ คุณเพิ่งย้ายการทดสอบของคุณไปที่คลาสฐานหรือคุณทดสอบคลาสย่อยทั้งหมดสำหรับลักษณะการทำงานที่คาดไว้

แก้ไข :

จากคำตอบของ @pdr ฉันคิดว่าเราควรพิจารณาเรื่องนี้: นี่IHerbivoreEaterเป็นเพียงสัญญาวิธีการลงนาม มันไม่ได้ระบุพฤติกรรม ตัวอย่างเช่น

[TestMethod]
public void Tiger_eats_herbivore_haunches_first()
{
    IHerbivoreEater_eats_herbivore_haunches_first(BuildTigerForTest);
}

[TestMethod]
public void Cheetah_eats_herbivore_haunches_first()
{
    IHerbivoreEater_eats_herbivore_haunches_first(BuildCheetahForTest);
}

[TestMethod]
public void Lion_eats_herbivore_head_first()
{
    IHerbivoreEater_eats_herbivore_head_first(BuildLionForTest);
}

เพื่อเหตุผลของการโต้แย้งคุณไม่ควรมีAnimalชั้นเรียนที่มีEat? สัตว์ทุกตัวกินและดังนั้นTigerและLionชั้นสามารถสืบทอดจากสัตว์
มัฟฟินแมน

1
@ นิค - นั่นเป็นประเด็นที่ดี แต่ฉันคิดว่านั่นเป็นสถานการณ์ที่แตกต่าง ดังที่ @pdr ชี้ให้เห็นหากคุณใส่Eatลักษณะการทำงานในคลาสพื้นฐานดังนั้นคลาสย่อยทั้งหมดควรแสดงEatพฤติกรรมเดียวกัน อย่างไรก็ตามฉันกำลังพูดถึง 2 คลาสที่ค่อนข้างไม่เกี่ยวข้องซึ่งเกิดขึ้นเพื่อแบ่งปันพฤติกรรม พิจารณาเช่นFlyพฤติกรรมของBrickและPersonที่เราอาจถือว่าแสดงพฤติกรรมการบินที่คล้ายกัน แต่มันไม่จำเป็นต้องทำให้ความรู้สึกที่มีให้พวกเขามาจากระดับฐานร่วมกัน
Scott Whitlock

คำตอบ:


6

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

มีสองวิธีในการดูที่นี้

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

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

หรือทำไปกับ Lion และ Tiger อย่างสมบูรณ์

แต่ถ้า Lion และ Tiger นั้นแตกต่างกันในทุก ๆ ความรู้สึกนอกเหนือจากนิสัยการกินของพวกเขาคุณต้องเริ่มสงสัยว่าคุณกำลังประสบปัญหาเกี่ยวกับต้นไม้มรดกที่ซับซ้อนหรือไม่ จะเป็นอย่างไรถ้าคุณต้องการรับทั้งสองคลาสจาก Feline หรือเฉพาะคลาส Lion จาก KingOfItsDomain (พร้อมกับ Shark อาจจะ) นี่คือที่มาของ LSP อย่างแท้จริง

ฉันอยากจะแนะนำว่ารหัสทั่วไปนั้นดีกว่า

public class Lion : IHerbivoreEater
{
    private IHerbivoreEatingStrategy _herbivoreEatingStrategy;
    private Lion (IHerbivoreEatingStrategy herbivoreEatingStrategy)
    {
        _herbivoreEatingStrategy = herbivoreEatingStrategy;
    }

    public Lion() : this(new StandardHerbivoreEatingStrategy())
    {
    }

    public void Eat(Herbivore herbivore)
    {
        _herbivoreEatingStrategy.Eat(herbivore);
    }
}

ไปเหมือนกันสำหรับ Tiger

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

และการทดสอบที่ซับซ้อนของคุณสิ่งที่คุณกังวลในตอนแรกเท่านั้นที่จะต้องทดสอบ StandardHerbivoreEatingStrategy หนึ่งคลาส, ชุดทดสอบหนึ่งชุด, ไม่มีโค้ดที่ต้องกังวลซ้ำ

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


+1 รูปแบบกลยุทธ์เป็นสิ่งแรกที่เข้ามาในหัวของฉันในการอ่านคำถาม
StuperUser

ดีมาก แต่ตอนนี้แทนที่ "การทดสอบหน่วย" ในคำถามของฉันด้วย "การทดสอบการรวม" เราไม่ได้จบลงด้วยปัญหาเดียวกันหรือ IHerbivoreEaterเป็นสัญญา แต่เฉพาะในส่วนที่ฉันต้องการสำหรับการทดสอบ ฉันคิดว่านี่เป็นกรณีหนึ่งที่การพิมพ์เป็ดช่วยได้จริงๆ ฉันแค่ต้องการส่งพวกเขาทั้งสองไปยังตรรกะการทดสอบเดียวกันตอนนี้ ฉันไม่คิดว่าอินเทอร์เฟซควรให้สัญญากับพฤติกรรม การทดสอบควรทำเช่นนั้น
Scott Whitlock

คำถามที่ดีคำตอบที่ดี คุณไม่จำเป็นต้องมีคอนสตรัคเตอร์ส่วนตัว คุณสามารถวางสาย IHerbivoreEatingStrategy ไปที่ StandardHerbivoreEatingStrategy โดยใช้คอนเทนเนอร์ IoC
azheglov

@ScottWhitlock: "ฉันไม่คิดว่าส่วนต่อประสานควรจะให้คำมั่นสัญญาเกี่ยวกับพฤติกรรมการทดสอบควรทำเช่นนั้น" นั่นคือสิ่งที่ฉันพูด หากมันให้สัญญากับพฤติกรรมคุณควรกำจัดมันและใช้คลาส (ฐาน -) คุณไม่จำเป็นต้องทำการทดสอบเลย
pdr

@azheglov: เห็นด้วย แต่คำตอบของฉันเป็นเวลานานพอสมควรแล้ว :)
สาธารณรัฐประชาธิปไตยประชาชนลาว

1

ดูเหมือนว่าซ้ำซ้อนในการทดสอบรหัสเดียวกันหลายครั้ง แต่มีบางกรณีที่คลาสย่อยสามารถและแทนที่การทำงานของคลาสพื้นฐาน

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

ส่วนหนึ่งของเหตุผลสำหรับการพัฒนาทดสอบหน่วยคือการอนุญาตให้ตัวเอง refactor ภายหลัง แต่รักษาอินเตอร์เฟซที่กล่องดำเดียวกัน การทดสอบหน่วยกำลังช่วยให้คุณมั่นใจได้ว่าชั้นเรียนของคุณยังคงปฏิบัติตามสัญญากับลูกค้าหรืออย่างน้อยที่สุดบังคับให้คุณตระหนักและคิดอย่างรอบคอบว่าสัญญาอาจเปลี่ยนแปลงอย่างไร คุณเองตระหนักดีว่าLionหรือTigerอาจจะแทนที่Eatในภายหลัง หากเป็นไปได้จากระยะไกลให้ทดสอบหน่วยทดสอบง่ายๆว่าสัตว์แต่ละตัวที่คุณเลี้ยงสามารถกินได้ในลักษณะ:

[TestMethod]
public void Tiger_can_eat_herbivore()
{
    IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}

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


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

1

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

ในสถานการณ์นั้นคุณถูกต้องอย่างสมบูรณ์ ผู้ใช้งาน Lion หรือ Tiger จะไม่สนใจ (อย่างน้อยก็ไม่ต้อง) ดูแลว่าพวกเขาเป็นทั้ง HerbivoreEaters และรหัสที่ใช้งานได้จริงสำหรับวิธีการนั้นเป็นเรื่องธรรมดาสำหรับทั้งสองในคลาสพื้นฐาน ในทำนองเดียวกันผู้ใช้บทคัดย่อ HerbivoreEater (จัดทำโดย Lion หรือ Tiger ที่เป็นรูปธรรม) จะไม่สนใจสิ่งที่มี สิ่งที่พวกเขาสนใจคือการใช้ Lion, Tiger หรือ HerbivoreEater จะกิน () Herbivore อย่างถูกต้องอย่างเป็นรูปธรรม

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

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