Repository 測試使用 LocalDB - Part.1

之前曾經試過以程式碼建立 LocalDB 的方式,例如使用指令碼的方式在執行 Repository 測試前建立 LocalDB、執行測試後再移除 LocalDB,這樣的做法也真的可行,不過卻相當不穩定,當測試全部都執行正確時是不會有問題,但是一旦當一個錯誤發生問題時,建立的 LocalDB instance 就會被鎖住而無法正確釋放、移除,這樣的問題一直困擾著我很久。

接下來的幾篇文章將會介紹一個穩定而且不會發生測試失敗就把 LocalDB 鎖住的方式,藉助 Entity Framework 所提供的類別與方法來完成 Repository 測試的 LocalDB 建立與移除,讓我們能夠以更為簡易的方式來完成 Repositoy 的單元測試。

接著幾篇文章將會使用到過去以來一直到前不久的文章裡面的內容,所以請各位務必要先熟悉,我就不再重複的交代,如下:

 

程式說明

先直接看要測試的專案與程式內容,

image

這是一個很簡單的程式,使用的資料庫是「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

image

測試資料與相關測試資料庫的一些類別,我是另外建立一個「TestResources」類別庫專案來放置,與 RepositoryTests 單元測試專按做區隔,讓單元測試專案盡量只有單元測試類別而已。

 

SQL Scripts

建立 Table 的 Script 可以直接在 SSMS 裡去產生

image

而 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

image

RepositoryTests 專案會安裝以下的 Packages

image

 

 


這一篇就先到這裡,因為接下來的篇幅會比較長一點,所以分成兩篇來做說明。

這次介紹的內容是我正在使用方式,同時也是我公司開發團隊也在使用的方式(這兩年多來所分享的文章內容都是有公司開發團隊的實證經驗),通常我都是自己先嘗試各種的方式,找出最適合的做法並且實際應用在我的專案裡,如果運行的好並且也有不錯的成效,接下來我就會在公司裡透過教育訓練的方式介紹給同事,然後開始在各開發團隊裡去導入使用,如果有任何的問題或反饋,我就會再做改進與調整,最後就會寫成文章分享出來。

 

以上

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