Posted on Leave a comment

單元測試 – 重構測試

重構test的重要性

好的測試應該要容易維護,容易閱讀,
不應包含程式邏輯在內,因此像是if, while, for迴圈等都不應該出現在測試裡
如果我們驗證的內容會和資料有關,則建議使用Substitute,這樣可以讓我們能夠在每一個測試裡增加不同的資料
而且可以直接在每個測試裡看到資料的內容是什麼
下面是一個範例
private void AmountShouldBe(int expected, DateTime start, DateTime end)
{
IList data = new List()
{
new Budget() {Amount = 310, YearMonth = “201801”},
new Budget() {Amount = 620, YearMonth = “201803”},
new Budget() {Amount = 900, YearMonth = “201804”}
};

var budgetCalculator = new BudgetCalculator(new TestDataBudgetRepository(data));
var budget = budgetCalculator.TotalAmount(start, end);
Assert.AreEqual(expected, budget);
}
91的課程中建議每一次寫成功一個測試案例,就可以先做重構,這樣之後的的開發速度可以更加速

重構測試的步驟

  • 抽出field: mock object(繼承假物件去創建假物件), stub(用Substitute)
  • Setup: SUT初始化(或者[TestInitialize])
  • 抽出方法(用Given為開頭):定義mock object行為,代表假如在跑這個scenario時…
  • Extra Method with “Shouldxxx()” => SUT行為+Assertion

也就是盡可能讓我們的重構能夠符合3A原則,
3A pattern: Arrange, Act, Assert
上面重構後Arrange就用Given…,然後Act就是SUT行為,Assert就是Assertion。

重構的測試的範例

下面為重構後的程式碼:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NSubstitute;
using NUnit.Framework;
using RsaSecureToken;
using Assert = NUnit.Framework.Assert;

namespace RsaSecureToken.Tests
{
[TestFixture]
public class AuthenticationServiceTests
{
private IProfile _fakeProfile;
private IRsaToken _fakeToken;
private ILogger _logger;
private AuthenticationService _authenticationService;

[SetUp]
public void Given()
{
_fakeProfile = Substitute.For();

_fakeToken = Substitute.For();

_logger = Substitute.For();

_authenticationService = new AuthenticationService(_fakeProfile, _fakeToken, _logger);
}

private void GivenToken(string token)
{
_fakeToken.GetRandom(“”).ReturnsForAnyArgs(token);
}

private void GivenPassword(string account, string password)
{
_fakeProfile.GetPassword(account).Returns(password);
}

private void ShouldBeValid(string account, string password)
{
var actual = _authenticationService.IsValid(account, password, _logger);
Assert.IsTrue(actual);
}

[Test()]
public void IsValidTest()
{
GivenPassword(account: “joey”, password: “91”);
GivenToken(token: “000000”);

ShouldBeValid(account: “joey”, password: “91000000”);
}

[Test()]
public void IsInValidTest()
{
GivenPassword(account: “joey”, password: “91”);
GivenToken(token: “000000”);

ShouldBeInValid(account: “joey”, errorPassword: “error password”);
}

private void ShouldBeInValid(string account, string errorPassword)
{
var actual = _authenticationService.IsValid(account, errorPassword, _logger);
Assert.IsFalse(actual);
}

[Test()]
public void ShouldLog()
{
GivenPassword(account: “joey”, password: “91”);
GivenToken(token: “000000”);

ShouldBeInValid(account: “joey”, errorPassword: “error password”);

//這個可能會有過度指定的問題,或許加一個痘號就會導致測試失敗
//_logger.Received(1).Save(Arg.Is(“account: joey try to login failed”));
_logger.Received(1).Save(Arg.Is(m => m.Contains(“joey”) && m.Contains(“login failed”)));
}
}
}

Posted on Leave a comment

單元測試 – 隔離框架Substitute.For

STUB的功能

這邊是NSubstitute的說明:http://nsubstitute.github.io/help/getting-started/
Substitute是.NET裡的一個隔離框架,若要使用,需要額外在測試專案用NUGET去安裝NSubstitute

1. 動態產生假物件
2. 模擬回傳值
3. 測試監聽事件
4. 驗證傳入參數是否正確

