Specflow 的 table 預設用來處理單一欄位,若是想要在欄位裡面塞入 json 或者是比對 json 就得自行處理,這裡列出我常用的方式。
開發環境
- Windows 11
- .NET 8
- Rider 2023.3.2
- SpecFlow.xUnit 3.9.74
- xunit 2.4.2
Specflow Table 轉成物件
錯誤情境描述
Feature 的描述如下
Scenario: 建立一筆會員(錯誤)
Given 已準備 Member 資料(錯誤)
| Id | Age | IpData | Orders | State |
| 1 | 18 | ["192.168.0.1","192.168.0.2"] | [{"Id":"123"}] | Active |
Step 如下
[Given(@"已準備 Member 資料\(錯誤\)")]
public void Given已準備Member資料錯誤(Table table)
{
var members = table.CreateSet(row =>
{
var member = new Member
{
};
return member;
});
}
Member 定義如下
namespace Lab.SpecflowTestProject;
public class Member
{
public string Id { get; set; }
public int Age { get; set; }
public Name Name { get; set; }
public State State { get; set; }
public List<string> IpData { get; set; }
public List<Order> Orders { get; set; }
}
public enum State
{
None,
Active,
Inactive,
}
public class Order
{
public string Id { get; set; }
}
public class Name
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
table.CreateSet 解出的 IpData 集合物件是
["192.168.0.1"
"129.168.0.2"]
table.CreateSet 解出的 Orders 集合物件是
null
兩個都是錯誤的
解法
最簡單的解法就是自行用 table.Rows 處理
[Given(@"已準備 Member 資料\(正確\)")]
public void Given已準備Member資料正確(Table table)
{
var members = new List<Member>();
foreach (var row in table.Rows)
{
var member = new Member();
foreach (var header in table.Header)
{
switch (header)
{
case nameof(Member.Id):
member.Id = row[header];
break;
case nameof(Member.Age):
member.Age = int.Parse(row[header]);
break;
case nameof(Member.State):
member.State = Enum.Parse<State>(row[header]);
break;
case nameof(Member.Name):
member.Name = TryGetName(row);
break;
case nameof(Member.IpData):
member.IpData = TryGetIpData(row);
break;
case nameof(Member.Orders):
member.Orders = TryGetOrders(row);
break;
}
}
members.Add(member);
}
}
private static Name? TryGetName(TableRow row)
{
var data = row.TryGetValue(nameof(Member.Name), out var name)
? JsonSerializer.Deserialize<Name>(name)
: null;
return data;
}
private static List<Order>? TryGetOrders(TableRow row)
{
var data = row.TryGetValue(nameof(Member.Orders), out var orders)
? JsonSerializer.Deserialize<List<Order>>(orders)
: new List<Order>();
return data;
}
private static List<string>? TryGetIpData(TableRow row)
{
var data = row.TryGetValue(nameof(Member.IpData), out var ip)
? JsonSerializer.Deserialize<List<string>>(ip)
: new List<string>();
return data;
}
這樣一來資料就正確地被轉成正確的物件了,如下圖:
擴充方法
把上面的方法改成可以支援泛型的擴充方法
public static class TableExtensions
{
public static IEnumerable<T>? CreateJsonSet<T>(this Table table)
{
var results = new List<T>();
var type = typeof(T);
foreach (var row in table.Rows)
{
var instance = Activator.CreateInstance<T>();
foreach (var header in table.Header)
{
var property = type.GetProperty(header);
if (property == null)
{
continue;
}
var value = row[header];
if (string.IsNullOrWhiteSpace(value))
{
continue;
}
var propertyType = property.PropertyType;
//若是泛型且是集合
if (propertyType.IsGenericType
&& propertyType.GetGenericTypeDefinition() == typeof(List<>))
{
var listType = propertyType.GetGenericArguments()[0];
var list = JsonSerializer.Deserialize(value, typeof(List<>).MakeGenericType(listType));
property.SetValue(instance, list);
}
//若是物件且不是字串
else if (propertyType.IsClass
&& propertyType != typeof(string))
{
var obj = JsonSerializer.Deserialize(value, propertyType);
property.SetValue(instance, obj);
}
//若是列舉
else if (propertyType.IsEnum)
{
property.SetValue(instance, Enum.Parse(propertyType, value));
}
else
{
property.SetValue(instance, Convert.ChangeType(value, propertyType));
}
}
results.Add(instance);
}
return results;
}
}
執行結果如下圖:
Specflow 比對
錯誤情境描述
Specflow 預設也是只支援單一欄位,內建的 table.ComareToSet 是不支援 json 比對的
Scenario: 建立一筆會員(錯誤)
Then 預期得到 Member 資料(錯誤)
| Id | Age | IpData | Orders | State |
| 1 | 18 | ["192.168.0.1","192.168.0.2"] | [{"Id":"123"}] | Active |
這樣寫,會得到比對失敗
[Then(@"預期得到 Member 資料\(錯誤\)")]
public void Then預期得到Member資料錯誤(Table table)
{
var actual = CreateActualMembers();
table.CompareToSet(actual);
}
解法
把 table 的內容轉成強型別 Excepted,手動建立 Actual,通過 FluentAssertions 進行兩者比對
[Then(@"預期得到 Member 資料\(正確\)")]
public void Then預期得到Member資料正確(Table table)
{
var actual = CreateActualMembers();
var expected = table.CreateJsonSet<Member>();
actual.Should().BeEquivalentTo(expected, options => options
.Including(x => x.Id)
.Including(x => x.Age)
.Including(x => x.Name)
.Including(x => x.State)
.Including(x => x.IpData)
.Including(x => x.Orders)
);
}
一個一個列出要比對的欄位有點辛苦,搭配 table.Header 指定要比對的欄位
[Then(@"預期得到 Member 資料\(正確\)")]
public void Then預期得到Member資料正確(Table table)
{
var actual = CreateActualMembers();
var expected = table.CreateJsonSet<Member>();
var header = table.Header.ToHashSet();
actual.Should().BeEquivalentTo(expected, options =>
{
options.Including(info => header.Contains(info.Name));
if (header.Contains(nameof(Member.Name)))
{
options.Including(info => info.Name);
}
return options;
});
}
經實驗,options.Including(info => header.Contains(info.Name)) 無法處理複雜型別,還需要加上 options.Including(info => info.Name)
if (header.Contains(nameof(Member.Name)))
{
options.Including(info => info.Name);
}
驗證一下,是不是可以比對出錯誤
Scenario: 建立一筆會員(正確)
Then 預期得到 Member 資料(正確)
| Id | Age | Name | IpData | Orders | State |
| 1 | 18 | {"FirstName":"yaochang","LastName":"yu1"} | ["192.168.0.1","192.168.0.3"] | [{"Id":"124"}] | Active |
如我所預期,上述兩種方法都比對失敗
Xunit.Sdk.XunitException
Expected property actual[0].Name.LastName to be "yu1" with a length of 3, but "yu" has a length of 2, differs near "u" (index 1).
Expected actual[0].IpData[1] to be "192.168.0.3", but "192.168.0.2" differs near "2" (index 10).
Expected property actual[0].Orders[0].Id to be "124", but "123" differs near "3" (index 2).
Expected property actual[0].Name.LastName to be "yu1" with a length of 3, but "yu" has a length of 2, differs near "u" (index 1).
範例位置
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET