iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 5
1
Modern Web

高效 Coding 術:Angular Schematics 實戰三十天系列 第 5

[高效 Coding 術:Angular Schematics 實戰三十天] Day04 - 為你的 Schematics 撰寫測試程式

什麼是測試?

「想像一下現在你的面前有兩台飛機,一台從零件、製程到組裝都經過完整的測試,而另外一台僅經過飛行的測試,你會想要搭哪一台飛機?」

很多人以為測試,就是找個人、打開瀏覽器、拿著滑鼠點一點就叫測試,但事情其實並沒有這麼簡單。

測試在工程上一直都是一項非常重要且關鍵的工作,它是整個開發流程的其中一環,也是掌控品質的手段之一。它的本質在於預防,無論程式有多精良,一但將其進行組裝時,也會很容易遇到無法解釋的問題,造成功能上的失效。而一段可被自動化測試的程式,也可以在無形中增加工程師信心。

只是依台灣目前的軟體產業生態,測試這件事一直沒有得到應有的重視,即便是具規模的軟體公司也未必擁有完備的軟體測試政策或流程,真的是非常可惜。

為什麼不寫測試

以下是一些常聽到不寫測試或是覺得測試沒必要的原因:

  • 都沒時間開發了,哪還有時間寫測試。
  • 之前本來就沒在寫了。
  • 測試很難維護。

大家回想一下平常我們在開發的時候,最常做的事情是什麼?

功能開發並驗證功能是否正常運作

假設有兩個工程師:小明與小華,他們各方面的能力相近,小明每個功能需要花費十分鐘的時間開發,驗證該功能需要花費一分鐘,總計花費十一分鐘;小華跟小明一樣,每個需要花費十分鐘的時間開發,另外再花十分鐘的時間寫測試,但驗證只需花費十秒鐘,總計花費二十分十秒鐘。

沒錯,就數字上來看,小華所花費的時間比小明多一倍。但通常一個功能開發完之後,都會伴隨著更多的調整。

假設小明每次調整需要花費一分鐘,驗證該次調整也一樣需要花費一分鐘,總計兩分鐘;小華每次調整也都跟小明一樣需花費一分鐘,但小華因為已經有寫好了測試,所以驗證該次調整只需花費十秒鐘,總計一分零十秒。

雖然寫測試在開發時期所花費的時間較多,但開發在整個軟體開發週期佔比其實不多,反而是後續的調整與維護才是最花時間的。從以上敘述中不難看出,只要願意投資時間在測試上,一則這個花費慢慢地就能拉平,甚至可以節省不少時間;二則提高程式碼品質,增加工程師的自信心。

其實大部份真正的理由,大多是不知道怎麼寫測試,或是不知道哪些該測試,甚至是根本不知道有測試這回事。

無論最終是否真的會寫,筆者還是要教大家如何撰寫 Schematics 的測試程式,只要有興趣、想學,隨時都能夠回來看看,而未來的每一個範例,也都會連撰寫測試的部份一起分享給大家。

如何撰寫 Schematics 的測試程式?

Schematics 的測試程式主要是使用 Jasmine 的語法與 API 來撰寫,這個部分由於網路上與 Jasmine 官方都已經有很多相關資源,筆者在這邊就不詳加介紹。

接著打開 Schematics 一開始在產生專案時,一併產生出來的檔案 — index_spec.ts

import { Tree } from '@angular-devkit/schematics';
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
import * as path from 'path';

const collectionPath = path.join(__dirname, '../collection.json');

describe('hello-world', () => {
  it('works', () => {
    const runner = new SchematicTestRunner('schematics', collectionPath);
    const tree = runner.runSchematic('hello-world', {}, Tree.empty());
    expect(tree.files).toEqual([]);
  });
});

筆者先稍微介紹一下這段程式碼裡的一些關鍵字:

  • collectionPath - 顧名思義,就是這個 Schematics 專案的 collection.json 的相對路徑(相對於 index_spec.ts ),後續會用來解析,基本上是固定寫法。
  • describe - 包含測試的描述與要做哪些測試,單一檔案裡可以有多個 describe ,也可以巢狀包覆,會由上到下依序執行。
  • it - 包含測試個案的描述與要做的測試內容,是主要執行測試的地方。
  • SchematicTestRunner - 專門用來寫 Schematic 測試的型別,基本上也是固定寫法。

