0%

【Unit Test】針對Repository做單元測試 (一)

大部分的專案都會需要跟資料庫溝通,存取資料。雖然說開發的時候往往都有測試資料庫,但測試機料庫也可能因為很多人都在開發存取,導致資料可能會時常受到異動。

單元測試的一大重點就是不管任何人、時、地,都要能測試成功,如果今天用來測試的資料庫可能面臨資料在不確定何時何人會異動的情境下,這可能會讓我們的單元測試雖然邏輯正確,但最後驗證失敗。例如:原本預期A會員權限有效,但因為測試資料庫別人也在使用開發,所以被改成失效,這時候你如果去跑單元測試就會亮起紅燈失敗,直到你去查了後才發現原來是源頭資料被改動了。為了解決這樣的問題,我們最好能創造一個資料庫只給單元測試使用,且能跟著專案走的(不然別人從Git把專案抓下來,沒有資料庫的情況下單元測試全掛掉…..這該如何是好),所以LocalDB就是一個很好的選擇。

第一篇就紀錄一下我如何建立LocalDB、並準備測試資料,這邊大部分都參考
MRKT大師 : 測試專案使用 LocalDB - 使用 Entity Framework 的情境 
MRKT大師 : Dapper - 使用 LINQPad 快速產生相對映 SQL Command 查詢結果的類別
,以及一些公司同事提供的方便小工具,非自己原創XD


第一步、建立專案來擺放LocalDB

建立一般的Library Project