使用Subsitute(Sub)
使用方法如下
calculator = Substitute.For();
設定呼叫某個方法應該回傳的值
calculator.Add(1, 2).Returns(3);
Assert.That(calculator.Add(1, 2), Is.EqualTo(3));
下面可以驗證是否Add這個FUNC有被呼叫到
calculator.Add(1, 2);
calculator.Received().Add(1, 2);
calculator.DidNotReceive().Add(5, 7);
下面的程式能夠判別傳入的參數是不是正確
calculator.Add(10, -5);
calculator.Received().Add(10, Arg.Any());
calculator.Received().Add(10, Arg.Is(x => x < 0));[/code] 驗證回傳值是否正確 [code lang="C#"]calculator .Add(Arg.Any(), Arg.Any())
.Returns(x => (int)x[0] + (int)x[1]);
Assert.That(calculator.Add(5, 10), Is.EqualTo(15));

使用Substitute來針對不同狀況實作假介面

這是一個用MOCK方法的範例

using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
//using Microsoft.VisualStudio.TestTools.UnitTesting;
using NUnit.Framework;
using RsaSecureToken;
using Assert = NUnit.Framework.Assert;

namespace RsaSecureToken.Tests
{
    [TestFixture]
    public class AuthenticationServiceTests
    {
        [Test()]
        public void IsValidTest()
        {
            var target = new AuthenticationService(new FakeProfile(), new FakeToken());

            var actual = target.IsValid("joey", "91000000");

            //always failed
            Assert.IsTrue(actual);
        }
    }

    public class FakeProfile:IProfile
    {

        public string GetPassword(string account)
        {
            if (account == "joey")
            {
                return "91";
            }

            throw new Exception();
        }
    }

    public class FakeToken:IRsaToken
    {
        public string GetRandom(string account)
        {
            return "000000";
        }
    }
}

上面做法有什麼問題?

  • 每一個不同的依賴案例就要製作一個不同的FakeObject,會讓寫測試的時間太久
  • 沒辦法直接從程式碼知道為什麼這樣會是Vaild

動態產生物件的使用完整範例

Subsitute.For()
定義假物件行為(stub)
fake.方法(參數).Returns(值)
[Test()]
public void IsValidTest()
{
var fakeProfile = Substitute.For();
fakeProfile.GetPassword(“joey”).Returns(“91”);

var fakeToken = Substitute.For();
fakeToken.GetRandom(“”).ReturnsForAnyArgs(“000000”);

var target = new AuthenticationService(fakeProfile, fakeToken);
var actual = target.IsValid(“joey”, “91000000”);

//always failed
Assert.IsTrue(actual);
}
}

驗證某個函數是否被呼叫

使用mock object assertion
需求:驗證是非法的時候要記一個log
fake.Receive(次數).方法(參數驗證)
不要用太多mock就算要使用要避免過度指定,也就是當prod code小小變動就導致測試程式壞掉

下面的程式碼可以驗證當呼叫SyncBookOrders()時是不是會呼叫Insert這個函數兩次:
[Test]
public void Test_SyncBookOrders_3_Orders_Only_2_book_order()
{
var result = new List
{
new Order
{
Type = “Book”
},
new Order
{
Type = “Book”
},
new Order
{
Type = “Item”
}
};

var target = new OrderServiceForTest();
target.SetOrder(result);

var fakeBookDao = Substitute.For();
target.SetDao(fakeBookDao);

target.SyncBookOrders();

fakeBookDao.Received(2).Insert(Arg.Is(m => m.Type == “Book”));
}

驗證傳入參數

驗證傳入的參數是否包含某些關鍵字
_logger.Received(1).Save(Arg.Is(m => m.Contains(“joey”) && m.Contains(“login failed”)));

Posted on Leave a comment

單元測試 – Code Coverage的意義

