2016年8月31日 星期三

輸出測試用資料的 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

 


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

 

以上

沒有留言:

張貼留言

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。