顯示具有 Unit Test 標籤的文章。 顯示所有文章
顯示具有 Unit Test 標籤的文章。 顯示所有文章
紀錄監聽事件的測試。實作概念 : 檢查事件監聽者是否在事件被觸發後做出相對應的響應。


這個測試範例為當View接收到錯誤(事件)後去呼叫Logger去紀錄Log(監聽者)。
被測試程式碼如下:

public interface IView
{
 event Action<string> ErrorOccured;
}

public interface ILogger
{
 void LogError(string text);
}

public class Presenter
{
 private readonly IView _view;

 private readonly ILogger _logger;

 public Presenter(IView view, ILogger logger)
 {
  this._view = view;
  this._logger = logger;
  this._view.ErrorOccured += ToLogError;

 }

 private void ToLogError(string text)
 {
  this._logger.LogError(text);
 }
}


測試程式碼 :
private IView _View;

private ILogger _Logger;

[TestMethod()]
public void PresenterTest_當View發生Error_呼叫寫Log()
{
 
 this._View = Substitute.For<IView>();
 this._Logger = Substitute.For<ILogger>();
 Presenter p = new Presenter(this._View, this._Logger);

 //使用NSubstitute觸發ErrorOccured事件
 this._View.ErrorOccured += 
                Raise.Event<Action<string>>("error message");

 
 //判斷監聽者是否做出正確的反應
 this._Logger.Received()
  .LogError(Arg.Is<string>(x => x == "error message"));
}

結語:

這個測試範例使用了一個存根(this._View)來觸發事件以及一個模擬對象(this._View)來驗證事件觸發後的監聽者是否呼叫了LogError。

前言:

單元測試的寫法基本上就是依照單元測試的3A原則:
"Arrange" :
初始化物件、要用到的參數設定(包含輸入及要輸出的期望結果)以及建立要模擬的物件或是存根(Stub)。
"Act" :
呼叫要測試的單元(通常是以方法為單位)。
"Assert" :
驗證結果,能驗證的情況又分三種,分別為驗證回傳值、改變系統狀態以及交互測試。

先寫一個簡單的加法方法,然後再來測試這個加法方法,加法方法程式碼如下:

public class Calculator
{
 public int Add(int first, int second)
 {
  return first + second;
 }
}


接著要產出單元測試的程式碼,在VS安裝Unit Test Generator


安裝完畢後,在要測試的方法右鍵選"Generate Unit Test"後會跳出一個視窗,基本上不用去改設定,直接選是就好。
測試的程式碼 :
[TestMethod()]
public void AddTest_輸入1和2_驗證應傳回3()
{
 //arrage
 //準備要傳入的參數
 var first = 1;
 var second = 2;
 
 //準備預期傳回的結果
 var expected = 3;
 Calculator sut = new Calculator();

 //act
 var actual = sut.Add(first, second);

 //assert
 Assert.AreEqual(expected, actual);
}

然後可以在測試總管選到這個測試,選擇執行後即可看到結果。

結語:

基本上就是把握三A原則來寫測試,這個範例只是入門,之後會介紹一些比較深入一點的東西。

範例二和範例一的不同之處在於範例一是虛擬化工廠方法,而範例二是虛擬化回傳值。
將被測試方法中相依外部去取值的方法抽成一個virtual的方法,建立一個新類別(TestableMemberService)去繼承這個被測試類別(MemberService)並附寫此virtual方法,在測試時直接使用子類別(TestableMemberService)來操作,傳入你想預設的結果,如此可達到隔離外部回傳的資料影響你被測試方法的測試結果。
程式碼如下:

//原程式碼
public class MemberRepository
{
 public string GetID(string name)
 {
  return "Original";
 }
}

public class MemberService
{
 public string GetMemberID(string name)
 {
  MemberRepository memberrepository =
      new MemberRepository();
  return memberrepository.GetID(name);
 }
}

//重構後
public class MemberRepository
{
 public string GetID(string name)
 {
  return "Original";
 }
}

public class MemberService
{
 protected virtual string GetID(string name)
 {
  MemberRepository mrp = new MemberRepository();
  return mrp.GetID(name);
 }

 //被測試的方法
 public string GetMemberID(string name)
 {
  return this.GetID(name);
 }
}

public class TestableMemberService : MemberService
{
 private string _ID;

 protected override string GetID(string name)
 {
  return this._ID;
 }

 internal void SetID(string id)
 {
  this._ID = id;
 }
}


測試程式碼 :
[TestMethod()]
public void GetMemberID_Test()
{
 //arrange
 var name = "Tom";
 var id = "321";

 TestableMemberService sut = new TestableMemberService();
 sut.SetID(id);

 //act
 var actual = sut.GetMemberID(name);

 //assert
 Assert.AreEqual(id, actual);
}

