本文

初始化與清除使用的 Attribute

上一篇我們介紹到使用改變時間的用法,但因為其中一個測試改變了時間可能會影響到其他單元測試,這時候就有幾個 Attribute 可以派上用場。

Attribute 用法 頻率
TestInitialize() 在執行每一項測試之前,會先執行 TestInitialize 的程式碼。 每執行一個 TestMethod 會觸發一次。
TestCleanup() 在執行每一項測試之後,會執行 TestCleanup 的程式碼。 每執行一個 TestMethod 會觸發一次。
ClassInitialize() 在執行該類別的測試之前,會先執行 ClassInitialize 的程式碼。 以測試類別為單位執行一次。
ClassCleanup() 在執行該類別的測試之後,會執行 ClassCleanup 的程式碼。 以測試類別為單位執行一次。
AssemblyInitialize() 在執行所有類別的測試之前,會先執行 AssemblyInitialize 的程式碼。 以測試專案為單位執行一次。
AssemblyCleanup() 在執行該類別的測試之後,會執行 AssemblyCleanup 的程式碼。 以測試專案為單位執行一次。

練習:
如題為了避免我們改變時間進而影響到其他單元測試,我們在執行每一個測試前後都會初始化一次時間。

        [TestInitialize]
        public void TestInitial()
        {
            SystemTime.SetToday = () => DateTime.Today;
        }

        [TestCleanup]
        public void TestClean()
        {
            SystemTime.SetToday = () => DateTime.Today;
        }

隔離物件

測試的時候我們會需要隔離要測試的物件,如果直接相依物件會產生一些不必要的問題導致測試結果可能有誤。

相依物件 可能發生情況
資料庫 直接相依資料庫的情況下,如果有異動到資料,每一次都要還原資料。
外部服務 會因為網路連線狀態而直接影響測試執行的速度。
特殊商業邏輯 無法進行測試,例如:亂數資料、加密資料等。

所以為了解決這個問題,就可以很簡單的利用先前教學所使用的依賴注入方式,透過介面注入抽換實體,那麼我在在測試的地方,就能直接做一個測試用的實體出來進行模擬測試。

我們先建立一個示範用的 Common 專案裡面只放了一個共用的Model。

    public class AccountModel
    {
        public string UserName { get; set; }

        public string Account { get; set; }
    }

接下來建立一個示範用的 Repository 專案,並建立一個假的取資料方法。

    public interface IRepository
    {
        /// <summary>
        /// 取得帳號
        /// </summary>
        /// <param name="account"></param>
        /// <returns></returns>
        AccountModel GetAccount(string account);
    }
    
    public class Repository : IRepository
    {
        public AccountModel GetAccount(string account)
        {
            //這邊應該是要跟DB拿取資料,現在先假裝一下請大家不要這麼認真。
            var data = new List<AccountModel>
            {
                new AccountModel
                {
                    Account = "123",
                    UserName = "Sian"
                },
                new AccountModel
                {
                    Account = "456",
                    UserName = "Tom"
                }
            };

            return data.Where(x => x.Account == account).FirstOrDefault();
        }
    }

接著建立 Service 的介面並實作,透過注入的方式跟 Repository 拿取資料。

    public interface IService
    {
        /// <summary>
        /// 取得帳號
        /// </summary>
        /// <param name="account"></param>
        /// <returns></returns>
        AccountModel GetAccount(string account);
    }
    
    public class Service : IService
    {
        private readonly IRepository _repository;

        public Service(
            IRepository repository)
        {
            this._repository = repository;
        }

        public AccountModel GetAccount(string account)
        {
            if (string.IsNullOrWhiteSpace(account))
            {
                throw new ArgumentNullException();
            }

            var data = this._repository.GetAccount(account);

            return data;
        }
    }

接下來我們在測試的地方,就能使用繼承介面的方式抽換實體物件,製造一個假的物件供我們測試使用。

    [TestClass()]
    public class ServiceTests
    {
        [TestMethod()]
        [Owner("Sian")]
        [TestCategory("ServiceTest")]
        [TestProperty("ServiceTests", "GetAccount")]
        public void GetAccount_傳入存在的帳號_應回傳正確帳號資訊()
        {
            //arrange
            var account = "123";
            var expect = new AccountModel
            {
                Account = "123",
                UserName = "test"
            };
            var service = new AccountService();

            //act
            var actual = service.GetAccount(account);
            //assert

            actual.Should().BeEquivalentTo(expect);
        }
    }

    public class AccountService : IService
    {
        public AccountModel GetAccount(string account)
        {
            return new AccountModel
            {
                Account = "123",
                UserName = "test"
            };
        }
    }

結果

現在我們知道了如何隔離物件,但如果每一次都要我們手動寫一個假實體來進行驗證程式是否正確,那還沒寫完測試已經累死了,這時候就要介紹一個好用的工具了。

NSubstitute

首先我們先到 Nuget 安裝套件。

接著修改我們的單元測試使用 NSubstitute 建立假實體。

        [TestMethod()]
        [Owner("Sian")]
        [TestCategory("ServiceTest")]
        [TestProperty("ServiceTests", "GetAccount")]
        public void GetAccount_傳入存在的帳號_應回傳正確帳號資訊()
        {
            //arrange
            var account = "123";
            var expect = new AccountModel
            {
                Account = "123",
                UserName = "test"
            };
            var service = Substitute.For<IService>();
            service.GetAccount(account).Returns(expect);

            //act
            var actual = service.GetAccount(account);

            //assert
            actual.Should().NotBeNull();
            actual.Should().BeEquivalentTo(expect);
        }

結果:

後續我們會在實作API中使用更多種方式,若對 NSubstitute 操作想先認識更多的可參考:

參考連結