ASP單元測試

希望能透過這一篇文章,至少Demo出來怎麼樣透過ASP來做單元測試。謹以此篇獻給還在與ASP奮鬥的朋友們。

我們直接進入正題吧。

假設前端網站現在有一個購物車,使用者購買了好幾樣商品,每一樣商品都有它的價格;為了簡化範例,基本的屬性不會太多,但是真實世界的需求肯定是複雜許多,在這邊我們為了簡化範例,只挑重點的部分,也就是商品的名稱、價格及數量。前端的部分今天不談,我們專注在後端的部分。也就是今天的主題:ASP

小明是個苦逼的碼農,對未來沒有什麼期待,每天的樂趣除了跟客服拉低賽以外,剩下的就是在每個月的發薪日,拿薪水犒勞自己的汗水跟淚水。今天在購物網站看到了好幾樣商品,情不自禁的就給他刷卡買下去了,我們來看一下小明買了甚麼東西。

  1. 任天堂 Nintendo Switch 藍紅手把組,單價9780 x 1
  2. OSIM按摩椅,單價12800 x 1
  3. 柔韌潔淨抽取衛生紙100抽(8包x8串/箱),單價820 x 1
    嗯,看來小明壓力很大,我們也不要糾結商品了,想小明要買甚麼東西還花了我10分鐘。總之就是這樣了。

所以這三樣東西小明應該要付的錢是多少呢?9780 + 12800 + 820 = 23400
OKAY,那就開始我們的第一個測試程式吧….

好像忘了先說,我們在ASP可以透過ASPUNIT來做單元測試,我也是找了很久才找到這一套還可以用的,然後,抱歉我真的忘了出處在哪裡,畢竟是好多年前找到的東西。之前找到的時候有順便調整了一下,把訊息改成我比較喜歡的,然後改來改去,我發現原始版本忘了放哪邊了,好了,閒話到這邊,最終這篇文章的程式會放在GitHub,有興趣請自取。

aspunit單元測試框架必須要先架在IIS上面讓他先跑起來,這次的練習中,瀏覽器網址填入的路徑就指向/test/go.asp。

新增測試程式

因為打算要做的是購物車的價格計算,預期的Service類別應該就叫做Cart,這個類別將負責計算商品價格總價,所以我們就分別在Services目錄下建立一個Cart.asp,然後再Test目錄下建立一個CartTest.asp吧。

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
' /Test/CartTest.asp
<!-- #include virtual="/Services/Cart.asp" -->
<%
Class CartTest ' Extends TestCase
Private target
Public Function TestCaseNames()

End Function

Public Sub SetUp()
set target = new Cart
End Sub

Public Sub TearDown()
Set target = Nothing
End Sub


Public Sub AddNumberTest(oTestResult)
'oTestResult.AssertEquals expected, actual
End Sub
End Class
%>

'/Services/Cart.asp
<%
Class Cart
Private Sub Class_Initialize()
End Sub
Private Sub Class_Terminate()
End Sub

Public Default function Init()
Set Init = Me
End function

End Class
%>

調整測試程式,使其可以運作

習慣上我會將asp的class基本語法以及常用的東西放在snippet裡面,因為asp沒有intellisense可以用,還是不要考驗自己的記憶力吧。而為了測試框架是否真的可以用,我會先讓他可以正常運作,之後才會開始開發。

而為了在asp環境偵錯,我也會先把框架的On Error Resume Next語法給註解掉,這樣在撰寫過程中真的有錯誤的話,可以直接透過IIS設定,將錯誤顯示在瀏覽器上,從而找到是哪一段出錯。

開始認真撰寫第一個測試程式

藉由TDD的概念,首先我們要先想好,我們期望未來Client端應該怎麼使用我們的程式。當然一定是越簡單越好,一看就知道怎麼用,然後很清楚明白這樣。

1
2
3
4
5
6
7
8
9
10
Public Sub ProductsShouldPay_23400(oTestResult)
dim productA : set productA = (new Product)("任天堂 Nintendo Switch 藍紅手把組", 9780, 1)
dim productB : set productB = (new Product)("OSIM按摩椅", 12800, 1)
dim productC : set productC = (new Product)("柔韌潔淨抽取衛生紙100抽(8包x8串/箱)", 820, 1)
dim products : products = Array(productA, productB, productC)