這樣的寫法比起範例一更加簡單,甚至不需要拉出介面,能夠在改動較少程式碼的情況下達成單元測試。

前言:

相信很多人都遇過Legancy Code,改沒有留下測試的Legancy Code最怕的就是改了東卻壞了西卻還不知道,為了避免類似問題,我們可以嘗試在Legancy Code中加入單元測試。但是事情往往沒那麼順利,很多專案都外部依賴的很頻繁,想重構切出介面等可能工程浩大,要想在動到最少程式碼的情況下去加入單元測試,"抽取和重寫(extract and override)"會是個有用的技術。


使用被測試類別中的一個virtual方法做為工廠方法,在MemberService的子類別複寫這個虛擬方法,產生出你需要的接口。在接口注入你準備好的Stub物件,之後測試就以子類別來進行。
程式碼如下:

//原程式碼
public class MemberRepository
{
 public string GetID(string name)
 {
  return "Original";
 }
}

public class MemberService
{
 public string GetMemberID(string name)
 {
  MemberRepository memberrepository =
      new MemberRepository();
  return memberrepository.GetID(name);
 }
}

//重構後
public class MemberRepository : IMemberRepository
{
 public string GetID(string name)
 {
  return "Original";
 }
}

public interface IMemberRepository
{
 string GetID(string name);
}

public class MemberService
{
 protected virtual IMemberRepository GetRepository()
 {
  return new MemberRepository();
 }

 //被測試的方法
 public string GetMemberID(string name)
 {
  return GetRepository().GetID(name);
 }
}

public class TestableMemberService : MemberService
{
 private IMemberRepository _IMemberRepository;

 public TestableMemberService(IMemberRepository memberRepository)
 {
  this._IMemberRepository = memberRepository;
 }

 protected override IMemberRepository GetRepository()
 {
  return this._IMemberRepository;
 }
}


測試程式碼 :
//Stub
public class FakeMemberRepository : IMemberRepository
{
 public string returnID { get; set; }
 public string GetID(string name)
 {
  return returnID;
 }
}

[TestMethod()]
public void GetMemberID_Test()
{
 //arrange
 var name = "Tom";
 var expected = "321";

 var stub = new FakeMemberRepository();
 stub.returnID = expected;
 TestableMemberService sut = new TestableMemberService(stub);


 //act
 var actual = sut.GetMemberID(name);

 //assert
 Assert.AreEqual(expected, actual);
}

結語:

抽取和重寫建立了全新的間接層,接近被測試單元的表層,越接近表層表示你需要修改的依賴項目就越少。抽取和重寫很適合用在模擬提供給被測試單元的輸入,例如呼叫其他方法取得值然後繼續做後續的操作。

前言:

這篇說明的是 "在方法被呼叫前注入偽對象",配合使用工廠模式來達成。


延續上一篇的例子,只是這次不用在MemberService的建構子傳入參數,而是透過工廠模式來返回Stub物件(實作IMemberRepository的Stub)。
程式碼如下:

//原程式碼
public class MemberRepository
{
 public string GetID(string name)
 {
  return "Original";
 }
}

public class MemberService
{
 public string GetMemberID(string name)
 {
  MemberRepository memberrepository =
      new MemberRepository();
  return memberrepository.GetID(name);
 }
}

//重構後
public class MemberRepository : IMemberRepository
{
 public string GetID(string name)
 {
  return "Original";
 }
}

public class MemberService
{
 private IMemberRepository _IMemberRepository;

 public MemberService()
 {
  //呼叫工廠
  this._IMemberRepository = MemberRepositoryFactory.Create();
 }

 //被測試的方法
 public string GetMemberID(string name)
 {
  return this._IMemberRepository.GetID(name);
 }
}

public interface IMemberRepository
{
 string GetID(string name);
}

//工廠方法
public class MemberRepositoryFactory
{
 private static IMemberRepository repository = null;

 public static IMemberRepository Create()
 {
  if (repository != null)
  {
   return repository;
  }
  return new MemberRepository();
 }

 public static void SetRepository(IMemberRepository mrp)
 {
  repository = mrp;
 }
}


測試程式碼 :
//Stub
public class FakeMemberRepository : IMemberRepository
{
 public string returnID { get; set; }
 public string GetID(string name)
 {
  return returnID;
 }
}

[TestMethod()]
public void GetMemberID_Test()
{
 //arrange
 var name = "Tom";
 var expected = "321";

 var imr = new FakeMemberRepository();
 imr.returnID = expected;
 MemberRepositoryFactory.SetRepository(imr);
 MemberService sut = new MemberService();

 //act
 var actual = sut.GetMemberID(name);

 //assert
 Assert.AreEqual(expected, actual);
}

