前言

我們已經介紹了基本寫單元測試的方法,接下來我們就可以回頭拿先前製作的 API 當作練習,補上該有的測試。

本文

首先我們建立一個放置測試專案的資料夾。

接著建立完測試專案。

2021/02/21更新 原本是只想開一個測試專案把 Repository 跟 Service 的測試都放在一起後來想想不太對。 更新後的專案結構如下。

因為是要對 Service 層進行測試,記得要加入專案參考。

接下來先把基礎測試專案需要用到的東西準備好。

        [TestClass]
    public class AccountServiceTest
    {
        private IAccountRepository _accountRepository;
        private IMapper _mapper
        {
            get
            {
                var config = new MapperConfiguration(options => { options.AddProfile<ServiceProfile>(); });
                return config.CreateMapper();
            }
        }

        [TestInitialize]
        public void TestInitialize()
        {
            this._accountRepository = Substitute.For<IAccountRepository>();
        }

        private AccountService GetSystemUnderTest()
        {
            var sut = new AccountService(this._accountRepository,this._mapper);
            return sut;
        }

        [TestMethod]
        public void TestMethod1()
        {
        }
    }

這邊需要特別注意的地方在 AutoMapper 的地方,我們並不需要製作一個假的 AutoMapper,所以可以直接使用原本方法。

接下來就可以開始寫單元測試了。

AddAccount(新增帳號)

1、有四個必填欄位需要確認。

        [TestMethod]
        [Owner("Sian")]
        [TestCategory("AccountServiceTest")]
        [TestProperty("AccountServiceTest", "AddAccount")]
        public void AddAccount_Account為空_應回傳Exception()
        {
            //assert
            var sut = this.GetSystemUnderTest();
            var info = new AccountInfoModel
            {
                Account = string.Empty,
                Email = "test@gmail.com",
                Password = "123456",
                Phone = "0917888444"
            };
            //act
            Action actual = () => sut.AddAccount(info);

            //arrange
            actual.Should().Throw<Exception>()
                .Which.Message.Contains("請檢查輸入欄位,缺一不可!");
        }

        [TestMethod]
        [Owner("Sian")]
        [TestCategory("AccountServiceTest")]
        [TestProperty("AccountServiceTest", "AddAccount")]
        public void AddAccount_Email為空_應回傳Exception()
        {
            //assert
            var sut = this.GetSystemUnderTest();
            var info = new AccountInfoModel
            {
                Account = "test",
                Email = string.Empty,
                Password = "123456",
                Phone = "0917888444"
            };
            //act
            Action actual = () => sut.AddAccount(info);

            //arrange
            actual.Should().Throw<Exception>()
                .Which.Message.Contains("請檢查輸入欄位,缺一不可!");
        }

        [TestMethod]
        [Owner("Sian")]
        [TestCategory("AccountServiceTest")]
        [TestProperty("AccountServiceTest", "AddAccount")]
        public void AddAccount_Password為空_應回傳Exception()
        {
            //assert
            var sut = this.GetSystemUnderTest();
            var info = new AccountInfoModel
            {
                Account = "test",
                Email = "test@gmail.com",
                Password = string.Empty,
                Phone = "0917888444"
            };
            //act
            Action actual = () => sut.AddAccount(info);

            //arrange
            actual.Should().Throw<Exception>()
                .Which.Message.Contains("請檢查輸入欄位,缺一不可!");
        }

        [TestMethod]
        [Owner("Sian")]
        [TestCategory("AccountServiceTest")]
        [TestProperty("AccountServiceTest", "AddAccount")]
        public void AddAccount_Phone為空_應回傳Exception()
        {
            //assert
            var sut = this.GetSystemUnderTest();
            var info = new AccountInfoModel
            {
                Account = "test",
                Email = "test@gmail.com",
                Password = "123456",
                Phone = string.Empty
            };
            //act
            Action actual = () => sut.AddAccount(info);

            //arrange
            actual.Should().Throw<Exception>()
                .Which.Message.Contains("請檢查輸入欄位,缺一不可!");
        }

