ส่งผ่านพารามิเตอร์ที่ซับซ้อนไปยัง [ทฤษฎี]


100

Xunit มีคุณสมบัติที่ดี : คุณสามารถสร้างการทดสอบหนึ่งรายการด้วยTheoryแอตทริบิวต์และใส่ข้อมูลในInlineDataแอตทริบิวต์และ xUnit จะสร้างการทดสอบมากมายและทดสอบทั้งหมด

ฉันต้องการจะมีบางสิ่งบางอย่างเช่นนี้ แต่พารามิเตอร์เป็นวิธีของฉันไม่ได้เป็น 'ข้อมูลง่าย (เช่นstring, int, double) แต่รายการระดับของฉัน:

public static void WriteReportsToMemoryStream(
    IEnumerable<MyCustomClass> listReport,
    MemoryStream ms,
    StreamWriter writer) { ... }

1
คู่มือฉบับสมบูรณ์ที่ส่งวัตถุที่ซับซ้อนเป็นพารามิเตอร์ไปยังวิธีการทดสอบประเภทที่ซับซ้อนในการทดสอบหน่วย
Iman Bahrampour

คำตอบ:


139

มีxxxxDataแอตทริบิวต์มากมายใน XUnit ดูตัวอย่างPropertyDataแอตทริบิวต์

IEnumerable<object[]>คุณสามารถใช้ทรัพย์สินที่ผลตอบแทน แต่ละobject[]วิธีที่สร้างขึ้นจะถูก "คลายแพ็ก" เป็นพารามิเตอร์สำหรับการเรียก[Theory]ใช้เมธอดของคุณเพียงครั้งเดียว

อีกทางเลือกหนึ่งคือClassDataซึ่งใช้งานได้เหมือนกัน แต่อนุญาตให้แชร์ 'เครื่องกำเนิดไฟฟ้า' ระหว่างการทดสอบในคลาส / เนมสเปซต่างๆได้อย่างง่ายดายและยังแยก 'ตัวสร้างข้อมูล' ออกจากวิธีการทดสอบจริง

ดูเช่นตัวอย่างเหล่านี้จากที่นี่ :

ตัวอย่าง PropertyData

public class StringTests2
{
    [Theory, PropertyData(nameof(SplitCountData))]
    public void SplitCount(string input, int expectedCount)
    {
        var actualCount = input.Split(' ').Count();
        Assert.Equal(expectedCount, actualCount);
    }

    public static IEnumerable<object[]> SplitCountData
    {
        get
        {
            // Or this could read from a file. :)
            return new[]
            {
                new object[] { "xUnit", 1 },
                new object[] { "is fun", 2 },
                new object[] { "to test with", 3 }
            };
        }
    }
}

ตัวอย่าง ClassData

public class StringTests3
{
    [Theory, ClassData(typeof(IndexOfData))]
    public void IndexOf(string input, char letter, int expected)
    {
        var actual = input.IndexOf(letter);
        Assert.Equal(expected, actual);
    }
}

public class IndexOfData : IEnumerable<object[]>
{
    private readonly List<object[]> _data = new List<object[]>
    {
        new object[] { "hello world", 'w', 6 },
        new object[] { "goodnight moon", 'w', -1 }
    };

    public IEnumerator<object[]> GetEnumerator()
    { return _data.GetEnumerator(); }

    IEnumerator IEnumerable.GetEnumerator()
    { return GetEnumerator(); }
}

@dcastro: ใช่ฉันกำลังค้นหาบางส่วนในเอกสาร xunit ดั้งเดิม
quetzalcoatl

2
@ Nick: ผมยอมรับว่าคล้ายกับ PropertyData staticแต่ยังคุณได้ชี้ให้เห็นเหตุผลที่มัน: นั่นเป็นเหตุผลที่ฉันไม่ทำ ClassData คือเมื่อคุณต้องการหลบหนีจากสถิตยศาสตร์ ด้วยการทำเช่นนี้คุณสามารถใช้ซ้ำ (เช่นรัง) เครื่องกำเนิดไฟฟ้าได้ง่ายขึ้น
quetzalcoatl

1
มีความคิดเกิดขึ้นกับ ClassData หรือไม่ ฉันไม่พบใน xUnit2.0 ตอนนี้ฉันใช้ MemberData ด้วยวิธีการแบบคงที่ซึ่งสร้างอินสแตนซ์ใหม่ของคลาสและส่งคืนสิ่งนั้น
Erti-Chris Eelmaa

