การทดสอบหน่วยในโลก“ ไม่มีตัวตั้ง”


23

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

public class MyClass
{
    public Boolean IsPublished
    {
        get { return PublishDate != null; }
    }

    public DateTime? PublishDate { get; set; }

    public void Publish()
    {
        if (IsPublished)
            throw new InvalidOperationException("Already published.");

        PublishDate = DateTime.Today;

        Raise(new PublishedEvent());
    }
}

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

ให้สถานการณ์เป็นจริงมากขึ้นด้วยรหัสต่อไปนี้:

public class Document
{
    public Document(String title)
    {
        if (String.IsNullOrWhiteSpace(title))
            throw new ArgumentException("title");

        Title = title;
    }

    public String ApprovedBy { get; private set; }
    public DateTime? ApprovedOn { get; private set; }
    public Boolean IsApproved { get; private set; }
    public Boolean IsPublished { get; private set; }
    public String PublishedBy { get; private set; }
    public DateTime? PublishedOn { get; private set; }
    public String Title { get; private set; }

    public void Approve(String by)
    {
        if (IsApproved)
            throw new InvalidOperationException("Already approved.");

        ApprovedBy = by;
        ApprovedOn = DateTime.Today;
        IsApproved = true;

        Raise(new ApprovedEvent(Title));
    }

    public void Publish(String by)
    {
        if (IsPublished)
            throw new InvalidOperationException("Already published.");

        if (!IsApproved)
            throw new InvalidOperationException("Cannot publish until approved.");

        PublishedBy = by;
        PublishedOn = DateTime.Today;
        IsPublished = true;

        Raise(new PublishedEvent(Title));
    }
}

ฉันต้องการเขียนการทดสอบหน่วยที่ตรวจสอบ:

  • ฉันไม่สามารถเผยแพร่ได้หากเอกสารได้รับการอนุมัติแล้ว
  • ฉันไม่สามารถเผยแพร่เอกสารได้อีกครั้ง
  • เมื่อเผยแพร่แล้วค่าที่ได้รับการเผยแพร่โดย PublishingBy และจะถูกตั้งค่าอย่างเหมาะสม
  • เมื่อ publised ที่เผยแพร่แล้ว Event จะเพิ่มขึ้น

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

คุณมีวิธีแก้ (ง) ปัญหานี้อย่างไร?


ยิ่งฉันคิดถึงสิ่งนี้มากเท่าไหร่ฉันก็ยิ่งคิดว่าปัญหาทั้งหมดของคุณคือการมีผลข้างเคียง หรือค่อนข้างเป็นวัตถุที่เปลี่ยนแปลงไม่ได้ ใน DDD- โลกคุณไม่ควรส่งคืนวัตถุเอกสารใหม่จากทั้งอนุมัติและเผยแพร่แทนที่จะอัพเดตสถานะภายในของวัตถุนี้
pdr

1
คำถามด่วนที่คุณใช้ O / RM ฉันเป็นแฟนตัวยงของ EF แต่การประกาศว่าเซทเทอร์ได้รับการคุ้มครองจะทำให้ฉันผิดไปเล็กน้อย
Michael Brown

เรามีการผสมผสานกันในขณะนี้เนื่องจากการพัฒนาช่วงฟรีที่ฉันถูกตั้งข้อหากับการโต้เถียง ADO.NET บางส่วนที่ใช้ AutoMapper เพื่อให้ความชุ่มชื้นจาก DataReader ซึ่งเป็นโมเดล Linq-to-SQL สองตัว (ซึ่งจะเป็นรุ่นถัดไปเพื่อแทนที่ ) และรุ่นใหม่ของ EF
SonOfPirate

การประกาศเผยแพร่สองครั้งนั้นไม่ส่งกลิ่นเหม็นเลยและเป็นวิธีที่จะทำ
Piotr Perak

คำตอบ:


27

