輸出測試用資料的 CSV 檔案 - 使用 LINQPad, AutoMapper, CsvHelper

資料存取層的測試… 真的比較麻煩

有些資料是已經存在於資料庫,例如定期匯入或排程計算的資料結果,如果在程式裡使用 AutoFixture 或是假資料來做測試,有時候會出現一些問題點,所以就會從資料庫直接取得一部份的資料來做為測試資料,匯出的方式有很多種,而這一篇所介紹的方式就是其中一種,我是希望可以盡量使用程式的方式來處理而不是以 Insert Script 的方式來,不過這不是絕對的做法,就看各位習慣什麼方式,或許大家有更好以及更好管理的方法。

LINQPad

首先,這邊的處理完全只會使用到 LINQPad 這個工具,而且應該是限定 Developer 以上的版本(Developer, Premium),因為必須要使用到 Nuget 功能,為何不用 Visual Studio 來處理呢?因為我要能夠快速執行、快速產生結果並且可以馬上驗證匯出結果是否正確,所以就使用 LINQPad。

關於 LINQPad 這個工具,如果還不知道或是不熟悉的話,這個部落格有多篇文章介紹,或是直接前往官網去看詳細的產品說明。

http://kevintsengtw.blogspot.tw/search?q=LINQPad

LINQPad 官網LINQPad 各版本比較

image

 

Nuget Packages

再來就是會使用到的 Nuget Packages,這邊會用到的有 AutoMapper, CsvHelper, Dapper

AutoMapper 的版本就使用目前最新的 5.1.1

image

CsvHelper,這是用來匯出與讀取匯入的重要套件

image

Dapper,在這一篇文章裡還不會用到,不過後續的文章裡就會用到

image

 

這裡所使用到的測試資料庫為 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; }
 
}

執行結果

image

這個匯出的結果看起一切都很正常,似乎這一篇所要講的就是這麼簡單的事情…

不過呢

我既然會想要寫,就一定會有事情不單純,將 CSV 再往右邊移,就可以看到問題點了

image

看出來了嗎?

資料表裡的欄位型別為 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 檔案

image

 

讀取 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; }
 
}

執行結果

image

image

 

 


以上這些只是在做測試資料的其中一種方式,這邊還沒有講到後續在單元測試裡的使用方式,不過那是之後講到資料存取層單元測試時才會看到,所以這篇就先到此為止。

 

以上

純粹是在寫興趣的,用寫程式、寫文章來抒解工作壓力