如何用Code Coverage來衡量單元測試的成果

  • 在build的時候要自動去跑測試(使用CI,CD當作工具)> 大於0%
  • 關心相對趨勢 大於 絕對數字,衡量相對的數字,評估code coverage的數值有沒有比昨天高,每一次針對PROD的程式碼修改都要加上測試,這樣這個數值只會往上不會往下。鼓勵持續的COMMIT > 持續整合,每天都要COMMIT個5~6次或7~8次。
  • 那些測試的投資報酬率高:為什麼要寫測試?目標是要提升產品品質。假設這段PROD CODE沒有BUG,其實不需要為它寫測試。那什麼東西的投資報酬率最高?
    • 1. 要修正的BUG(BUG越晚發現修正成本就更高)
    • 2. 實務上常跑到的scenario
    • 3. 最主要的情境
    • 4.和錢有關的
    • 5.和人命有關的(EX:自動駕駛系統)
    • 6. 最常改到的CODE,可以避免被改錯
  • 那code coverage的意義如下:
    • 觀察沒有被含蓋到的情境:判斷要不要補測試
    • Dead Code:根本就不會跑到的Code(代表這一段PROD CODE不會跑到)

最好導入的方式

從上面看來,兩個最好導入的方式:1. 針對所有BUG的修改去寫測試,2. 針對新的專案去寫測試
然後要確認code coverage不可以往下掉。
假如現在有一陀爛CODE,要增加新功能在那堆爛CODE裡,先把爛CODE抽成method,然後再抽成新的Class
針對爛CODE的public的情境先寫測試,會讓需要寫測試的範圍變小很多。

測試的品質

  • 測試的程式一定要重構
  • 測試的語意一定要清楚明白,一般Assert都會抽出成一個func,這是為了讓測試更容易理解,可以很簡單的的從測試的程式碼由語意就能理解這個測試要做什麼
  • 寫測試的難度會反應程式的好壞
Posted on Leave a comment

單元測試 – 使用Fake Object

抽取相依的物件並且覆寫

下面是一個範例,這個範例的SUT是Holiday.cs,如果今天是9/1就傳HappyBirthday,否則就傳No
因為單元測試應該要能夠具有隔離的特性,不可因為今天的日期不一樣而有不同的結果
所以”取得今天日期”,就會讓程式碼變得不可測試。

在下面的範例裡,我們可以看見如何使用假物件去測試不可測試的程式碼:
Holiday.cs
using System;

namespace TestProject1
{
public class Holiday
{
public string IsTodayJoeyBirthday()
{
var date = GetToday();
return (date.Month == 9 && date.Day == 1) ? “HappyBirthday” : “No”;
}
//將有相依的部份抽出來
protected virtual DateTime GetToday()
{
return DateTime.Today;
}
}
}
Test2Cs.cs
using System;
using NUnit.Framework;
using TestProject1;

namespace TestProject1
{
[TestFixture]
public class Test2Cs
{
[Test]
public void today_is_not_joey_birthday()
{
var target = new HolidayForTest();
target.SetToday(new DateTime(2015,1,1));
Assert.AreEqual(target.IsTodayJoeyBirthday(), “No”);
}

[Test]
public void today_is_joey_birthday()
{
var target = new HolidayForTest();
target.SetToday(new DateTime(2015,9,1));
Assert.AreEqual(target.IsTodayJoeyBirthday(), “HappyBirthday”);
}
}
}
//創建一個假物件並且繼承Holiday,替換掉取得今日日期這件事
internal class HolidayForTest : Holiday
{
private DateTime _today;
protected override DateTime GetToday()
{
return _today;
}

public void SetToday(DateTime date)
{
_today = date;
}
}

如何對現行沒有測試的PROD CODE加上unit test


這一個測試的重點在於:針對依賴的code,找出不可控制的地方並抽出來

  • 找到不可控制的依賴
  • 抽出方法
  • 把private改成protect virtual
  • 在測試專案新增子類繼承SUT
  • override protected方法並加開set方法
  • 測試SUT改測子類
  • set依賴的值

使用依賴注入

使用依賴注入來達成低藕合高內聚的程式碼
1. 針對相依的物件抽出interface,針對相依的值抽出field
2. 產生contructor,選要注入的field去依賴注入
3. 產生無參數的constructor,確保本來的程式無誤
4. 測試程式新增fake物件做interface,決定行為並注入SUT

所謂低藕合高內聚的程式碼也可以讓程式更容易被測試

用Fake Object來取代相依物件的方法

