去年有寫過一篇關於在測試專案裡使用 LocalDB 的文章「測試專案使用 LocalDB - 使用 Entity Framework 的情境」,的確是一個可以執行的做法,但是在多人開發的專案或是多分支開發的專案裡,這樣的做法會帶來一些問題,另外在一些環境裡執行單元測試時會遇到權限不足的問題(例如登入使用者並不具有 Administrator 權限時)會造成測試執行失敗,以致於最後我不在使用那種方式。
既然不用了也還是必須要有替代的方式去完成 Repository 的單元測試,於是我之後嘗試使用指令碼的方式在執行 Repository 測試前建立 LocalDB、執行測試後再移除 LocalDB,這樣的做法也真的可行,不過卻相當不穩定,當測試全部都執行正確時是不會有問題,但是一旦當一個錯誤發生問題時,建立的 LocalDB instance 就會被鎖住而無法正確釋放、移除,這樣的問題一直困擾著我很久。
接下來的幾篇文章將會介紹一個穩定而且不會發生測試失敗就把 LocalDB 鎖住的方式,藉助 Entity Framework 所提供的類別與方法來完成 Repository 測試的 LocalDB 建立與移除,讓我們能夠以更為簡易的方式來完成 Repositoy 的單元測試。
接著幾篇文章將會使用到過去以來一直到前不久的文章裡面的內容,所以請各位務必要先熟悉,我就不再重複的交代,如下:
- Dapper 練習題 - 新增多筆或大量資料
- Dapper - 使用 LINQPad 快速產生相對映 SQL Command 查詢結果的類別
- 使用 LINQPad 快速產生 Table 的 Insert Script
- 輸出測試用資料的 CSV 檔案 - 使用 LINQPad, AutoMapper, CsvHelper
- 編寫單元測試時的好用輔助套件 - Fluent Assertions
程式說明
先直接看要測試的專案與程式內容,
這是一個很簡單的程式,使用的資料庫是「Northwind」,建立一個 CustomerRepository 類別,透過 Dapper 去讀取 Customers 資料庫表格裡的資料,
CustomerModel
namespace NorthwindRepository.Models{public class CustomerModel
{public string CustomerID { get; set; }
public string CompanyName { get; set; }
public string ContactName { get; set; }
public string ContactTitle { 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 Phone { get; set; }
public string Fax { get; set; }
}
}
介面 ICustomerRepository
using System.Collections.Generic;using NorthwindRepository.Models;namespace NorthwindRepository.Interface{public interface ICustomerRepository
{List<CustomerModel> GetAll();
CustomerModel Get(string customerId);}
}
實作 CustomerRepository
using System;using System.Collections.Generic;using System.Linq;using Dapper;using NorthwindRepository.Database;using NorthwindRepository.Interface;using NorthwindRepository.Models;namespace NorthwindRepository.Implements{ /// <summary> /// CustomerRepository /// </summary>public class CustomerRepository : ICustomerRepository
{ private IDatabaseConnectionFactory DatabaseConnectionFactory { get; } public CustomerRepository(IDatabaseConnectionFactory factory) { this.DatabaseConnectionFactory = factory;}
/// <summary> /// 取得全部資料 /// </summary> /// <returns></returns> public List<CustomerModel> GetAll() { var dbConnection = this.DatabaseConnectionFactory.Create(); using (var conn = dbConnection) { var sqlCommand = "select * from dbo.Customers";var result = conn.Query<CustomerModel>(sqlCommand);
return result.ToList();}
}
/// <summary> /// 以 CustomerID 取得指定資料 /// </summary> /// <param name="customerId"></param> /// <returns></returns>public CustomerModel Get(string customerId)
{if (string.IsNullOrWhiteSpace(customerId))
{throw new ArgumentNullException(nameof(customerId));
}
var dbConnection = this.DatabaseConnectionFactory.Create(); using (var conn = dbConnection) { var sqlCommand = "select * from dbo.Customers where CustomerID = @CustomerID";var result = conn.QueryFirstOrDefault<CustomerModel>(
sqlCommand,
new {CustomerID = customerId
});
return result;}
}
}
}
一般寫 Repository 程式內容時,不管是使用傳統的 ADO.NET 或 Dapper,都不應該在程式裡直接去建立 DbConnection,我這邊的做法是建立一個 DatabaseConnectionFactory,程式要取得 DbConnection 就必須要透過 DatabaseConnectionFactory 建立。
介面 IDatabaseConnectionFactory
using System.Data;namespace NorthwindRepository.Database{public interface IDatabaseConnectionFactory
{IDbConnection Create();
}
}
實作 DatabaseConnectionFactory
using System;using System.Data;using System.Data.SqlClient;namespace NorthwindRepository.Database{public class DatabaseConnectionFactory : IDatabaseConnectionFactory
{private readonly string _connectionString;
public DatabaseConnectionFactory(string connectionString)
{if (string.IsNullOrWhiteSpace(connectionString))
{throw new ArgumentNullException(nameof(connectionString));
}
this._connectionString = connectionString;}
/// <summary> /// Create DbConnection /// </summary> /// <returns></returns> public IDbConnection Create() { var sqlConnection = new SqlConnection(_connectionString); return sqlConnection;}
}
}
準備測試資料
這邊我先使用從原始資料庫匯出資料到 CSV 檔案的方式準備測試資料,至於怎麼準備、怎麼執行與匯出,就請先看過這篇文章的內容「輸出測試用資料的 CSV 檔案 - 使用 LINQPad, AutoMapper, CsvHelper」
測試資料與相關測試資料庫的一些類別,我是另外建立一個「TestResources」類別庫專案來放置,與 RepositoryTests 單元測試專按做區隔,讓單元測試專案盡量只有單元測試類別而已。
SQL Scripts
建立 Table 的 Script 可以直接在 SSMS 裡去產生
而 Insert value Script 則是可以看「使用 LINQPad 快速產生 Table 的 Insert Script」這篇文章
程式內容
namespace NorthwindRepository.TestResources.TableSchemas{public class Northwind_Tables
{public static string Customers_Create()
{ var sqlCommand = new System.Text.StringBuilder(); sqlCommand.AppendLine(@"IF OBJECT_ID('dbo.Customers', 'U') IS NOT NULL"); sqlCommand.AppendLine(@" DROP TABLE dbo.Customers; "); sqlCommand.AppendLine(@""); sqlCommand.AppendLine(@"CREATE TABLE [dbo].[Customers]("); sqlCommand.AppendLine(@" [CustomerID] [nchar](5) NOT NULL,"); sqlCommand.AppendLine(@" [CompanyName] [nvarchar](40) NOT NULL,"); sqlCommand.AppendLine(@" [ContactName] [nvarchar](30) NULL,"); sqlCommand.AppendLine(@" [ContactTitle] [nvarchar](30) NULL,"); sqlCommand.AppendLine(@" [Address] [nvarchar](60) NULL,"); sqlCommand.AppendLine(@" [City] [nvarchar](15) NULL,"); sqlCommand.AppendLine(@" [Region] [nvarchar](15) NULL,"); sqlCommand.AppendLine(@" [PostalCode] [nvarchar](10) NULL,"); sqlCommand.AppendLine(@" [Country] [nvarchar](15) NULL,"); sqlCommand.AppendLine(@" [Phone] [nvarchar](24) NULL,"); sqlCommand.AppendLine(@" [Fax] [nvarchar](24) NULL,"); sqlCommand.AppendLine(@" CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED "); sqlCommand.AppendLine(@"("); sqlCommand.AppendLine(@" [CustomerID] ASC"); sqlCommand.AppendLine(@"));"); return sqlCommand.ToString();}
public static string Customers_Insert()
{ var sqlCommand = new System.Text.StringBuilder(371); sqlCommand.AppendLine(@"INSERT INTO [dbo].[Customers]"); sqlCommand.AppendLine(@"("); sqlCommand.AppendLine(@" [CustomerID],"); sqlCommand.AppendLine(@" [CompanyName],"); sqlCommand.AppendLine(@" [ContactName],"); sqlCommand.AppendLine(@" [ContactTitle],"); sqlCommand.AppendLine(@" [Address],"); sqlCommand.AppendLine(@" [City],"); sqlCommand.AppendLine(@" [Region],"); sqlCommand.AppendLine(@" [PostalCode],"); sqlCommand.AppendLine(@" [Country],"); sqlCommand.AppendLine(@" [Phone],"); sqlCommand.AppendLine(@" [Fax]"); sqlCommand.AppendLine(@")"); sqlCommand.AppendLine(@"VALUES"); sqlCommand.AppendLine(@"("); sqlCommand.AppendLine(@" @CustomerID,"); sqlCommand.AppendLine(@" @CompanyName,"); sqlCommand.AppendLine(@" @ContactName,"); sqlCommand.AppendLine(@" @ContactTitle,"); sqlCommand.AppendLine(@" @Address,"); sqlCommand.AppendLine(@" @City,"); sqlCommand.AppendLine(@" @Region,"); sqlCommand.AppendLine(@" @PostalCode,"); sqlCommand.AppendLine(@" @Country,"); sqlCommand.AppendLine(@" @Phone,"); sqlCommand.AppendLine(@" @Fax"); sqlCommand.AppendLine(@");"); return sqlCommand.ToString();}
}
}
有建立 Table 與 Insert Value 的 Script 內容,還要另外準備移除 Table 以及清空 Table 內容的 Script,每個單元測試類別與單元測試方法都是獨立的,不應該有順序性或是相依性,所以在一個單元測試類別裡所建立且匯入測試資料的測試環境,都必須要在該單元測試類別完成後要做移除 Table 的處理。
千萬不要想其他單元測試類別會用到而去建立一個共用的測試環境,這相當不建議這麼做,還是那句話,你要確保單元測試的獨立與隔離,讓測試之間不會有影響與相依,否則測試程式都亂七八糟的,那麼這樣的測試還有什麼可信度呢?
移除與清空 Table 的 Script 程式
using System;namespace NorthwindRepository.TestResources.TableSchemas{ /// <summary> /// Class TableCommands. /// </summary>public static class TableCommands
{ /// <summary> /// Drops the table. /// </summary> /// <param name="tableName">Name of the table.</param> /// <returns>System.String.</returns> /// <exception cref="ArgumentNullException">please input tableName.</exception>public static string DropTable(string tableName)
{if (string.IsNullOrWhiteSpace(tableName))
{throw new ArgumentNullException(nameof(tableName), "please input tableName.");
}
var sqlCommand = new System.Text.StringBuilder(); sqlCommand.AppendLine($@"IF OBJECT_ID('dbo.{tableName}', 'U') IS NOT NULL"); sqlCommand.AppendLine($@" DROP TABLE dbo.{tableName}; "); return sqlCommand.ToString();}
/// <summary> /// Truncates the table. /// </summary> /// <param name="tableName">Name of the table.</param> /// <returns>System.String.</returns> /// <exception cref="ArgumentNullException">please input tableName.</exception>public static string TruncateTable(string tableName)
{if (string.IsNullOrWhiteSpace(tableName))
{throw new ArgumentNullException(nameof(tableName), "please input tableName.");
}
var sqlCommand = new System.Text.StringBuilder(); sqlCommand.AppendLine($@"IF OBJECT_ID('dbo.{tableName}', 'U') IS NOT NULL"); sqlCommand.AppendLine($@" TRUNCATE TABLE dbo.{tableName}; "); return sqlCommand.ToString();}
}
}
測試相關專案使用的 NuGet Packages
在進入到下一篇說明怎麼透過 Entity Framework 建立與移除 LocalDB 之前,讓大家知道在 TestResources 與 RepositoryTests 專案裡所使用的 NuGet Packages,
TestResources 專案會需要安裝 Entity Framework 6.1.3
RepositoryTests 專案會安裝以下的 Packages
這一篇就先到這裡,因為接下來的篇幅會比較長一點,所以分成兩篇來做說明。
這次介紹的內容是我正在使用方式,同時也是我公司開發團隊也在使用的方式(這兩年多來所分享的文章內容都是有公司開發團隊的實證經驗),通常我都是自己先嘗試各種的方式,找出最適合的做法並且實際應用在我的專案裡,如果運行的好並且也有不錯的成效,接下來我就會在公司裡透過教育訓練的方式介紹給同事,然後開始在各開發團隊裡去導入使用,如果有任何的問題或反饋,我就會再做改進與調整,最後就會寫成文章分享出來。
以上
沒有留言:
張貼留言