至於這段程式碼裡的 it 在做的事情是:

  1. 用當前專案內的 collection.json 產生出一個 SchematicTestRunner
  2. SchematicTestRunner 產生出一個 Tree (也就是最終結果)。其中給 runSchematic 的第一個參數是 Schematic 名稱,也就是 hello-world;第二個參數是要給這個 Schematic 的參數;第三個參數是要給這個 Schematic 的 Tree ,用以模擬現實的專案環境 。
  3. expect 驗證 tree.files 是否等於 [] ;

接著我們可以輸入以下指令執行測試:

npm test

正常如果有通過的話,應該會出現以下圖示:

Imgur

綠色的 . 表示該測試案例通過,有幾個 . 就有幾個測試案例通過;而如果是紅色的 F ,則表示該測試案例不通過,有幾個 F 就有幾個測試案例不通過。

撰寫第一個 Schematics 測試程式

那要如何測試我們昨天做的 Schematic 呢?

先來複習一下昨天在 index.ts 的程式碼:

export function helloWorld(_options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    tree.create(_options.name || 'hello', 'world');
    return tree;
  };
}

所以我們的需求是:

產生一個檔案,如果使用者有給檔名,則檔名為使用者輸入的內容;如果使用者沒給檔名,則檔名為 hello。而檔案內容固定為 world

這個需求有兩個情境,一是有給檔名、二是沒給。我們先從沒給檔名開始:

// 以上省略
describe('hello-world', () => {
  it('使用者沒給檔名,則檔名為 \'hello\'', () => {
    const runner = new SchematicTestRunner('schematics', collectionPath);
    const tree = runner.runSchematic('hello-world', {}, Tree.empty());
    expect(tree.files).toContain('/hello');
  });
});

接著是有給檔名時:

// 以上省略
describe('hello-world', () => {
  it('使用者沒給檔名,則檔名為 \'hello\'', () => {
    // 略
  });
  it('使用者有給檔名,則檔名為使用者給的檔名', () => {
    const fileName = 'Leo';
    const runner = new SchematicTestRunner('schematics', collectionPath);
    const tree = runner.runSchematic('hello-world', { name: fileName }, Tree.empty());
    expect(tree.files).toContain(`/${fileName}`);
  });
});

再加上驗證檔案內容的部份:

// 以上省略
describe('hello-world', () => {
  it('使用者沒給檔名,則檔名為 "/hello",檔案內容為 "world"', () => {
    // 略
    expect(tree.readContent('/hello')).toEqual('world');
  });
  it('使用者有給檔名,則檔名為使用者給的檔名,檔案內容為 "world"', () => {
    // 略
    expect(tree.readContent(`/${fileName}`)).toEqual('world'); 
  });
});

最後將測試程式碼重構一下:

import { Tree } from '@angular-devkit/schematics';
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
import * as path from 'path';

const collectionPath = path.join(__dirname, '../collection.json');

describe('hello-world', () => {
  const expectResult = (fileName?: string) => {
    const fullFileName = `/${fileName || 'hello'}`;
    const params = fileName ? { name: fileName } : {};
    const runner = new SchematicTestRunner('schematics', collectionPath);
    const tree = runner.runSchematic('hello-world', params, Tree.empty())
    expect(tree.files).toContain(fullFileName);
    expect(tree.readContent(fullFileName)).toEqual('world');
  }

  it('使用者沒給檔名,則檔名為 "/hello",檔案內容為 "world"', () => {
    expectResult();
  });
  it('使用者有給檔名,則檔名為使用者給的檔名,檔案內容為 "world"', () => {
    expectResult('Leo');
  });
});

以上每個步驟,筆者其實都有使用 npm test 來確認自己寫的東西是否正確,而且每個驗證的步驟都不用一秒鐘,讓筆者可以非常放心且大膽的重構,這就是寫測試的好處之一。

從上面的程式碼可以看出一件事,寫測試其實就是寫規格,只是我們把規格轉成程式碼的形式呈現而已。

本日結語

希望透過今天的分享,可以讓越來越多開發者能夠體會測試的好處,進而養成寫測試的習慣。

今天的程式碼一樣會上傳到 Github 上讓大家參考,明天筆者將會介紹 JSON Schema 以及它的一些基本用法,敬請期待。

參考資料


上一篇
[高效 Coding 術:Angular Schematics 實戰三十天] Day03 - 第一個 Schematic
下一篇
[高效 Coding 術:Angular Schematics 實戰三十天] Day05 - JSON Schema
系列文
高效 Coding 術:Angular Schematics 實戰三十天32

1 則留言

0
Sonic
iT邦新手 5 級 ‧ 2020-02-07 13:00:32

新手Tips:
執行 npm test 之前,
記得把 day3 加料的 tree.create(options.name || 'hello', 'world'); 移除,
否則會 fail

我要留言

立即登入留言