วิธีที่ดีที่สุดในการทดสอบหน่วยวิธีการที่เรียกวิธีการอื่น ๆ ในชั้นเรียนเดียวกัน


35

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

นี่เป็นตัวอย่างที่ง่ายมาก ในความเป็นจริงฟังก์ชั่นมีความซับซ้อนมากขึ้น

ตัวอย่าง:

public class MyClass
{
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionB()
     {
         return new Random().Next();
     }
}

ดังนั้นเพื่อทดสอบสิ่งนี้เรามี 2 วิธี

วิธีที่ 1: ใช้ฟังก์ชั่นและการกระทำเพื่อแทนที่ฟังก์ชั่นของวิธีการ ตัวอย่าง:

public class MyClass
{
     public Func<int> FunctionB { get; set; }

     public MyClass()
     {
         FunctionB = FunctionBImpl;
     }

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionBImpl()
     {
         return new Random().Next();
     }
}

[TestClass]
public class MyClassTests
{
    private MyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new MyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionB = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

วิธีที่ 2: ทำให้สมาชิกเสมือนคลาสที่สืบทอดมาและในคลาสที่ได้รับใช้ฟังก์ชันและการดำเนินการเพื่อแทนที่ฟังก์ชันการทำงานตัวอย่าง:

public class MyClass
{     
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected virtual int FunctionB()
     {
         return new Random().Next();
     }
}

public class TestableMyClass
{
     public Func<int> FunctionBFunc { get; set; }

     public MyClass()
     {
         FunctionBFunc = base.FunctionB;
     }

     protected override int FunctionB()
     {
         return FunctionBFunc();
     }
}

[TestClass]
public class MyClassTests
{
    private TestableMyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new TestableMyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionBFunc = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

ฉันต้องการรู้ว่าอะไรดีกว่าและทำไม?

ปรับปรุง: หมายเหตุ: FunctionB ยังสามารถเป็นสาธารณะ


ตัวอย่างของคุณง่าย แต่ไม่ถูกต้อง FunctionAส่งกลับค่าบูล แต่ตั้งค่าตัวแปรเฉพาะที่xและไม่ส่งคืนสิ่งใด
Eric P.

1
ในตัวอย่างนี้ FunctionB สามารถอยู่public staticในคลาสอื่นได้

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

1
FunctionBถูกทำลายโดยการออกแบบ new Random().Next()ผิดเกือบทุกครั้ง Randomคุณควรฉีดตัวอย่างของ ( Randomเป็นคลาสที่ออกแบบมาไม่ดีซึ่งอาจทำให้เกิดปัญหาเพิ่มเติมเล็กน้อย)
CodesInChaos

ในหมายเหตุทั่วไป DI ที่ได้รับมอบหมายมาก ๆ เป็นเรื่องที่ดีมาก imho
jk

คำตอบ:


32

แก้ไขดังต่อไปนี้การปรับปรุงโปสเตอร์ต้นฉบับ

ข้อจำกัดความรับผิดชอบ: ไม่ใช่โปรแกรมเมอร์ C # (ส่วนใหญ่เป็น Java หรือ Ruby) คำตอบของฉันคือ: ฉันจะไม่ทดสอบเลยและฉันไม่คิดว่าคุณควร

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

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

ดูการสนทนาที่เกี่ยวข้องที่นั่น: https://stackoverflow.com/questions/105007/should-i-test-private-methods-or-only-public-ones

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

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


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

สวัสดีขอบคุณสำหรับการตอบกลับ แต่ไม่ตอบคำถามของฉัน ฉันไม่สำคัญว่า FunctionB นั้นเป็นแบบส่วนตัว / มีการป้องกัน มันยังสามารถเป็นสาธารณะและยังคงถูกเรียกจาก FunctionA

วิธีทั่วไปในการจัดการปัญหานี้โดยไม่ต้องออกแบบคลาสพื้นฐานใหม่คือ subclass MyClassและแทนที่เมธอดด้วยฟังก์ชันที่คุณต้องการสตับ มันอาจเป็นความคิดที่ดีที่จะอัปเดตคำถามของคุณเพื่อรวมคำถามที่FunctionBเป็นสาธารณะ
Eric P.

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

2
ความจริงที่ว่า FunctionA เรียกใช้ FunctionB นั้นเป็นรายละเอียดที่ไม่เกี่ยวข้องจากมุมมองการทดสอบหน่วย หากการทดสอบสำหรับ FunctionA ถูกเขียนอย่างถูกต้องจะเป็นรายละเอียดการใช้งานที่สามารถ refactored-out ในภายหลังโดยไม่ทำลายการทดสอบ (ตราบใดที่พฤติกรรมโดยรวมของ FunctionA ไม่มีการเปลี่ยนแปลง) ปัญหาจริงคือการดึงหมายเลขสุ่มของ FunctionB นั้นจำเป็นต้องทำกับวัตถุที่ถูกฉีดเพื่อให้คุณสามารถใช้จำลองในระหว่างการทดสอบเพื่อให้แน่ใจว่าหมายเลขที่รู้จักกันดีจะถูกส่งกลับ สิ่งนี้ช่วยให้คุณทดสอบอินพุต / เอาต์พุตที่รู้จักกันดี
Dan Lyons

11

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

ดังนั้นถ้าฉันอยู่ในสถานการณ์ที่ฉันมี

class A 
{
     public B C()
     {
         D();
     }