14
@Erti ใช้[MemberData("{static member}", MemberType = typeof(MyClass))]เพื่อแทนที่ClassDataแอตทริบิวต์
Junle Li

7
สำหรับ C # 6 ขอแนะนำให้ใช้nameofคีย์เวิร์ดแทนการเข้ารหัสชื่อคุณสมบัติ (แบ่งได้ง่าย แต่เงียบ)
sara

41

การอัปเดต @ คำตอบของ Quetzalcoatl: แอตทริบิวต์[PropertyData]ได้ถูกแทนที่โดย[MemberData]ซึ่งจะใช้เวลาเป็นอาร์กิวเมนต์ที่ชื่อสตริงของวิธีการคงข้อมูลใด ๆ IEnumerable<object[]>หรือทรัพย์สินที่ส่งกลับ (ฉันรู้สึกดีเป็นอย่างยิ่งที่มีวิธีการวนซ้ำที่สามารถคำนวณกรณีทดสอบทีละกรณีได้จริงโดยให้ผลตามที่คำนวณได้)

แต่ละองค์ประกอบในลำดับที่ส่งกลับโดยตัวแจงนับคือobject[]อาร์เรย์และแต่ละอาร์เรย์ต้องมีความยาวเท่ากันและความยาวนั้นจะต้องเป็นจำนวนอาร์กิวเมนต์ของกรณีทดสอบของคุณ (ใส่คำอธิบายประกอบด้วยแอตทริบิวต์[MemberData]และแต่ละองค์ประกอบต้องมีประเภทเดียวกันกับพารามิเตอร์วิธีการที่เกี่ยวข้อง (หรืออาจจะเป็นประเภทเปิดประทุนก็ได้ก็ไม่รู้)

(ดูบันทึกประจำรุ่นสำหรับ xUnit.net มีนาคม 2014และแพตช์จริงพร้อมโค้ดตัวอย่าง )


3
@davidbak codplex หายไปแล้ว ลิงก์ไม่ทำงาน
Kishan Vaishnav

11

สมมติว่าเรามีคลาสรถที่ซับซ้อนซึ่งมีคลาสผู้ผลิต:

public class Car
{
     public int Id { get; set; }
     public long Price { get; set; }
     public Manufacturer Manufacturer { get; set; }
}
public class Manufacturer
{
    public string Name { get; set; }
    public string Country { get; set; }
}

เราจะเติมและส่งผ่านคลาสรถยนต์ไปยังการทดสอบทฤษฎี

ดังนั้นสร้างคลาส 'CarClassData' ที่ส่งคืนอินสแตนซ์ของคลาส Car ดังต่อไปนี้:

public class CarClassData : IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] {
                new Car
                {
                  Id=1,
                  Price=36000000,
                  Manufacturer = new Manufacturer
                  {
                    Country="country",
                    Name="name"
                  }
                }
            };
        }
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

ถึงเวลาสร้างวิธีทดสอบ (CarTest) และกำหนดรถเป็นพารามิเตอร์:

[Theory]
[ClassData(typeof(CarClassData))]
public void CarTest(Car car)
{
     var output = car;
     var result = _myRepository.BuyCar(car);
}

ประเภทที่ซับซ้อนในทฤษฎี

โชคดี


4
คำตอบนี้กล่าวอย่างชัดเจนถึงคำถามเกี่ยวกับการส่งประเภทที่กำหนดเองเป็นอินพุตทฤษฎีซึ่งดูเหมือนจะหายไปจากคำตอบที่เลือก
JD Cain

1
นี่เป็นกรณีการใช้งานที่ฉันกำลังมองหาซึ่งเป็นวิธีการส่งผ่านประเภทที่ซับซ้อนเป็นพารามิเตอร์ไปยังทฤษฎี ทำงานได้อย่างสมบูรณ์แบบ! สิ่งนี้คุ้มค่าสำหรับการทดสอบรูปแบบ MVP ตอนนี้ฉันสามารถตั้งค่าอินสแตนซ์ต่างๆของมุมมองได้ในทุกสถานะและส่งต่อทั้งหมดไปยังทฤษฎีเดียวกันซึ่งจะทดสอบเอฟเฟกต์ที่วิธีของผู้นำเสนอมีต่อมุมมองนั้น รักมัน!
Denis M. Kitchen

10

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

ก่อนอื่นให้กำหนดคลาสที่ใช้ร่วมกันที่ใช้ซ้ำได้

//http://stackoverflow.com/questions/22093843
public interface ITheoryDatum
{
    object[] ToParameterArray();
}

public abstract class TheoryDatum : ITheoryDatum
{
    public abstract object[] ToParameterArray();

