0%

【Unit Test】Day 4 - Mock

Demo檔案 : Git傳送門
因為程式是昨天的延續,所以是同一個Repository切出UnitTest_Day4的Branch

讓繼續我們昨天的議題,節錄昨天最後的結論

…這個方法也不是百分之百沒有缺點,你應該也可以觀察到其中一段在這單元測試中無法被涵蓋到,
那就是組API Url那段,在這種透過繼承解耦合的方式中,因為回傳結果是自己設定的,
實際上組成的URL正確與否我們並不知道,換句話說,如果我們今天這樣寫測試可能會過,
但上線後才會發現錯誤,檢查之下發現原來是在串Url的時候錯字之類的。

身為一位專業有責任感的工程師,你一定想著「爾等豈能如此苟且偷生便宜行事草率」,所以今天就來談談另一種方法,Mock。

何謂Mock。
當測試時你關注與外部相依物件互動時其狀態的變化,並且驗證它,則此就是Mock物件。

很抽象對吧,沒關係讓我們來改改昨天的實作,重中瞭解Stub與Mock的差異。

先將呼叫API的地方徹底隔離出去
開一個NetTool的資料夾,建立一個IRestSharp的介面

該介面定義出呼叫API該傳入Url,回傳得到的結果

1
2
3
4
5
6
7
8
9
10
public interface IRestSharp
{
/// <summary>
/// 用Get的方法呼叫API
/// </summary>
/// <param name="url">Url</param>
/// <returns>回傳的內容</returns>
string Get(string url);
}

實做這個介面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyRestSharp : IRestSharp
{
/// <summary>
/// 用Get的方法呼叫API
/// </summary>
/// <param name="url">Url</param>
/// <returns>回傳的內容</returns>
public string Get(string url)
{
//Use RestSharp Call API
var client = new RestClient(url);
var request = new RestRequest(Method.GET);
request.AddHeader("cache-control", "no-cache");
IRestResponse response = client.Execute(request);

if (response.StatusCode == HttpStatusCode.OK)
{
return response.Content;
}

return string.Empty;
}
}

修改PTX的程式

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
public class PTX
{
IRestSharp _MyRestSharp;

/// <summary>
/// Construct
/// </summary>
/// <param name="myRestSharp">外部注入呼叫API的實體</param>
public PTX(IRestSharp myRestSharp)
{
//改由外部注入呼叫API的實體
this._MyRestSharp = myRestSharp;
}

/// <summary>
/// 取得巴士路線資料
/// </summary>
/// <param name="city">縣市名稱</param>
/// <param name="routeName">巴士路線名稱</param>
/// <returns></returns>
public BusRouteDTO Get(string city,string routeName)
{
BusRouteDTO Result = null;

//要呼叫的API Url
string Url = string.Format($"http://ptx.transportdata.tw/MOTC/v2/Bus/StopOfRoute/City/{city}/{routeName}?%24top=1&%24format=JSON");

var JsonResult = _MyRestSharp.Get(Url);

if (!string.IsNullOrEmpty(JsonResult))
{
var APIResult = JsonConvert.DeserializeObject<List<PTXBusRouteResult>>(JsonResult);

if (APIResult != null && APIResult.Count > 0)
{
var Route = APIResult.First();
Result = new BusRouteDTO
{
Name = Route.RouteName.Zh_tw,
BusStops = new List<BusRouteDTO.BusStop>()
};

foreach (var stop in Route.Stops)
{
Result.BusStops.Add(new BusRouteDTO.BusStop
{
ID = stop.StopUID,
Name = stop.StopName.Zh_tw
});
}
}
}

return Result;
}
}

修改Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Program
{
static void Main(string[] args)
{
//new PTX時變成要帶入呼叫API的實體
var PTXFunction = new PTX(new MyRestSharp());

var Result = PTXFunction.Get("Taipei","307");

Console.Write(JsonConvert.SerializeObject(Result));

Console.ReadKey();
}
}

執行後結果是對的!!

單元測試

到目前為止都沒有解釋太多為何要這樣改,不過這跟物件導向設計比較有關,即所謂的外部注入即依賴介面,但這比較偏設計的範疇這邊就不特別討論,不過之後應該會漸漸的發現,如果單元測試要能寫,很多地方都會必須要有物件導向的設計方式才有辦法。

讓我們回頭看單元測試,首先昨天做的PTXStub就沒用了,因為目前的PTX沒有將呼叫API封裝起來,也就沒有可以override的CallAPI方法了,把它砍掉了。

這時就會看到昨天的單元測試壞掉了,因為找不到PTXStub