     private E D();
     {
         // i actually want to control what this produces when I test C()
         // or this is important enough to test on its own
         // and, typically, both of the above
     }
}

จากนั้นฉันก็จะไป refactor

class A 
{
     ICollaborator collaborator;

     public A(ICollaborator collaborator)
     {
         this.collaborator = collaborator;
     }

     public B C()
     {
         collaborator.D();
     }
}

ตอนนี้ฉันมีสถานการณ์ที่ D () สามารถทดสอบได้อย่างอิสระและเปลี่ยนได้อย่างเต็มที่

ในฐานะที่เป็นองค์กรผู้ทำงานร่วมกันของฉันอาจไม่อยู่ในเนมสเปซเดียวกัน ตัวอย่างเช่นถ้าAอยู่ใน FooCorp.BLL ผู้ทำงานร่วมกันของฉันอาจเป็นอีกชั้นหนึ่งลึกเช่นเดียวกับใน FooCorp.BLL.Collaborators (หรือชื่ออะไรก็ตามที่เหมาะสม) ผู้ทำงานร่วมกันของฉันอาจมองเห็นได้เฉพาะภายในแอสเซมบลีผ่านinternalตัวแก้ไขการเข้าถึงเท่านั้นซึ่งฉันก็จะได้สัมผัสกับโครงการทดสอบหน่วยของฉันผ่านInternalsVisibleToแอททริบิวต์แอสเซมบลี สิ่งที่ควรหลีกเลี่ยงคือคุณยังคงสามารถรักษาความสะอาด API ของคุณได้ตราบใดที่ผู้ติดต่อยังกังวลขณะที่สร้างรหัสที่สามารถตรวจสอบได้


ใช่ถ้า ICollaborator ต้องการหลายวิธี หากคุณมีวัตถุที่มีเพียงงานเดียวคือการห่อวิธีการเดียวฉันอยากจะเห็นว่ามันถูกแทนที่ด้วยผู้แทน
jk

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

0

เพิ่มไปยังสิ่งที่ Martin ชี้ให้เห็น

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

ในทั้งสองวิธีที่คุณพูดถึงฉันมีข้อกังวลเหล่านี้ -

วิธีที่ 1 - สิ่งนี้จะเปลี่ยนชั้นเรียนภายใต้พฤติกรรมการทดสอบในการทดสอบ

วิธีที่ 2 - อันนี้ไม่ได้ทดสอบรหัสการผลิตแทนการทดสอบการใช้งานอื่นแทน

ในปัญหาที่ระบุไว้ฉันเห็นว่าตรรกะของ A เท่านั้นคือการดูว่าผลลัพธ์ของ FunctionB เป็นแบบคู่ ถึงแม้ว่าเป็นตัวอย่าง FunctionB ให้ค่าสุ่มซึ่งยากต่อการทดสอบ

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


3
protectedpublicเกือบจะเป็นเช่นเดียวกับ เท่านั้นprivateและinternalมีรายละเอียดการใช้งาน
CodesInChaos

@codeinchaos - ฉันอยากรู้อยากเห็นที่นี่ สำหรับการทดสอบวิธีการที่ได้รับการป้องกันคือ 'ส่วนตัว' เว้นแต่คุณจะปรับเปลี่ยนแอตทริบิวต์การประกอบ เฉพาะประเภทที่ได้รับเท่านั้นที่สามารถเข้าถึงสมาชิกที่ได้รับความคุ้มครอง ด้วยข้อยกเว้นของเสมือนฉันไม่เห็นว่าทำไมการป้องกันควรได้รับการปฏิบัติเช่นเดียวกับสาธารณะจากการทดสอบ กรุณาอธิบายเพิ่มเติมหน่อยได้ไหม?
Srikanth Venugopalan

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

@CodesInChaos ตกลงว่าคลาสที่ได้รับอาจอยู่ในชุดประกอบที่แตกต่างกัน แต่ขอบเขตยังคง จำกัด อยู่ที่ประเภทฐานและประเภทที่ได้รับมา การแก้ไขตัวปรับการเข้าถึงเพียงเพื่อให้สามารถทดสอบได้เป็นสิ่งที่ฉันกังวลเล็กน้อย ฉันทำไปแล้ว แต่ดูเหมือนจะเป็นปฏิปักษ์ต่อฉัน
Srikanth Venugopalan

0

ฉันใช้ Method1 เป็นการส่วนตัวทำให้วิธีการทั้งหมดเป็น Actions หรือ Funcs เพราะนี่เป็นการปรับปรุงความสามารถในการทดสอบโค้ดของฉัน เช่นเดียวกับการแก้ปัญหาใด ๆ มีข้อดีข้อเสียกับวิธีการนี้:

ข้อดี