結語:

和之前做法不同的是,這次是將接口加在工廠類別中,使用這種技術的測試程式碼可讀性較佳,每個類別的職責清晰。

前言:

接續上一篇提過的 抽取出一個接口,使底層實現可以替換 的重構方法後,這次介紹 "依賴注入(DI):在被測試單元中注入一個偽實現"。


和上一篇的例子相同,只是不同處在於這次在類別中創造一個接口,並將Stub放入接口,至於如何創造接口,就是把抽出來的介面IMemberRepository,透過MemberService類別的建構子注入。
程式碼如下:

//抽取前
public class MemberRepository
{
 public string GetID(string name)
 {
 ....
 }
}

public class MemberService
{
 public string GetMemberID(string name)
 {
  MemberRepository memberrepository = 
      new MemberRepository();
  return memberrepository.GetID(name);
 }
}

//抽取後
public class MemberRepository : IMemberRepository
{
 public string GetID(string name)
 {
 ....
 }
}

public class MemberService
{
 private IMemberRepository _IMemberRepository { get; set; }

 public MemberService(IMemberRepository memberRepository)
 {
  this._IMemberRepository = memberRepository;
 }

 //被測試的方法
 public string GetMemberID(string name)
 {
  return this._IMemberRepository.GetID(name);
 }
}

interface IMemberRepository
{
    string GetID(string name);
}


測試程式碼 :
//Stub
public class FakeMemberRepository : IMemberRepository
{
 public string returnID {get; set;}

 public string GetID(string name)
 {
  return returnID;
 }
}

[TestMethod()]
public void GetMemberID_Test()
{
 //arrange
 var name = "Tom";
 var expected = "321";
 IMemberRepository imr = new FakeMemberRepository();
 imr.returnID = expected;
 MemberService sut = new MemberService(imr);
 
 //act
 var actual = sut.GetMemberID(name);
 
 //assert
 Assert.AreEqual(expected, actual);
}

結語:

這次的範例就是一個完整的測試案例,重點在於如何將你要測試的方法中引用的外部資源隔離。需要注意的是,使用這種建構子注入有可能在越來越多的建構式或是建構式的傳入參數很多就會變得難維護,可讀性也變差,想解決這個問題,可以用一個特殊的類別來包含初始化這個類別所需要的所有值,那你的建構函式就會只需要傳入這個特殊類別參數(Parameter object Refactoring)。
或是使用控制反轉(Inversion of Control, IoC)容器如Unity、Autofac也可以解決這個問題。
打破依賴的重構方法分為兩類,我們分別稱為A型和B型重構,兩者相互依賴:

A: 把具體的類別抽象成接口(interfaces)或是委派(delegates)。
     - 抽取出一個接口,使底層實現可以替換

B: 重構代碼,藉由重構來產生能注入這種委派和接口的偽實現(fake implementation)。
     - 在被測試類別中注入一個Stub實體
     - 在建構子注入一個偽對象
     - 注入一個作為屬性設置和讀取的偽對象
     - 在一個方法呼叫前,注入一個偽對象


先來介紹 "抽取出一個接口,使底層實現可以替換" :
簡單來說,就是將方法從類別中提取成介面。
舉個例,想測試MemberService的GetMemberID方法,先將這個方法用到的外部資源MemberRepository隔離開來,抽成介面IMemberRepository,接著就可以用這個介面當接口
程式碼如下:

//抽取前
public class MemberRepository
{
 public string GetID(string name)
 {
 ....
 }
}

public class MemberService
{
 public string GetMemberID(string name)
 {
  MemberRepository memberrepository = 
      new MemberRepository();
  return memberrepository.GetID(name);
 }
}

//抽取後
public class MemberRepository : IMemberRepository
{
 public string GetID(string name)
 {
 ....
 }
}

public class MemberService 
{
 //被測試的方法
 public string GetMemberID(string name)
 {
  IMemberRepository imr = new MemberRepository();
  return imr.GetID(name);
 }
}

interface IMemberRepository
{
    string GetID(string name);
}

返回固定值的簡單Stub程式碼如下:
//Stub
public class FakeMemberRepository : IMemberRepository
{
 public string GetID(string name)
 {
  return "123";
 }
}

//引用Stub
public string GetMemberID(string name)
{
 IMemberRepository imr = new FakeMemberRepository();
 return imr.GetID(name);
}

結語:

這種利用實現偽對象的技術,可能導致程式碼裡有太多這種為了測試而寫的類別,將來可能不好維護,而到現在為止,你的被測試方法還是相依於IMemberRepository的實作類別,因此需要在程式碼中加入一個接口插入存根(Stub)。