[![](https://4.bp.blogspot.com/-QN2Lgo0WWLM/V5nWU8uV4rI/AAAAAAAAHwg/AjshLUxypfUm3whgktw12ZXq7ApXm3FGgCLcB/s640/1.png)](https://4.bp.blogspot.com/-QN2Lgo0WWLM/V5nWU8uV4rI/AAAAAAAAHwg/AjshLUxypfUm3whgktw12ZXq7ApXm3FGgCLcB/s1600/1.png)

接下在專案底下新增LocalDB

[![](https://2.bp.blogspot.com/-8T2Ln5dZrpo/V5nWwfdpzFI/AAAAAAAAHwk/vk4Ujt5NUeMltYoBoSBlZnMSmv-HD27twCLcB/s640/1.png)](https://2.bp.blogspot.com/-8T2Ln5dZrpo/V5nWwfdpzFI/AAAAAAAAHwk/vk4Ujt5NUeMltYoBoSBlZnMSmv-HD27twCLcB/s1600/1.png)
[![](https://4.bp.blogspot.com/-sfnO0MbA31s/V5rEVI9uucI/AAAAAAAAHw4/dHkcoCnpGTc_4O7I_BY0NKphzT2uP03vACLcB/s1600/1.png)](https://4.bp.blogspot.com/-sfnO0MbA31s/V5rEVI9uucI/AAAAAAAAHw4/dHkcoCnpGTc_4O7I_BY0NKphzT2uP03vACLcB/s1600/1.png)

第二步、幫LocalDB建立Schema (已NorthWind資料庫為例)

基本上SSMS操作差不多,在資料表右鍵 > 新增查詢,然後把Create Employees Table的Script貼過去執行
[![](https://2.bp.blogspot.com/-5ji6J1LhAWQ/V5rJYfKcExI/AAAAAAAAHxI/IFxpZPmJ5skP5CFyB11VLs-iKe0DLSnLACLcB/s1600/1.png)](https://2.bp.blogspot.com/-5ji6J1LhAWQ/V5rJYfKcExI/AAAAAAAAHxI/IFxpZPmJ5skP5CFyB11VLs-iKe0DLSnLACLcB/s1600/1.png)
[![](https://3.bp.blogspot.com/-nqLVzU-QAqQ/V5rJolPPJII/AAAAAAAAHxM/Z99R12JALYgIIAq12h6mZGB4JdVBQl0HQCLcB/s640/1.png)](https://3.bp.blogspot.com/-nqLVzU-QAqQ/V5rJolPPJII/AAAAAAAAHxM/Z99R12JALYgIIAq12h6mZGB4JdVBQl0HQCLcB/s1600/1.png)

第三步、準備測試資料

因為每個測試之間應該都是獨立的,為了避免互相干擾,所以資料通常不會預先倒到LocalDB裡面去,而是在執行每次測試時,動態的將準備好的資料寫進去,每個測試完畢後砍掉資料,這樣不停的重複著。
而一個Table的資料可能會很多很多,如果用手寫成SQL Script應該寫完專案DeadLine也過了,
所以這邊打算將資料匯出到CSV檔案中,然後透過CSVHelper讀出來倒進DB,做法如下:
已下搭配LinqPad使用
```csharp void Main() { // 這邊修改為你要執行的 SQL Command var sqlCommand = @"select top 1 * from Employees"; // 第一個參數填入 SQL Command, 第二個參數輸入要產生的 Class 名稱 this.Connection.DumpClass(sql: sqlCommand.ToString(), className: "Employees").Dump(); } public static class LINQPadExtensions { private static readonly Dictionary TypeAliases = new Dictionary { { 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 NullableTypes = new HashSet { 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(); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72


這支程式的目的是先做出符合Employees對應的Class,方便支後匯出跟匯入資料,執行後應該會看到產出的Class文字檔
<div class="separator" style="clear: both; text-align: center;">[![](https://4.bp.blogspot.com/-lnj9t366iQ4/V5rM--fHASI/AAAAAAAAHxc/fb_na4MSXXgUZTpXFxaxy65YfjGEWs5lgCLcB/s640/1.png)](https://4.bp.blogspot.com/-lnj9t366iQ4/V5rM--fHASI/AAAAAAAAHxc/fb_na4MSXXgUZTpXFxaxy65YfjGEWs5lgCLcB/s1600/1.png)</div>

接著用第一支程式產出的Class貼到第二支程式底下

```csharp
void Main()
{
//修改匯出CSV資料存放的位置
using (var sw = new StreamWriter(@"D:\Employees.csv"))
using (var writer = new CsvWriter(sw))
{
var result = new List<Employees>();
var connectionString = this.Connection.ConnectionString;

using (var conn = new SqlConnection(connectionString))
{
conn.Open();

var sqlCommand = new StringBuilder();
//要匯出哪些資料的SQL SCript
sqlCommand.AppendLine(@"select top 10 * from Employees");
result = conn.Query<Employees>(sqlCommand.ToString())
.ToList();
}
writer.WriteRecords(result);
}
}
//將第一支程式產出的Class貼在這邊!!!

public class Employees
{
public int EmployeeID { get; set; }

public string LastName { get; set; }

public string FirstName { get; set; }

public string Title { get; set; }

public string TitleOfCourtesy { get; set; }

public DateTime? BirthDate { get; set; }

public DateTime? HireDate { get; set; }

public string Address { get; set; }

public string City { get; set; }

public string Region { get; set; }

public string PostalCode { get; set; }

public string Country { get; set; }

public string HomePhone { get; set; }

public string Extension { get; set; }

public byte[] Photo { get; set; }

public string Notes { get; set; }

public int? ReportsTo { get; set; }

public string PhotoPath { get; set; }

}

執行之後,依照你寫的路徑去找到產出的CSV檔,並把他貼到測試專案之中

[![](https://3.bp.blogspot.com/-UxqEWgpQ1Z8/V5rQF-UsS7I/AAAAAAAAHxo/UM-O8j-1vqg4RoZOaC266QtsGuVB742IACLcB/s1600/1.png)](https://3.bp.blogspot.com/-UxqEWgpQ1Z8/V5rQF-UsS7I/AAAAAAAAHxo/UM-O8j-1vqg4RoZOaC266QtsGuVB742IACLcB/s1600/1.png)
將檔案屬性改成如下
[![](https://1.bp.blogspot.com/-5JmcyzIAZrY/V5rQUsVtfsI/AAAAAAAAHxs/Hzn8l6q895IZQ-qdmNZJjzopfvReaDfSwCLcB/s320/1.png)](https://1.bp.blogspot.com/-5JmcyzIAZrY/V5rQUsVtfsI/AAAAAAAAHxs/Hzn8l6q895IZQ-qdmNZJjzopfvReaDfSwCLcB/s1600/1.png)
將測試專案參考放LocalDB的Resource專案,基本上基礎的設置就大功告成了
[![](https://2.bp.blogspot.com/-J3w_UzNgjqI/V5rQv3jUagI/AAAAAAAAHxw/2ZLjd4ZnFlwKx1k4SWnmR42iD4t2NIgzgCLcB/s320/1.png)](https://2.bp.blogspot.com/-J3w_UzNgjqI/V5rQv3jUagI/AAAAAAAAHxw/2ZLjd4ZnFlwKx1k4SWnmR42iD4t2NIgzgCLcB/s1600/1.png)
[![](https://2.bp.blogspot.com/-W22EdOO54UQ/V5rQ4alyKlI/AAAAAAAAHx0/0OsWQ0t7roQiwlxBcctsrV8Nnwkb9J1JgCLcB/s640/1.png)](https://2.bp.blogspot.com/-W22EdOO54UQ/V5rQ4alyKlI/AAAAAAAAHx0/0OsWQ0t7roQiwlxBcctsrV8Nnwkb9J1JgCLcB/s1600/1.png)
[![](https://3.bp.blogspot.com/-0GrZTpNVuYA/V5rRUwzF5EI/AAAAAAAAHx8/EU3rhcWLSFo0HH0UsxyYuEI3czy_nJKWwCLcB/s320/1.png)](https://3.bp.blogspot.com/-0GrZTpNVuYA/V5rRUwzF5EI/AAAAAAAAHx8/EU3rhcWLSFo0HH0UsxyYuEI3czy_nJKWwCLcB/s1600/1.png)
下一篇接著寫Production Code,並針對那段Code去做測試