之前曾經試過以程式碼建立 LocalDB 的方式,例如使用指令碼的方式在執行 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 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
這一篇就先到這裡,因為接下來的篇幅會比較長一點,所以分成兩篇來做說明。
這次介紹的內容是我正在使用方式,同時也是我公司開發團隊也在使用的方式(這兩年多來所分享的文章內容都是有公司開發團隊的實證經驗),通常我都是自己先嘗試各種的方式,找出最適合的做法並且實際應用在我的專案裡,如果運行的好並且也有不錯的成效,接下來我就會在公司裡透過教育訓練的方式介紹給同事,然後開始在各開發團隊裡去導入使用,如果有任何的問題或反饋,我就會再做改進與調整,最後就會寫成文章分享出來。
以上
純粹是在寫興趣的,用寫程式、寫文章來抒解工作壓力