  1. ช่วยให้โครงสร้างรหัสอย่างง่ายที่ใช้รูปแบบรหัสเพียงสำหรับการทดสอบหน่วยอาจเพิ่มความซับซ้อนเพิ่มเติม
  2. อนุญาตให้คลาสปิดผนึกและสำหรับการกำจัดวิธีเสมือนที่จำเป็นโดยกรอบการเยาะเย้ยที่เป็นที่นิยมเช่น Moq การปิดผนึกคลาสและกำจัดวิธีเสมือนทำให้ผู้สมัครได้รับการอินไลน์และการปรับแต่งคอมไพเลอร์อื่น ๆ ( https://msdn.microsoft.com/en-us/library/ff647802.aspx )
  3. ลดความสามารถในการทดสอบได้ง่าย ๆ โดยแทนที่การใช้งาน Func / Action ในการทดสอบหน่วยนั้นทำได้ง่ายเพียงแค่กำหนดค่าใหม่ให้กับ Func / Action
  4. อนุญาตให้ทำการทดสอบว่ามีการเรียก Func แบบคงที่จากวิธีอื่นหรือไม่เนื่องจากวิธีการแบบสแตติกไม่สามารถจำลองได้
  5. ง่ายต่อการปรับโครงสร้างวิธีที่มีอยู่ให้กับ Funcs / Actions อีกครั้งเนื่องจากไวยากรณ์สำหรับการเรียกใช้วิธีการบนไซต์การโทรยังคงเหมือนเดิม (สำหรับบางครั้งที่คุณไม่สามารถปรับวิธีให้เป็น Func / Action ได้อีกให้ดูข้อเสีย)

จุดด้อย

  1. ไม่สามารถใช้ Funcs / Actions หากคลาสของคุณได้มาจาก Funcs / Actions ไม่มีเส้นทางการสืบทอดเช่นเมธอด
  2. ไม่สามารถใช้พารามิเตอร์เริ่มต้น การสร้าง Func ด้วยพารามิเตอร์เริ่มต้นจะเป็นการสร้างผู้รับมอบสิทธิ์ใหม่ซึ่งอาจทำให้รหัสสับสนขึ้นอยู่กับกรณีการใช้งาน
  3. ไม่สามารถใช้ไวยากรณ์ของพารามิเตอร์ที่กำหนดชื่อสำหรับวิธีการโทรเช่นอะไรบางอย่าง (ชื่อ: "S", นามสกุล: "K")
  4. ข้อผิดพลาดที่ใหญ่ที่สุดคือคุณไม่สามารถเข้าถึงการอ้างอิง 'this' ใน Funcs และ Actions ดังนั้นการอ้างอิงใด ๆ ในชั้นเรียนจะต้องส่งผ่านอย่างชัดเจนเป็นพารามิเตอร์ มันดีเหมือนที่คุณรู้ว่าการพึ่งพาทั้งหมด แต่ไม่ดีถ้าคุณมีคุณสมบัติมากมายที่ Func ของคุณจะขึ้นอยู่กับ ไมล์สะสมของคุณจะแตกต่างกันไปตามกรณีการใช้งานของคุณ

ดังนั้นโดยสรุปการใช้ Funcs และ Actions for Unit test นั้นยอดเยี่ยมถ้าคุณรู้ว่าคลาสของคุณจะไม่ถูกแทนที่

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

public class MyClass
{
     public Func<int> FunctionB = () => new Random().Next();

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }
}

หวังว่านี่จะช่วยได้!


-1

ใช้จำลองเป็นไปได้ Nuget: https://www.nuget.org/packages/moq/

และเชื่อฉันมันค่อนข้างง่ายและมีเหตุผล

public class SomeClass
{
    public SomeClass(int a) { }

    public void A()
    {
        B();
    }

    public virtual void B()
    {

    }
}

[TestFixture]
public class Test
{
    [Test]
    public void Test_A_Calls_B()
    {
        var mockedObject = new Mock<SomeClass>(5); // You can also specify constructor arguments.
        //You can also setup what a function can return.
        var obj = mockedObject.Object;
        obj.A();

        Mock.Get(obj).Verify(x=>x.B(),Times.AtLeastOnce);//This test passes
    }
}

จำลองต้องการวิธีการเสมือนเพื่อแทนที่

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