    public static ITheoryDatum Factory<TSystemUnderTest, TExpectedOutput>(TSystemUnderTest sut, TExpectedOutput expectedOutput, string description)
    {
        var datum= new TheoryDatum<TSystemUnderTest, TExpectedOutput>();
        datum.SystemUnderTest = sut;
        datum.Description = description;
        datum.ExpectedOutput = expectedOutput;
        return datum;
    }
}

public class TheoryDatum<TSystemUnderTest, TExecptedOutput> : TheoryDatum
{
    public TSystemUnderTest SystemUnderTest { get; set; }

    public string Description { get; set; }

    public TExpectedOutput ExpectedOutput { get; set; }

    public override object[] ToParameterArray()
    {
        var output = new object[3];
        output[0] = SystemUnderTest;
        output[1] = ExpectedOutput;
        output[2] = Description;
        return output;
    }

}

ตอนนี้การทดสอบส่วนบุคคลและข้อมูลสมาชิกของคุณเขียนง่ายขึ้นและสะอาดขึ้น ...

public class IngredientTests : TestBase
{
    [Theory]
    [MemberData(nameof(IsValidData))]
    public void IsValid(Ingredient ingredient, bool expectedResult, string testDescription)
    {
        Assert.True(ingredient.IsValid == expectedResult, testDescription);
    }

    public static IEnumerable<object[]> IsValidData
    {
        get
        {
            var food = new Food();
            var quantity = new Quantity();
            var data= new List<ITheoryDatum>();

            data.Add(TheoryDatum.Factory(new Ingredient { Food = food }                       , false, "Quantity missing"));
            data.Add(TheoryDatum.Factory(new Ingredient { Quantity = quantity }               , false, "Food missing"));
            data.Add(TheoryDatum.Factory(new Ingredient { Quantity = quantity, Food = food }  , true,  "Valid"));

            return data.ConvertAll(d => d.ToParameterArray());
        }
    }
}

Descriptionคุณสมบัติของสตริงคือการโยนกระดูกตัวเองเมื่อหนึ่งในหลาย ๆ กรณีการทดสอบของคุณล้มเหลว


1
ฉันชอบสิ่งนี้; มันมีศักยภาพที่แท้จริงสำหรับวัตถุที่ซับซ้อนมากฉันต้องตรวจสอบความถูกต้องของคุณสมบัติมากกว่า 90 รายการ ฉันสามารถส่งผ่านออบเจ็กต์ JSON แบบธรรมดายกเลิกการกำหนดค่าเริ่มต้นและสร้างข้อมูลสำหรับการทดสอบซ้ำ ทำได้ดีมาก
Gustyn

1
ไม่ใช่พารามิเตอร์สำหรับวิธีการทดสอบ IsValid ผสมกัน - ไม่ควรเป็น IsValid (ส่วนผสม, exprectedResult, testDescription) หรือไม่
pastacool

3

คุณสามารถลองวิธีนี้:

public class TestClass {

    bool isSaturday(DateTime dt)
    {
       string day = dt.DayOfWeek.ToString();
       return (day == "Saturday");
    }

    [Theory]
    [MemberData("IsSaturdayIndex", MemberType = typeof(TestCase))]
    public void test(int i)
    {
       // parse test case
       var input = TestCase.IsSaturdayTestCase[i];
       DateTime dt = (DateTime)input[0];
       bool expected = (bool)input[1];

       // test
       bool result = isSaturday(dt);
       result.Should().Be(expected);
    }   
}

สร้างคลาสอื่นเพื่อเก็บข้อมูลการทดสอบ:

public class TestCase
{
   public static readonly List<object[]> IsSaturdayTestCase = new List<object[]>
   {
      new object[]{new DateTime(2016,1,23),true},
      new object[]{new DateTime(2016,1,24),false}
   };

   public static IEnumerable<object[]> IsSaturdayIndex
   {
      get
      {
         List<object[]> tmp = new List<object[]>();
            for (int i = 0; i < IsSaturdayTestCase.Count; i++)
                tmp.Add(new object[] { i });
         return tmp;
      }
   }
}

1

สำหรับความต้องการของฉันฉันแค่ต้องการเรียกใช้ชุด 'ผู้ใช้ทดสอบ' ผ่านการทดสอบบางอย่าง - แต่ [ClassData] ฯลฯ ดูเหมือนจะเกินความจำเป็นสำหรับสิ่งที่ฉันต้องการ (เนื่องจากรายการของรายการถูกแปลเป็นภาษาท้องถิ่นสำหรับการทดสอบแต่ละครั้ง)