ฉันไม่สามารถนำวัตถุเข้าสู่สถานะที่จำเป็นในการทำการทดสอบ

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

  • ฉันไม่สามารถเผยแพร่ได้ถ้าเอกสารได้รับการอนุมัติ: เขียนการทดสอบที่เรียกการประกาศก่อนที่จะเรียกการอนุมัติทำให้เกิดข้อผิดพลาดทางขวาโดยไม่เปลี่ยนสถานะวัตถุ

    void testPublishBeforeApprove() {
        doc = new Document("Doc");
        AssertRaises(doc.publish, ..., NotApprovedException);
    }
    
  • ฉันไม่สามารถเผยแพร่เอกสารอีกครั้ง: เขียนการทดสอบที่อนุมัติวัตถุแล้วเรียกการเผยแพร่เมื่อสำเร็จ แต่ครั้งที่สองทำให้เกิดข้อผิดพลาดที่ถูกต้องโดยไม่เปลี่ยนสถานะของวัตถุ

    void testRePublish() {
        doc = new Document("Doc");
        doc.approve();
        doc.publish();
        AssertRaises(doc.publish, ..., RepublishException);
    }
    
  • เมื่อเผยแพร่แล้วค่า PublishingBy และ PublishedOn จะได้รับการตั้งค่าอย่างถูกต้อง: เขียนการทดสอบที่เรียกว่าอนุมัติจากนั้นเรียกใช้การเผยแพร่ยืนยันว่าสถานะวัตถุเปลี่ยนแปลงอย่างถูกต้อง

    void testPublish() {
        doc = new Document("Doc");
        doc.approve();
        doc.publish();
        Assert(doc.PublishedBy, ...);
        ...
    }
    
  • เมื่อ publised ที่เผยแพร่แล้ว Event: ยกเบ็ดกับระบบเหตุการณ์และตั้งค่าสถานะเพื่อให้แน่ใจว่ามันถูกเรียก

คุณต้องเขียนแบบทดสอบเพื่อขออนุมัติ

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


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

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

4
ฉันยังไม่เห็นการทดสอบหน่วยซึ่งอาจล้มเหลวได้ด้วยเหตุผลเดียวที่เป็นไปได้ นอกจากนี้คุณสามารถใส่ส่วน "การจัดการของรัฐ" ของการทดสอบเป็นsetup()วิธี --- ไม่ใช่การทดสอบ
Peter K.

12
ทำไมจะขึ้นอยู่กับความapprove()เปราะอย่างใด แต่ขึ้นอยู่กับsetApproved(true)อย่างใดไม่ได้? approve()เป็นการอ้างอิงที่ถูกต้องตามกฎหมายในการทดสอบเพราะเป็นการอ้างอิงในข้อกำหนด หากการพึ่งพานั้นมีอยู่ในการทดสอบนั่นจะเป็นปัญหาอื่น
Karl Bielefeldt

2
@pdr คุณจะทดสอบคลาสสแต็กได้อย่างไร คุณจะลองทดสอบpush()และpop()วิธีการอย่างอิสระหรือไม่?
Winston Ewert

2

อีกวิธีหนึ่งคือการสร้างนวกรรมิกของคลาสที่อนุญาตให้ตั้งค่าคุณสมบัติภายในบนอินสแตนซ์:

 public Document(
  String approvedBy,
  DateTime? approvedOn,
  Boolean isApproved,
  Boolean isPublished,
  String publishedBy,
  DateTime? publishedOn,
  String title)
{
  ApprovedBy = approvedBy;
  ApprovedOn = approvedOn;
  IsApproved = isApproved;
  IsApproved = isApproved;
  PublishedBy = publishedBy;
  PublishedOn = publishedOn;
}

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

เข้าใจ ตัวอย่างของคุณไม่ได้กล่าวถึงคุณสมบัติอื่น ๆ อีกมากมายและจำนวนในตัวอย่างคือ "บน cusp" ซึ่งมีวิธีการที่ถูกต้อง ดูเหมือนว่าที่นี้จะบอกคุณบางสิ่งบางอย่างเกี่ยวกับการออกแบบของคุณ: คุณไม่สามารถใส่วัตถุของคุณลงในใด ๆของรัฐที่ถูกต้องในการเริ่ม นั่นหมายความว่าคุณต้องใส่มันลงในสถานะเริ่มต้นที่ถูกต้องและพวกเขาจัดการมันเข้าสู่สถานะที่เหมาะสมสำหรับการทดสอบ นั่นหมายถึงคำตอบโกหกของไรอันคือวิธีที่จะไป
Peter K.

แม้ว่าวัตถุมีคุณสมบัติเดียวและจะไม่เปลี่ยนวิธีการแก้ปัญหานี้ไม่ดี อะไรทำให้ทุกคนหยุดไม่ให้ใช้ตัวสร้างนี้ในการผลิต? คุณจะทำเครื่องหมายคอนสตรัคนี้ [TestOnly] อย่างไร
Piotr Perak