結果:

2、使用者的帳號不可以重複。

        [TestMethod]
        [Owner("Sian")]
        [TestCategory("AccountServiceTest")]
        [TestProperty("AccountServiceTest", "AddAccount")]
        public void AddAccount_使用者帳號重複_應回傳錯誤訊息()
        {
            //assert
            var sut = this.GetSystemUnderTest();
            var info = new AccountInfoModel
            {
                Account = "test",
                Email = "test@gmail.com",
                Password = "123456",
                Phone = "0917888111"
            };

            var data = new AccountDataModel
            {
                Account = "test",
                Email = "test@gmail.com",
                Phone = "0917888111"
            };

            var expect = new ResultDto
            {
                Success = false,
                Message = "該使用者帳號已存在,請確認!"
            };

            this._accountRepository.GetAccount(Arg.Any<string>()).Returns(data);

            //act
            var actual = sut.AddAccount(info);

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

結果:

3、密碼長度不可低於6碼。

        [TestMethod]
        [Owner("Sian")]
        [TestCategory("AccountServiceTest")]
        [TestProperty("AccountServiceTest", "AddAccount")]
        public void AddAccount_密碼長度低於6碼_應回傳錯誤訊息()
        {
            //assert
            var sut = this.GetSystemUnderTest();
            var info = new AccountInfoModel
            {
                Account = "test",
                Email = "test@gmail.com",
                Password = "1234",
                Phone = "0917888111"
            };

            var expect = new ResultDto
            {
                Success = false,
                Message = "使用者密碼長度不可低於6碼!"
            };

            //act
            var actual = sut.AddAccount(info);

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

結果:

4、密碼需透過加密處理存入資料庫。

這邊比較特別的是 ConverPassword 這個 Function 是 Private,一般來說我們並不會特別針對 Private Function 做測試,理由是一個 Private Function 一定會在某個 Public Function 被執行。

當然要對 Private Function 進行測試也不是不行的,可以使用以下方式。

        [TestMethod]
        [Owner("Sian")]
        [TestCategory("AccountServiceTest")]
        [TestProperty("AccountServiceTest", "ConverPassword")]
        public void ConverPassword_輸入密碼_應回傳加密結果()
        {
            //assert
            var sut = this.GetSystemUnderTest();
            var method = sut.GetType().GetMethod("ConverPassword", BindingFlags.Instance | BindingFlags.NonPublic);
            var expect = "9GYVaHLoOg+y+V/HHwKtkzBH3y8XWn14h8ifWPYViLc=";

            //act
            var actual = method.Invoke(sut, new[] { "test2", "12371324" });

            //arrange
            actual.Should().Be(expect);
        }

結果:

這邊特別提醒一下,.NET Core 似乎已經沒有Private Object這個 Class 了,所以另尋出入找了這個方法。

可以參考這邊的資料

021/02/21更新 發現公司大神提供了另一個方法,還不趕快更新一下。

因為 Private 只能透過上面那種方式測試,但如果是 Internal 那就有不同做法了。
首先我們試著把 ConverPassword 這個 function 改成 Internal。

接著需要安裝 Meziantou.MSBuild.InternalsVisibleTo 這個 Nuget 套件。

請注意這邊安裝的是目標專案,並不是測試專案喔!!!
此套件的使用方法可以參照這個地方。

安裝完後要到 Service 的 csproj 檔新增以下設定。

對專案左鍵點擊兩下即可進入 csproj。

<ItemGroup>
  <InternalsVisibleTo Include="$(AssemblyName)Tests"/> 
  //專案名稱有Tests結尾的都可以看到Internal
</ItemGroup>

接下來到我們的測試專案原本測試 Private function 的地方。

修改一下寫法。

        [TestMethod]
        [Owner("Sian")]
        [TestCategory("AccountServiceTest")]
        [TestProperty("AccountServiceTest", "ConverPassword")]
        public void ConverPassword_輸入密碼_應回傳加密結果()
        {
            //assert
            var sut = this.GetSystemUnderTest();
            var expect = "9GYVaHLoOg+y+V/HHwKtkzBH3y8XWn14h8ifWPYViLc=";

            //act
            var actual = sut.ConverPassword("test2", "12371324" );

            //arrange
            actual.Should().Be(expect);
        }

結果:

檢查信箱是否合法。

同上一樣可以透過以下方式確認這個 Function 是否正確。

        [TestMethod]
        [Owner("Sian")]
        [TestCategory("AccountServiceTest")]
        [TestProperty("AccountServiceTest", "CheckMailFormate")]
        public void CheckMailFormate_輸入錯誤格式Mail_應回傳False()
        {
            //assert
            var sut = this.GetSystemUnderTest();
            var method = sut.GetType().GetMethod("CheckMailFormate", BindingFlags.Instance | BindingFlags.NonPublic);

            //act
            var actual = method.Invoke(sut, new[] { "fff#gmail.com"});

            //arrange
            actual.Should().Be(false);
        }
        
        [TestMethod]
        [Owner("Sian")]
        [TestCategory("AccountServiceTest")]
        [TestProperty("AccountServiceTest", "CheckMailFormate")]
        public void CheckMailFormate_輸入正確格式Mail_應回傳True()
        {
            //assert
            var sut = this.GetSystemUnderTest();
            var method = sut.GetType().GetMethod("CheckMailFormate", BindingFlags.Instance | BindingFlags.NonPublic);

            //act
            var actual = method.Invoke(sut, new[] { "fff@gmail.com" });

            //arrange
            actual.Should().Be(true);
        }

結果:

接下來別忘了要回去測試我們檢查信箱後回傳的訊息。

        [TestMethod]
        [Owner("Sian")]
        [TestCategory("AccountServiceTest")]
        [TestProperty("AccountServiceTest", "AddAccount")]
        public void AddAccount_輸入信箱格式部正確_應回傳錯誤訊息()
        {
            //assert
            var sut = this.GetSystemUnderTest();
            var info = new AccountInfoModel
            {
                Account = "test",
                Email = "test#gmail.com",
                Password = "1234567",
                Phone = "0917888111"
            };

            var expect = new ResultDto
            {
                Success = false,
                Message = "請確認信箱格式!"
            };

            //act
            var actual = sut.AddAccount(info);

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

結果:

新增結果。

        [TestMethod]
        [Owner("Sian")]
        [TestCategory("AccountServiceTest")]
        [TestProperty("AccountServiceTest", "AddAccount")]
        public void AddAccount_新增成功_應回傳正確訊息()
        {
            //assert
            var sut = this.GetSystemUnderTest();
            var info = new AccountInfoModel
            {
                Account = "test",
                Email = "test@gmail.com",
                Password = "1234567",
                Phone = "0917888111"
            };

            this._accountRepository.AddAccount(Arg.Any<AccountCondition>()).Returns(true);

            var expect = new ResultDto
            {
                Success = true,
                Message = "新增成功"
            };

            //act
            var actual = sut.AddAccount(info);

            //arrange
            actual.Should().BeEquivalentTo(expect);
        }
        
        [TestMethod]
        [Owner("Sian")]
        [TestCategory("AccountServiceTest")]
        [TestProperty("AccountServiceTest", "AddAccount")]
        public void AddAccount_新增失敗_應回傳錯誤訊息()
        {
            //assert
            var sut = this.GetSystemUnderTest();
            var info = new AccountInfoModel
            {
                Account = "test",
                Email = "test@gmail.com",
                Password = "1234567",
                Phone = "0917888111"
            };

            this._accountRepository.AddAccount(Arg.Any<AccountCondition>()).Returns(false);

            var expect = new ResultDto
            {
                Success = false,
                Message = "新增失敗"
            };

            //act
            var actual = sut.AddAccount(info);

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

結果:

後記

這樣我們第一個功能新增帳號的 Service 部分就完成了,接下來後續示範就會加緊腳步,比較特殊地方才會拿出來講解。

以上示範一樣會同步放在 GitHub 有需要觀看歡迎取用。

參考資料