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;
}
}
}

Leave a Reply