Xunitには素晴らしい機能があります :Theory
属性を使用して1つのテストを作成し、InlineData
属性にデータを入れると、xUnitは多くのテストを生成し、それらすべてをテストできます。
このようなものが欲しいのですが、メソッドのパラメーターは「単純なデータ」ではなく(string
、int
、double
など)、クラスのリストです:
public static void WriteReportsToMemoryStream(
IEnumerable<MyCustomClass> listReport,
MemoryStream ms,
StreamWriter writer) { ... }
XUnitには多くのxxxxData
属性があります。たとえば、PropertyData
属性を確認してください。
IEnumerable<object[]>
を返すプロパティを実装できます。このメソッドが生成する各object[]
は、[Theory]
メソッドへの1回の呼び出しのパラメーターとして「アンパック」されます。
別のオプションは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(); }
}
@Quetzalcoatlの答えを更新するには:属性[PropertyData]
は、[MemberData]
を返す静的メソッド、フィールド、またはプロパティの文字列名を引数として取るIEnumerable<object[]>
に置き換えられました。 (実際にcalculateテストケースを1つずつ実行し、計算されたとおりにそれらを生成できるイテレータメソッドがあることは特に素晴らしいと思います。)
列挙子によって返されるシーケンスの各要素はobject[]
であり、各配列は同じ長さである必要があり、その長さはテストケースへの引数の数である必要があります(属性[MemberData]
および各要素対応するメソッドパラメーターと同じ型である必要があります(または、変換可能な型である可能性がありますが、わかりません)。
( 2014年3月のxUnit.netリリースノート および サンプルコードを含む実際のパッチ を参照してください。)
匿名オブジェクト配列の作成は、データを構築する最も簡単な方法ではないため、プロジェクトでこのパターンを使用しました
最初に、再利用可能な共有クラスを定義します
//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, string testDescription, bool expectedResult)
{
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つが失敗したときにボーンをスローすることです。
Manufacturerクラスを持つ複雑なCarクラスがあるとします:
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; }
}
Carクラスを埋めて、理論テストに渡します。
したがって、以下のようにCarクラスのインスタンスを返す 'CarClassData'クラスを作成します。
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);
}
幸運
あなたはこの方法を試すことができます:
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;
}
}
}
必要に応じて、いくつかのテストで一連の「テストユーザー」を実行したかっただけですが、[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);
}
テストの意図を明確に保ちながら、これは私の目標を達成しました。インデックスの同期を保つ必要があるだけですが、それだけです。
結果は素敵に見えますが、折りたたみ可能で、エラーが発生した場合は特定のインスタンスを再実行できます。