如果測試的資料都是固定的,可以在測試裡面創建假物件來注入SUT(測試目標)

以下為範例程式
AuthenticationServiceTests.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
//using Microsoft.VisualStudio.TestTools.UnitTesting;
using NUnit.Framework;
using RsaSecureToken;
using Assert = NUnit.Framework.Assert;

namespace RsaSecureToken.Tests
{
[TestFixture]
public class AuthenticationServiceTests
{
[Test()]
public void IsValidTest()
{
var target = new AuthenticationService(new FakeProfile(), new FakeToken());

var actual = target.IsValid(“joey”, “91000000”);

//always failed
Assert.IsTrue(actual);
}
}

public class FakeProfile:IProfile
{

public string GetPassword(string account)
{
if (account == “joey”)
{
return “91”;
}

throw new Exception();
}
}

public class FakeToken:IRsaToken
{
public string GetRandom(string account)
{
return “000000”;
}
}
}
AuthenticationService.cs
using System;
using System.Collections.Generic;

namespace RsaSecureToken
{
public class AuthenticationService
{
private IProfile _profileDao;
private IRsaToken _rsaToken;

public AuthenticationService()
{
_profileDao = new ProfileDao();
_rsaToken = new RsaTokenDao();
}
//for test
public AuthenticationService(IProfile profile, IRsaToken rsaToken)
{
_profileDao = profile;
_rsaToken = rsaToken;
}

public bool IsValid(string account, string password)
{
// 根據 account 取得自訂密碼
var passwordFromDao = _profileDao.GetPassword(account);

// 根據 account 取得 RSA token 目前的亂數
var randomCode = _rsaToken.GetRandom(account);

// 驗證傳入的 password 是否等於自訂密碼 + RSA token亂數
var validPassword = passwordFromDao + randomCode;
var isValid = password == validPassword;

if (isValid)
{
return true;
}
else
{
return false;
}
}
}

public interface IProfile
{
string GetPassword(string account);
}

public class ProfileDao : IProfile
{
public string GetPassword(string account)
{
return Context.GetPassword(account);
}
}

public static class Context
{
public static Dictionary profiles;

static Context()
{
profiles = new Dictionary();
profiles.Add(“joey”, “91”);
profiles.Add(“mei”, “99”);
}

public static string GetPassword(string key)
{
return profiles[key];
}
}

public interface IRsaToken
{
string GetRandom(string account);
}

public class RsaTokenDao : IRsaToken
{
public string GetRandom(string account)
{
var seed = new Random((int)DateTime.Now.Ticks & 0x0000FFFF);
var result = seed.Next(0, 999999).ToString(“000000”);
Console.WriteLine(“randomCode:{0}”, result);

return result;
}
}
}

Posted on Leave a comment

單元測試 – 寫測試的基本原則

熱鍵

  • Alt+Enter: Quick+Action
  • Ctrl+R,M 抽方法出來
  • code snippets / live template
  • constructor: ctor
  • property:prop
  • console.writeLine)():cw
  • ctrl+R,F: Extract Field
  • Alt+Insert
  • 循環剪貼簿:ctrl+shift+V

測試替身有下面三種

  • stub:不做驗證,單純只做模擬相依物件的行為
  • mock:一開始就要把所有的都定義清楚,應該要呼叫那個方法,所有的值一定要一模一樣,否則就會報錯(嚴格,敏感,不穩定)
  • spy: 則是把所有的互動先做完,只驗要驗的,剩的沒有測就是都算過,差異是一個從嚴一個從寬。(寬鬆)

因此mock和spy本身含有驗證(Assertion),而stub本身只有在模擬相依的物件而已。

比較物件屬性的方式

比較兩邊的物件包含子物件完全相同

expected.ToExpectedObject().ShouldEqual(actual);

比較以expected為主的屬性去比較相對應的actual是否相同

expected.ToExpectedObject().ShouldMatch(actual);