ทำไมการผลิตถึงไม่ดี? (ฉันอยากรู้จริงๆ) บางครั้งจำเป็นต้องสร้างสถานะที่แม่นยำของวัตถุที่สร้าง ... ไม่ใช่เพียงวัตถุเริ่มต้นที่ถูกต้องเพียงครั้งเดียว
Peter K.

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

1

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

ในกลยุทธ์ C # หนึ่งอาจจะทำให้ผู้ตั้งค่าภายในจากนั้นเปิดเผยภายในเพื่อทดสอบโครงการ

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


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

1

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

แก้ไขบันทึกย่อบางครั้งการทำให้เป็นอันดับ JSON ล้มเหลว (เช่นในกรณีของกราฟของวัตถุวงจรที่มีกลิ่น btw) ในสถานการณ์เช่นนี้ฉันช่วยในการทำให้เป็นอันดับไบนารี มันใช้งานได้จริง แต่ใช้งานได้ดี :-)


และคุณจะเตรียมวัตถุในสถานะที่คาดหวังได้อย่างไรถ้าไม่มีตัวตั้งค่าและคุณไม่ต้องการเรียกว่าเป็นวิธีการสาธารณะในการตั้งค่า
Piotr Perak

ฉันได้เขียนเครื่องมือเล็ก ๆ สำหรับเรื่องนั้น มันโหลดคลาสโดยการสะท้อนที่สร้างเอนทิตีใหม่โดยใช้ตัวสร้างสาธารณะ (โดยทั่วไปจะใช้เพียงตัวระบุ) และเรียกใช้อาร์เรย์ของ Action <TEntity> บนมันบันทึกสแน็ปช็อตหลังจากการดำเนินการแต่ละครั้ง (ด้วยชื่อทั่วไปตามดัชนีของการกระทำ และชื่อเอนทิตี) เครื่องมือถูกเรียกใช้งานด้วยตนเองในแต่ละการสร้างใหม่ของรหัสเอนทิตีและสแน็ปช็อตจะถูกติดตามโดย DCVS เห็นได้ชัดว่าแต่ละการกระทำเรียกคำสั่งสาธารณะของเอนทิตี แต่นี่เป็นการทดสอบที่ดำเนินการโดยวิธีนี้เป็นการทดสอบหน่วยอย่างแท้จริง
Giacomo Tesio

ฉันไม่เข้าใจว่ามันเปลี่ยนแปลงอะไร หากยังคงเรียกวิธีการสาธารณะใน sut (ระบบภายใต้การทดสอบ) ก็ไม่แตกต่างกันเพียงแค่เรียกวิธีการเหล่านั้นในการทดสอบ
Piotr Perak

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

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

-7

คุณพูด

พยายามใช้แนวทางปฏิบัติที่ดีที่สุดเท่าที่จะทำได้

และ

ORM ที่เราใช้เพื่อให้ความชุ่มชื้นกับวัตถุที่ใช้การสะท้อนจึงสามารถเข้าถึงตัวตั้งค่าส่วนตัว

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


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

หากคุณกังวลเกี่ยวกับการขยายตัวของโค้ดให้ทำการตัดวิธีการทดสอบใน #ifdefs เพื่อให้ใช้ได้เฉพาะในรหัสการดีบัก (อาจเป็นวิธีปฏิบัติที่ดีที่สุด)


4
-1: การทำลายกรอบการทดสอบของคุณและกลับไปที่วิธีทดสอบภายในชั้นเรียนจะกลับไปสู่ยุคมืดของการทดสอบหน่วย
Robert Johnson

9
ไม่มี -1 จากเราได้ แต่รวมทั้งรหัสการทดสอบในการผลิตโดยทั่วไปเป็นBad Thing (TM)
Peter K.

OP ทำอะไรได้อีก ติดกับการกวดขันกับ setters ส่วนตัว! มันเหมือนกับการเลือกพิษที่คุณต้องการดื่ม ข้อเสนอแนะของฉันสำหรับ OP คือการทำให้การทดสอบหน่วยเป็นรหัสการดีบักไม่ใช่การผลิต จากประสบการณ์ของฉันการทดสอบหน่วยลงในโครงการที่แตกต่างกันก็หมายความว่าโครงการนั้นเชื่อมโยงกับต้นฉบับอย่างใกล้ชิดอยู่แล้วดังนั้นจาก dev PoV จึงมีความแตกต่างเล็กน้อย
gbjbaanb
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.