抽取相依的物件並且覆寫
下面是一個範例,這個範例的SUT是Holiday.cs,如果今天是9/1就傳HappyBirthday,否則就傳No
因為單元測試應該要能夠具有隔離的特性,不可因為今天的日期不一樣而有不同的結果
所以”取得今天日期”,就會讓程式碼變得不可測試。
在下面的範例裡,我們可以看見如何使用假物件去測試不可測試的程式碼:
Holiday.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
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<string, string> profiles; static Context() { profiles = new Dictionary<string, string>(); 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; } } } |