測試的目的
不知道大家有沒有那種,隨著專案功能越來越多,一不小心就把舊的功能改壞的經歷?
或是手動重複做相同的測試,花費大量時間呢?
測試可以幫助減少這些問題,除此之外還有:
- 測試結果是否符合預期
- 提供一個範例:假如這個功能很複雜,查看測試可以快速了解這個功能的用處
- 提供類似規格書的功能:查看test就可以大概知曉這個class開發了哪些功能
- 方便重構或優化:重構以後還可以保證功能沒有壞掉
測試種類
事先聲明! 我覺得有些專有名詞的定義不是很清晰,有些人的觀點甚至有點衝突,所以這篇都是我查閱許多資料和教學,基於個人理解寫出來的
測試的種類有很多,但這篇只會講我接觸過的三種
- Unit test:專注測試一個class
- Feature test:測試一個功能,確保符合驗收標準
- Browser test:瀏覽器模擬使用者操作
Feature Test相較Unit Test更注重是否符合需求,Unit Test比較能定位問題在哪裡
打個比方,電腦無法開機有很多原因,一個一個單元測試每個東西才能定位原因
Unit Test 跟Feature Test 同樣重要
以測試覆蓋率來講,好像Unit Test寫完就好了?但Feature Test一樣很重要,舉個搞笑的例子,之前我在Laracast這篇貼文看到一個gif
烘手機和垃圾桶的單元測試都沒問題,組合在一起就出事(吹葉機?)
所以我們需要功能測試確保各個部件組合出來的功能正常
Unit Test
測試的結構
我通常會用Given-When-Then的結構寫測試
Given表示前情提要,When表示做了什麼事,Then表示預期的結果
舉一個屬於feature test的例子:
- Given:我是會員
- When:進入首頁
- Then:可以在文章下方看到喜歡、收藏按鈕
Test Double
之前我總是搞不清楚這是什麼,在這裡分享我自己的理解
Double 是測試的某種替代品,我們不用關心裡面怎麼實作,它是
Double常用於第三方的API,例如丟檔案到S3、寄信等,或是這個API回應時間很長的情況,或是這個API需要付錢等等
當然這種寄信功能我們確實要測試正不正常,但不需要每次都測試,因此可以用Double代替
來看一個PHP Test的例子:
這是一個News class,建立一則新聞的時候,會使用Downloader去S3下載一張圖片作為封面圖
 1// News.php
 2namespace App;
 3
 4class News
 5{
 6    protected $coverImage = null;
 7
 8    public function __construct(ImageDownloader $downloader)
 9    {
10        $this->downloader = $downloader;
11    }
12
13    public function create()
14    {
15        $this->coverImage = $this->downloader->downloadCoverImage();
16    }
17
18    public function hasCoverImage()
19    {
20        return !empty($this->coverImage);
21    }
22}
23
24// ImageDownloader.php
25namespace App;
26
27class ImageDownloader
28{
29    public function downloadCoverImage()
30    {
31        // Download a random image.
32    }
33
34    public function downloadIcon()
35    {
36        // Download a icon.
37    }
38}
再來看測試,遵循剛才說的Given-When-Then方法:
- Given:建立ImageDownloader和News class
- When:建立一篇新聞
- Then:斷言這篇新聞有封面圖
 1// NewsTest.php
 2namespace Tests;
 3
 4use App\News;
 5use App\ImageDownloader;
 6use PHPUnit\Framework\TestCase;
 7
 8class NewsTest extends TestCase
 9{
10    /** @test */
11    public function download_a_image_when_news_created()
12    {
13        $downloader = new ImageDownloader;
14        $news = new News($downloader);
15        $news->create();
16        $this->assertTrue($news->hasCoverImage());
17    }
18}
用Dummy/Fake改寫
上面提到Downloader會去S3下載圖片,但我又不希望每個新聞測試都真的下載一張圖
現在用Dummy,或者說Fake來改寫,建立一個假的class取代原本的,下載圖片的function永遠只會回傳fake cover
 1namespace Tests;
 2
 3class FakeImageDownloader
 4{
 5    public function downloadCoverImage()
 6    {
 7        return 'fake_cover.jpg';
 8    }
 9
10    public function downloadIcon()
11    {
12        // Download a icon.
13        return 'fake_icon.jpg';
14    }
15}
回頭修改測試 Given的部分,用建立fake物件取代原本的,然後建立News object
 1namespace Tests;
 2
 3use App\News;
 4use PHPUnit\Framework\TestCase;
 5
 6class NewsTest extends TestCase
 7{
 8    /** @test */
 9    public function download_a_image_when_news_created()
10    {
11        $downloader = new FakeImageDownloader;
12        $news = new News($downloader);
13        $news->create();
14        $this->assertTrue($news->hasCoverImage());
15    }
16}
Stub & Mock
每次給一個Test寫一個class,多出新檔案還是挺費事了,可以用Stub或Mock代替
Stub
用createMock建立一個stub,然後設定downloadCoverImage這個function會return stub cover jpg
 1class NewsTest extends TestCase
 2{
 3    /** @test */
 4    public function download_a_image_when_news_created()
 5    {
 6        // stub
 7        $downloader = $this->createMock(ImageDownloader::class);
 8        $downloader->method('downloadCoverImage')->willReturn('stub_cover.jpg');
 9        // downloadIcon根本不會用到,但多寫以下這行也不會噴錯。stub不關心你呼叫了指定的method幾次,甚至不關心是否呼叫過
10        $downloader->method('downloadIcon')->willReturn('stub_icon.jpg');
11        $news = new News($downloader);
12        $news->create();
13        $this->assertTrue($news->hasCoverImage());
14    }
15}
Mock
Mock一樣也是用createMock建立,然後預期downloadCoverImage這個function會被呼叫一次,然後return mock cover jpg
 1class NewsTest extends TestCase
 2{
 3    /** @test */
 4    public function download_a_image_when_news_created()
 5    {
 6        // mock
 7        $downloader = $this->createMock(ImageDownloader::class);
 8        $downloader->expects($this->once())->method('downloadCoverImage')->willReturn('mock_cover.jpg');
 9        // 以下這行會噴錯,因為預期(expects)執行downloadIcon method一次,但實際上沒有
10        $downloader->expects($this->once())->method('downloadIcon')->willReturn('mock_icon.jpg');
11        $news = new News($downloader);
12        $news->create();
13        $this->assertTrue($news->hasCoverImage());
14    }
15}
Stub 和Mock 的區別?
建立新聞的時候不會使用downloadIcon這個function
Stub不關心執行指定的method幾次,甚至不關心是否執行過,因此stub加上下面這行也不會噴錯
而mock,因為mock會預期(expects)執行downloadIcon method一次,但實際上沒有,就會噴錯
我個人認為Mock用在需要嚴謹測試這個被mock的class的時候才需要用到,否則只是一個小改動就要改一堆mock
Browser Test
這裡我用Python Test當範例,因為用Python操作Selenium真的很方便((誠懇
首先我指定使用chrome,然後打開我們的首頁,接下來尋找導航欄的「新聞」,我這裡用css selector尋找元素
之後將由標懸停在這個元素上
最後斷言會在頁面上看到「台股盤勢」四個字
 1import unittest
 2from selenium import webdriver
 3from selenium.webdriver import ActionChains
 4from selenium.webdriver.common.by import By
 5
 6class HomeTest(unittest.TestCase):
 7    def setUp(self):
 8        self.driver = webdriver.Chrome()
 9
10    def test_search_in_python_org(driver):
11        driver = self.driver
12        driver.get("https://www.cnyes.com/")
13        elem = driver.find_element(
14            by=By.CSS_SELECTOR,
15            value='nav ul li a[data-global-ga-label="新聞"]>span'
16        )
17        ActionChains(driver).move_to_element(elem).perform()
18        self.assertIn("台股盤勢", driver.page_source)
19
20    def tearDown(self):
21        self.driver.close()