ดังนั้นฉันจึงทำสิ่งต่อไปนี้โดยมีอาร์เรย์ภายในการทดสอบ - จัดทำดัชนีจากภายนอก:

[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public async Task Account_ExistingUser_CorrectPassword(int userIndex)
{
    // DIFFERENT INPUT DATA (static fake users on class)
    var user = new[]
    {
        EXISTING_USER_NO_MAPPING,
        EXISTING_USER_MAPPING_TO_DIFFERENT_EXISTING_USER,
        EXISTING_USER_MAPPING_TO_SAME_USER,
        NEW_USER

    } [userIndex];

    var response = await Analyze(new CreateOrLoginMsgIn
    {
        Username = user.Username,
        Password = user.Password
    });

    // expected result (using ExpectedObjects)
    new CreateOrLoginResult
    {
        AccessGrantedTo = user.Username

    }.ToExpectedObject().ShouldEqual(response);
}

สิ่งนี้บรรลุเป้าหมายของฉันในขณะที่รักษาเจตนาของการทดสอบให้ชัดเจน คุณเพียงแค่ต้องให้ดัชนีซิงค์ แต่นั่นคือทั้งหมด

ผลลัพธ์ดูดีมันยุบได้และคุณสามารถเรียกใช้อินสแตนซ์ที่ต้องการซ้ำได้หากคุณได้รับข้อผิดพลาด:

ป้อนคำอธิบายภาพที่นี่


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

อันที่จริงผมเพิ่งทำงานออกว่ามันเป็นไปได้ด้วยMemberDataถ้าคุณใช้และเลือกTheoryData IXunitSerializableข้อมูลเพิ่มเติมและ exmaples ที่นี่ ... github.com/xunit/xunit/issues/429#issuecomment-108187109
Oliver Pearmain

1

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

    [Theory]
    [ClassData(typeof(DeviceTelemetryTestData))]
    public async Task ProcessDeviceTelemetries_TypicalDeserialization_NoErrorAsync(params DeviceTelemetry[] expected)
    {
        // Arrange
        var timeStamp = DateTimeOffset.UtcNow;

        mockInflux.Setup(x => x.ExportTelemetryToDb(It.IsAny<List<DeviceTelemetry>>())).ReturnsAsync("Success");

        // Act
        var actual = await MessageProcessingTelemetry.ProcessTelemetry(JsonConvert.SerializeObject(expected), mockInflux.Object);

        // Assert
        mockInflux.Verify(x => x.ExportTelemetryToDb(It.IsAny<List<DeviceTelemetry>>()), Times.Once);
        Assert.Equal("Success", actual);
    }

นี่คือการทดสอบหน่วยของฉันสังเกตพารามิเตอร์params สิ่งนี้อนุญาตให้ส่งวัตถุจำนวนอื่น และตอนนี้คลาสDeviceTelemetryTestDataของฉัน:

    public class DeviceTelemetryTestData : IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
        }

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

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


-1

ฉันเดาว่าคุณเข้าใจผิดที่นี่ Theoryแอตทริบิวต์xUnit หมายถึงอะไร: คุณต้องการทดสอบฟังก์ชันนี้โดยส่งค่าพิเศษ / สุ่มเป็นพารามิเตอร์ที่ฟังก์ชันนี้อยู่ภายใต้การทดสอบได้รับ นั่นหมายความว่าสิ่งที่คุณกำหนดเป็นแอตทริบิวต์ต่อไปเช่น: InlineData, PropertyData, ClassDataฯลฯ .. จะเป็นแหล่งสำหรับพารามิเตอร์เหล่านั้น นั่นหมายความว่าคุณควรสร้างวัตถุต้นทางเพื่อจัดเตรียมพารามิเตอร์เหล่านั้น ในกรณีของคุณฉันเดาว่าคุณควรใช้ClassDataobject เป็น source นอกจากนี้โปรดทราบว่าClassDataสืบทอดมาจาก: IEnumerable<>- นั่นหมายความว่าทุกครั้งที่ชุดของพารามิเตอร์ที่สร้างขึ้นอื่นจะถูกใช้เป็นพารามิเตอร์ขาเข้าสำหรับฟังก์ชันภายใต้การทดสอบจนกว่าจะIEnumerable<>สร้างค่า

ตัวอย่างที่นี่: Tom DuPont .NET

ตัวอย่างอาจไม่ถูกต้อง - ฉันไม่ได้ใช้ xUnit เป็นเวลานาน

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