開始做我們的假物件MyRestSharpMock吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyRestSharpMock : IRestSharp
{
public string Get(string url)
{
if (url == "http://ptx.transportdata.tw/MOTC/v2/Bus/StopOfRoute/City/Taipei/307?%24top=1&%24format=JSON")
{
var sb = new System.Text.StringBuilder(12766);
sb.AppendLine(@"[{""RouteUID"":""TPE16111"",""RouteID"":""16111"",""RouteName"":{""Zh_tw"":""307"",""En"":""307""},.....省略");

return sb.ToString();
}

return string.Empty;
}
}

這邊特別注意一下if那邊,依據昨天的單元測試,最後網址應該要組成這樣才是正確的,所以我們MyRestSharpMock特地加上這段判斷,已經不像昨天override一樣,不關心它組的網址為何,一律依照我們設定的結果回傳,即便你其實Url組的是錯誤的。

修改PTXTest.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[TestClass()]
public class PTXTests
{
[TestMethod()]
public void GetTest_傳入縣市和公車路線名稱_如果查的到資料_應回傳該路線的BusRouteDTO()
{
//arrange
var RestSharpMock = new MyRestSharpMock();
var Sut = new PTX(RestSharpMock); //注入我們模擬的假物件MyRestSharpMock
var City = "Taipei";
var RouteName = "307";

var expected = "307";
//act
var actual = Sut.Get(City, RouteName);

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

我們注入的是自己Mock的物件,他不會實際去呼叫API,但它關注我們傳入的值對不對,執行單元測試看看結果。

可能到這邊會有一個疑問是,「廢話,Mock的物件是我們自己做的,我們想怎麼樣都馬可以!!」,一開始學到這段的時候也有這個疑問,所以容我解釋一下,還記得先前提到的觀念嗎?

單元測試關注本身的邏輯,而非外部的關聯

如果今天上線後發現錯誤,而發生錯誤的地方是在呼叫API的程式,那該修改的是誰?當然是MyRestSharp.cs,也絕對不是PTX這支程式,因為它職責掌管的邏輯全部都是正確的。而我們現在單元測試以及MyRestSharpMock都是在做什麼? 都是在輔助我們驗證PTX所有包含到的邏輯,我們並不關注外部其他程式到底幹了什麼,退一萬步說(好啦其實退一步就可以了)(你好煩)今天IRestSharp這個Interface你的夥伴忘記去實作導致程式上線壞了,那也不會是改你的PTX吧,要改可以,over my dead body先,所以希望這囉說的補充可以幫助釐清這件事情。

好的,上面這邊如果沒問題的話,那下一步應該心裡就會冒出一個OS :我的好天鵝啊!! 如果外部相依的物件很多,每個都自己做Mock物件,做完都老了,單元測試沒寫完Deadline就到了,搞毛啊….
看官稍安勿躁,你的心聲有人聽到了,所以接下來介紹一個可以省略掉剛剛一堆繁瑣的動作,登登登登,為您隆重介紹NSubstitute,怎麼用呢?讓我們在UnitTestDay3Tests這個專案透過Nuget安裝這個套件

怎麼用呢?先Using

1
2
using NSubstitute;

然後…

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
[TestClass()]
public class PTXTests
{
[TestMethod()]
public void GetTest_傳入縣市和公車路線名稱_如果查的到資料_應回傳該路線的BusRouteDTO()
{
//arrange
//透過NSubstitute跟它說你想Mock實作的Interface
var NsubRestSharpMock = Substitute.For<IRestSharp>();
var Sut = new PTX(NsubRestSharpMock); //注入

//期望API回傳的Json
var sb = new System.Text.StringBuilder(12766);
sb.AppendLine(@"[{""RouteUID"":""TPE16111"",""RouteID"":""16111"",""RouteName"":...後略");

//這邊的意思是,當這個透過NSubstitute Mock的物件
//被呼叫Get的方法時,且帶入的參數是我們這邊寫的字串
//就回傳Returns那裡面我們設定的字串
NsubRestSharpMock.Get("http://ptx.transportdata.tw/MOTC/v2/Bus/StopOfRoute/City/Taipei/307?%24top=1&%24format=JSON")
.Returns(sb.ToString());
var City = "Taipei";
var RouteName = "307";

var expected = "307";
//act
var actual = Sut.Get(City, RouteName);

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

因為NSubstitute Mock出來實作IRestSharp的物件也會有Get的方法,他還可以設定帶入參數為何才回傳值,如果比對的Url不符合預期,則就會回傳String的預設值Null,是不是超方便的!!

當然如果你的方法是要回傳物件,一樣往Returns擺進去就對了,且它驗證參數的方法還有很多種而且很彈性,例如帶入如果是數字,你可以說大於100才回傳結果之類的,因為太多有需要的話還是直接參考他的文件比較快。

那今天的部分就談到這邊,能幫助我們做到Mock的套件還有很多,這邊只是其中一種我使用的而已,不一定要一樣,但只要了解原來Mock是這麼回事即可。