dim actual : actual = 23400
dim expected : expected = target.ShouldPay(products)
oTestResult.AssertEquals expected, actual
End Sub

我們用到了一個商品類別(Product),透過建構式注入了商品的名稱、單價及數量,接著將牠放到陣列裡面全部塞給購物車,最後購物車應該要付23400塊錢。

因為用到了一個還不存在的商品類別,所以我們需要設計一下這個資料傳輸物件類別,讓測試程式能正常運行,順便將Service的ShouldPay()直接回傳0,讓他出現第一個紅燈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Class Product 
public Name '名稱
public Price '單價
public Qty '數量

Private Sub Class_Initialize()
End Sub
Private Sub Class_Terminate()
End Sub

Public Default function Init(productName, productPrice, productQty)
Name = productName
Price = productPrice
Qty = productQty
Set Init = Me
End function
End Class

點亮第一個綠燈

1
2
3
4
5
6
7
public function ShouldPay(products)
dim result : result = 0
dim prod : for each prod in products
result = result + ( prod.Price * prod.Qty )
next
ShouldPay = result
end function

TDD的概念雖然有一個BabyStep的部分,照理說我不應該在這一個測試就直接寫成單價乘數量加總,因為測試案例中並沒有體現這一點。不過當你如果很熟悉的話,自然這個步伐可以稍微的跨大一點。所以,在TDD開發中,測試案例的撰寫是一個非常重要的學問,因為每個測試案例都應該是一個關鍵的情境,關於這個有一本書很推薦 驗收測試驅動開發 ATDD實例詳解。

重構

剛剛測試程式那邊在產生假資料的時候還在用productABC,最後還要用一個Array把它串再一起。如果東西很多我不就看的很累?所以我決定把它抽象出來,主要的程式就保留意圖就好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Public Sub ProductsShouldPay_23400(oTestResult)
dim products : products = GetWantedProducts()
dim actual : actual = 23400
dim expected : expected = target.ShouldPay(products)
oTestResult.AssertEquals expected, actual
End Sub

private function GetWantedProducts()
dim result() : redim result(-1)
ReDim Preserve result(UBound(result) + 1) : set result(UBound(result)) = (new Product)("任天堂 Nintendo Switch 藍紅手把組", 9780, 1)
ReDim Preserve result(UBound(result) + 1) : set result(UBound(result)) = (new Product)("OSIM按摩椅", 12800, 1)
ReDim Preserve result(UBound(result) + 1) : set result(UBound(result)) = (new Product)("柔韌潔淨抽取衛生紙100抽(8包x8串/箱)", 820, 1)
GetWantedProducts = result
end function

重構完畢,測試看看有沒有問題。這個步驟很重要喔,改壞了馬上就知道。

加入會員打折的規則

我們接下來來模擬一個需求,如果客戶是VIP會員的話,那就打八折吧。

我希望在計算的時候從外面傳入參數,直接告訴我會員的身份。而不用建構式注入的方式給購物車,這樣子購物車就不會跟會員身份綁在一起。萬一哪一天要做兩種價格的呈現讓使用者一次看到,喔,原來我如果是VIP會員的話,我比一般會員節省了多少錢這樣。。。。這樣子的話我只要呼叫兩次方法,而不用建立兩個Cart的instance

這邊因為沒有C#的Enum可以用,我們還是用傳統的字串來表明身份吧。同時因為我們以前的情境也不可以壞掉,所以我會再補上一個測試案例給VIP,而一般會員與VIP的測試案例,在呼叫Cart.ShouldPay的時候,因為簽章方法需要額外的會員身份,所以也要跟著改。很重要的一點是,既然加入了一些新的變因,原先的測試案例名稱好像也顯得不合時宜,需要一併調整。


第二個紅燈出現,讓我們來消滅它吧。

到這邊為止,大概就是Demo一下如何在ASP裡面也可以透過單元測試來保護你的程式。

當然這個範例還可以繼續玩下去,玩得很誇張,把一些設計模式加入進來,例如用策略模式來替換掉ShouldPay裡面的演算法。但是我現在這篇文章已經打了三小時,肚子很餓了。

以後有衝動再寫吧。