資料存取層的測試… 真的比較麻煩
有些資料是已經存在於資料庫,例如定期匯入或排程計算的資料結果,如果在程式裡使用 AutoFixture 或是假資料來做測試,有時候會出現一些問題點,所以就會從資料庫直接取得一部份的資料來做為測試資料,匯出的方式有很多種,而這一篇所介紹的方式就是其中一種,我是希望可以盡量使用程式的方式來處理而不是以 Insert Script 的方式來,不過這不是絕對的做法,就看各位習慣什麼方式,或許大家有更好以及更好管理的方法。
LINQPad
首先,這邊的處理完全只會使用到 LINQPad 這個工具,而且應該是限定 Developer 以上的版本(Developer, Premium),因為必須要使用到 Nuget 功能,為何不用 Visual Studio 來處理呢?因為我要能夠快速執行、快速產生結果並且可以馬上驗證匯出結果是否正確,所以就使用 LINQPad。
關於 LINQPad 這個工具,如果還不知道或是不熟悉的話,這個部落格有多篇文章介紹,或是直接前往官網去看詳細的產品說明。
http://kevintsengtw.blogspot.tw/search?q=LINQPad
Nuget Packages
再來就是會使用到的 Nuget Packages,這邊會用到的有 AutoMapper, CsvHelper, Dapper
AutoMapper 的版本就使用目前最新的 5.1.1
CsvHelper,這是用來匯出與讀取匯入的重要套件
Dapper,在這一篇文章裡還不會用到,不過後續的文章裡就會用到
這裡所使用到的測試資料庫為 AdventureWorks2012 (現在已經有 2014 的版本了,一樣也可以使用)
https://msftdbprodsamples.codeplex.com/
產生對映 SQL Command 查詢結果的類別
在之前的文章「Dapper - 使用 LINQPad 快速產生相對映 SQL Command 查詢結果的類別」裡有介紹過如何產生對映 SQL Command 查詢結果的類別,這邊就會用到,如下所示,要產生對映 Production.Product 的類別
void Main()
{
// 這邊修改為你要執行的 SQL Command
var sqlCommand = @"select top 1 * from [Production].[Product];";
// 在 DumpClass 方法裡放 SQLCommand 和 Class 名稱
this.Connection.DumpClass(sqlCommand.ToString(), "Product").Dump();
}
public static class LINQPadExtensions
{
private static readonly Dictionary<Type, string> TypeAliases = new Dictionary<Type, string> {
{ typeof(int), "int" },
{ typeof(short), "short" },
{ typeof(byte), "byte" },
{ typeof(byte[]), "byte[]" },
{ typeof(long), "long" },
{ typeof(double), "double" },
{ typeof(decimal), "decimal" },
{ typeof(float), "float" },
{ typeof(bool), "bool" },
{ typeof(string), "string" }
};
private static readonly HashSet<Type> NullableTypes = new HashSet<Type> {
typeof(int),
typeof(short),
typeof(long),
typeof(double),
typeof(decimal),
typeof(float),
typeof(bool),
typeof(DateTime)
};
public static string DumpClass(this IDbConnection connection, string sql, string className = "Info")
{
if(connection.State != ConnectionState.Open)
{
connection.Open();
}
var cmd = connection.CreateCommand();
cmd.CommandText = sql;
var reader = cmd.ExecuteReader();
var builder = new StringBuilder();
do
{
if(reader.FieldCount <= 1) continue;
builder.AppendFormat("public class {0}{1}", className, Environment.NewLine);
builder.AppendLine("{");
var schema = reader.GetSchemaTable();
foreach (DataRow row in schema.Rows)
{
var type = (Type)row["DataType"];
var name = TypeAliases.ContainsKey(type) ? TypeAliases[type] : type.Name;
var isNullable = (bool)row["AllowDBNull"] && NullableTypes.Contains(type);
var collumnName = (string)row["ColumnName"];
builder.AppendLine(string.Format("\tpublic {0}{1} {2} {{ get; set; }}", name, isNullable ? "?" : string.Empty, collumnName));
builder.AppendLine();
}
builder.AppendLine("}");
builder.AppendLine();
} while(reader.NextResult());
return builder.ToString();
}
}
執行結果
public class Product
{
public int ProductID { get; set; }
public string Name { get; set; }
public string ProductNumber { get; set; }
public bool MakeFlag { get; set; }
public bool FinishedGoodsFlag { get; set; }
public string Color { get; set; }
public short SafetyStockLevel { get; set; }
public short ReorderPoint { get; set; }
public decimal StandardCost { get; set; }
public decimal ListPrice { get; set; }
public string Size { get; set; }
public string SizeUnitMeasureCode { get; set; }
public string WeightUnitMeasureCode { get; set; }
public decimal? Weight { get; set; }
public int DaysToManufacture { get; set; }
public string ProductLine { get; set; }
public string Class { get; set; }
public string Style { get; set; }
public int? ProductSubcategoryID { get; set; }
public int? ProductModelID { get; set; }
public DateTime SellStartDate { get; set; }
public DateTime? SellEndDate { get; set; }
public DateTime? DiscontinuedDate { get; set; }
public Guid rowguid { get; set; }
public DateTime ModifiedDate { get; set; }
}
再來就是要使用 CsvHelper 與 AutoMapper 將 Production.Product 的所有資料給匯出為 CSV 檔案,有關於 CsvHelper 與 AutoMapper 的相關資訊,在這個部落格也提供了許多,雖然當初介紹 AutoMapper 時的版本是 3.x 而現在是 5.x,在設定與部分的使用是有些差異,但基本的使用方式都是差不多的,不過還是以 AutoMapper 的官方版本為準。
CsvHelper
http://kevintsengtw.blogspot.tw/search?q=CsvHelper
https://github.com/JoshClose/CsvHelper
AutoMapper
http://kevintsengtw.blogspot.tw/search/label/AutoMapper
https://github.com/AutoMapper/AutoMapper/wiki
執行的程式內容如下:
void Main()
{
using (var sw = new StreamWriter(@"D:\Product_Data.csv"))
using (var writer = new CsvWriter(sw))
{
var source = this.Products.OrderByDescending(x => x.ProductID).ToList();
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Product, Product_Data>();
});
var mapper = config.CreateMapper();
List<Product_Data> records = mapper.Map<List<Product>, List<Product_Data>>(source);
writer.WriteRecords(records);
}
}
public class Product_Data
{
public int ProductID { get; set; }
public string Name { get; set; }
public string ProductNumber { get; set; }
public bool MakeFlag { get; set; }
public bool FinishedGoodsFlag { get; set; }
public string Color { get; set; }
public short SafetyStockLevel { get; set; }
public short ReorderPoint { get; set; }
public decimal StandardCost { get; set; }
public decimal ListPrice { get; set; }
public string Size { get; set; }
public string SizeUnitMeasureCode { get; set; }
public string WeightUnitMeasureCode { get; set; }
public decimal? Weight { get; set; }
public int DaysToManufacture { get; set; }
public string ProductLine { get; set; }
public string Class { get; set; }
public string Style { get; set; }
public int? ProductSubcategoryID { get; set; }
public int? ProductModelID { get; set; }
public DateTime SellStartDate { get; set; }
public DateTime? SellEndDate { get; set; }
public DateTime? DiscontinuedDate { get; set; }
public Guid rowguid { get; set; }
public DateTime ModifiedDate { get; set; }
}
執行結果
這個匯出的結果看起一切都很正常,似乎這一篇所要講的就是這麼簡單的事情…
不過呢
我既然會想要寫,就一定會有事情不單純,將 CSV 再往右邊移,就可以看到問題點了
看出來了嗎?
資料表裡的欄位型別為 datetime 的資料在匯出的時候,因為使用者電腦的語系而在匯出時會帶有語系的AM、PM,我們所看到的就是上午、下午,這個在做匯入的時候會出現問題,因為無法直接轉換成 DateTime 的型別,所以再做的時候就需要動一點手腳啦
使用 AutoMapper 處理
首先把匯出要用的類別改名為 Product_Data,這個類別之後在做資料存取層單元測試時就會用到,這邊就先改名,然後將 DateTime 或 DateTime? 都改為 string 型別,
public class Product_Data
{
public int ProductID { get; set; }
public string Name { get; set; }
public string ProductNumber { get; set; }
public bool MakeFlag { get; set; }
public bool FinishedGoodsFlag { get; set; }
public string Color { get; set; }
public short SafetyStockLevel { get; set; }
public short ReorderPoint { get; set; }
public decimal StandardCost { get; set; }
public decimal ListPrice { get; set; }
public string Size { get; set; }
public string SizeUnitMeasureCode { get; set; }
public string WeightUnitMeasureCode { get; set; }
public decimal? Weight { get; set; }
public int DaysToManufacture { get; set; }
public string ProductLine { get; set; }
public string Class { get; set; }
public string Style { get; set; }
public int? ProductSubcategoryID { get; set; }
public int? ProductModelID { get; set; }
public string SellStartDate { get; set; }
public string SellEndDate { get; set; }
public string DiscontinuedDate { get; set; }
public Guid rowguid { get; set; }
public string ModifiedDate { get; set; }
}
再來就是要將 datetime 或 datetime? 的資料給轉換為一般的日期字串的格式「yyyy-MM-dd HH:mm:ss」,如下:
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Product, Product_Data>()
.ForMember(d => d.SellStartDate,
o => o.MapFrom(s => s.SellStartDate.ToString("yyyy-MM-dd HH:mm:ss")))
.ForMember(d => d.SellEndDate,
o => o.MapFrom(s => s.SellEndDate.HasValue
? s.SellEndDate.Value.ToString("yyyy-MM-dd HH:mm:ss")
: ""))
.ForMember(d => d.DiscontinuedDate,
o => o.MapFrom(s => s.DiscontinuedDate.HasValue
? s.DiscontinuedDate.Value.ToString("yyyy-MM-dd HH:mm:ss")
: ""))
.ForMember(d => d.ModifiedDate,
o => o.MapFrom(s => s.ModifiedDate.ToString("yyyy-MM-dd HH:mm:ss")));
});
修改後的完整程式:
void Main()
{
using (var sw = new StreamWriter(@"D:\Product_Data.csv"))
using (var writer = new CsvWriter(sw))
{
var source = this.Products.OrderByDescending(x => x.ProductID).ToList();
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Product, Product_Data>()
.ForMember(d => d.SellStartDate,
o => o.MapFrom(s => s.SellStartDate.ToString("yyyy-MM-dd HH:mm:ss")))
.ForMember(d => d.SellEndDate,
o => o.MapFrom(s => s.SellEndDate.HasValue
? s.SellEndDate.Value.ToString("yyyy-MM-dd HH:mm:ss")
: ""))
.ForMember(d => d.DiscontinuedDate,
o => o.MapFrom(s => s.DiscontinuedDate.HasValue
? s.DiscontinuedDate.Value.ToString("yyyy-MM-dd HH:mm:ss")
: ""))
.ForMember(d => d.ModifiedDate,
o => o.MapFrom(s => s.ModifiedDate.ToString("yyyy-MM-dd HH:mm:ss")));
});
var mapper = config.CreateMapper();
List<Product_Data> records = mapper.Map<List<Product>, List<Product_Data>>(source);
writer.WriteRecords(records);
}
}
public class Product_Data
{
public int ProductID { get; set; }
public string Name { get; set; }
public string ProductNumber { get; set; }
public bool MakeFlag { get; set; }
public bool FinishedGoodsFlag { get; set; }
public string Color { get; set; }
public short SafetyStockLevel { get; set; }
public short ReorderPoint { get; set; }
public decimal StandardCost { get; set; }
public decimal ListPrice { get; set; }
public string Size { get; set; }
public string SizeUnitMeasureCode { get; set; }
public string WeightUnitMeasureCode { get; set; }
public decimal? Weight { get; set; }
public int DaysToManufacture { get; set; }
public string ProductLine { get; set; }
public string Class { get; set; }
public string Style { get; set; }
public int? ProductSubcategoryID { get; set; }
public int? ProductModelID { get; set; }
public string SellStartDate { get; set; }
public string SellEndDate { get; set; }
public string DiscontinuedDate { get; set; }
public Guid rowguid { get; set; }
public string ModifiedDate { get; set; }
}
匯出的 CSV 檔案
讀取 CSV 檔案
匯出 CSV 之後要再做一次驗證,以確保匯出的資料在之後的使用都能正確,所以使用 CsvHelper 讀取 CSV 檔案
完整的程式如下:
void Main()
{
using (var sr = new StreamReader(@"d:\Product_Data.csv"))
using (var reader = new CsvReader(sr))
{
var records = reader.GetRecords<Product_Data>();
records.Dump();
}
}
public class Product_Data
{
public int ProductID { get; set; }
public string Name { get; set; }
public string ProductNumber { get; set; }
public bool MakeFlag { get; set; }
public bool FinishedGoodsFlag { get; set; }
public string Color { get; set; }
public short SafetyStockLevel { get; set; }
public short ReorderPoint { get; set; }
public decimal StandardCost { get; set; }
public decimal ListPrice { get; set; }
public string Size { get; set; }
public string SizeUnitMeasureCode { get; set; }
public string WeightUnitMeasureCode { get; set; }
public decimal? Weight { get; set; }
public int DaysToManufacture { get; set; }
public string ProductLine { get; set; }
public string Class { get; set; }
public string Style { get; set; }
public int? ProductSubcategoryID { get; set; }
public int? ProductModelID { get; set; }
public string SellStartDate { get; set; }
public string SellEndDate { get; set; }
public string DiscontinuedDate { get; set; }
public Guid rowguid { get; set; }
public string ModifiedDate { get; set; }
}
執行結果
以上這些只是在做測試資料的其中一種方式,這邊還沒有講到後續在單元測試裡的使用方式,不過那是之後講到資料存取層單元測試時才會看到,所以這篇就先到此為止。
以上
純粹是在寫興趣的,用寫程式、寫文章來抒解工作壓力