測試範例如下
namespace AssertionSample
{
[TestFixture]
public class AssertionSample
{
private CustomerRepo customerRepo = new CustomerRepo();

//比較物件屬性的方式
[Test]
public void CompareCustomer()
{
var actual = customerRepo.Get();
var expected = new Customer
{
Id = 2,
Age = 18,
Birthday = new DateTime(1990, 1, 26)
};

//CollectionAssert也是在比較物件的位置,所以也會有相同的問題
expected.ToExpectedObject().ShouldEqual(actual);
}

//比較集合的方式
[Test]
public void CompareCustomerList()
{
var actual = customerRepo.GetAll();
var expected = new List
{
new Customer()
{
Id = 3,
Age = 20,
Birthday = new DateTime(1993,1,2)
},
new Customer()
{
Id = 4,
Age = 21,
Birthday = new DateTime(1993,1,3)
}
};

expected.ToExpectedObject().ShouldEqual(actual);
}

//組合式物件的比較
[Test]
public void CompareComposedCustomer()
{
var actual = customerRepo.GetComposedCustomer();

var expected = new Customer()
{
Age = 30,
Id = 11,
Birthday = new DateTime(1999, 9, 9),
Order = new Order {Id = 19, Price = 91},
};

expected.ToExpectedObject().ShouldEqual(actual);
}

//回傳的資料的PROPERITY很多,但是我們只想比其中幾項
[Test]
public void PartialCompare_Customer_Birthday_And_Order_Price()
{
var actual = customerRepo.GetComposedCustomer();

//有自定型別的一定要改成匿名型別
var expected = new
{
Birthday = new DateTime(1999, 9, 9),
Order = new {Price = 91},
};

//使用匿名型別 去比較以expected為主去比較相對應的actual是否相同
expected.ToExpectedObject().ShouldMatch(actual);
}
}

寫測試的規則


測試程式不含商業邏輯,所有的測試都應該是直述句
不應包含以下的元素:

  • prod business logic
  • 不含if, else, switch case等邏輯程式碼
  • 更不含try..catch
  • 不含for, while, foreach, do..while

善用assertion package

  • C#: expectedObjects,FluentAssertions
  • Java: Assert J

不要攤開properity做比較

[Test]
public void Divide_Zero()
{
var calculator = new Calculator();
var actual = calculator.Divide(5, 0);
Action action = () =>
{
calculator.Divide(5, 0);
};
action.Should().Throw();
//never use try/catch in unit test
}

寫測試一定要重構,不然在測試需求異動時和寫測試時會太花時間
因此不應該要有太多不會用到的資訊
讓測試的目標的意圖可以很明顯
要如何加快單元測試撰寫的速度很重要,這樣才有可能可以實踐單元測試
沒有時間是個問題,但是我們要去面對如何解決這個問題
要知道怎麼用工具怎麼寫比較快

Posted on Leave a comment

單元測試的藝術-單元測試基礎

單元測試

一個單元代表的是系統中的工作單元或是一個使用案例
被測試的系統(System Under Test)我們稱做SUT或者Class Unit Test(CUT)
一個單元測試是一段程式呼叫一個工作單元,並驗證工作單元的一個具體最終結果。如果對這個最終結果的假設是錯誤的,那單元測試就失敗了。一個單元測試的範圍,可以小到一個方法,大到多個類別。

優秀單元測試的特質

  • 自動化,可被重覆執行的
  • 很容易被實現
  • 非臨時性的
  • 任何人都可以按鈕執行他
  • 執行速度快
  • 執行結果每次都是一致的
  • 能完全掌控被測試的單元
  • 完全被隔離的
  • 若執行失敗會有清楚的原因

整合測試

整合測試是一個有順序的測試過程,將軟硬體相結合並進行測試直到整個系統被整合在一起。也就是這個測試對被測試的單元並沒有完全的控制,而是使用該單元一個或多個真實依賴的相依物件,例如:時間,網路,資料庫,執行緒,亂數產生器等等。

一個單元測試通常包含了三個行為

  • 準備(Arrange):物件,建立物件,進行必要的設定
  • 操作(Act): 物件
  • 驗證(Asset): 某件事符合預期

Assert類別

  • Assert.True: 驗證一個布林條件,見Assert.False
  • Assert.AreEqual: 驗證傳回的值應相同
  • Assert.AreSame: 驗證兩個參數應指向同一個物件

使用參數來測試

使用TestCase標籤

Setup和tesrdown