iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 28
0
Software Development

Dart 語言 - 開啟 Flutter 的鑰匙系列 第 28

Day 28:測試你的代碼

在開發程式的時候,無論是用 TDD 開發或是面對遺留代碼 (Legacy code),單元測試都是一個相當重要的工具。單元測試可以協助開發者確認每一個使用情境都是如預期般運行。

在現今的程式語言開發中,每一個語言都會有它們自己的單元測試,當然 Dart 也不例外。

Dart 的單元測試 package 在 https://pub.dev/packages/test ,目前的版本為 1.15.4。


安裝測試的 package

開啟 pubspec.yaml 檔案,在 dev_dependencies: 新增 test: ^1.15.4

(預設應該就已經有加入 test 的 package,不過可能不是最新的)

name: unit_test_workshop
description: A starting point for Dart libraries or applications.

environment:
  sdk: '>=2.8.1 <3.0.0'

dev_dependencies:
  test: ^1.15.4

第一個單元測試

  • 假設我們的程式架構如下:在 lib 目錄底下,有一個 battery.dart
    • 依照單元測試的慣例,我們在 test 目錄底下新增與待測試檔案相對應路徑的測試檔案,並且命名為 battery_test.dart
root directory --- lib   --- src --- battery_.dart
                |
                |- test  --- src --- battery_test.dart
                |
                -- pubspec.yaml 

我們的 battery.dart 內容如下:

  • 有一個 int 用來儲存電量:powerLevel
  • 有一個 method 用來判斷是否滿電量,若是則回傳 true;反之,回傳 false。
class Battery {
	int powerLevel;

	Battery(this.powerLevel);

	bool isFullPower(){
		return powerLevel == 100;
	}	
}

battery_test.dart 要怎麼寫呢?

每一個測試皆兩個函數,一是 test() ,另一則是 expect()

test():需要填入測試名稱與測試的內容。

expect():需要填入實際值與期望值,若實際值與期望值符合,則通過測試;反之,測試不通過。

import 'package:unit_test_workshop/src/battery.dart';
import 'package:test/test.dart';

void main(){

  test('isFullPower() should return true when powerLevel is 100', (){
      final battery = Battery(100);
      final result = battery.isFullPower();
      expect(result, true);
  });

}

→ 執行測試,果然 pass。

  • 新增一測項,判斷 powerLevel 不足 100 時,isFullPower() 函數是否會回傳 false
import 'package:unit_test_workshop/src/battery.dart';
import 'package:test/test.dart';

void main(){

  test('isFullPower() should return true when powerLevel is 100', (){
      final battery = Battery(100);
      final result = battery.isFullPower();
      expect(result, true);
  });

  test('isFullPower() should return false when powerLevel is 50', (){
      final battery = Battery(50);
      final result = battery.isFullPower();
      expect(result, false);
  });
}

→ 測試成功,代表 isFullPower() 的行為如我們預期。

由於這兩個測項都是測試 isFullPower() 函數,我們可以利用 group() 函數將這兩個測試整合在一起。測試時便可以 group() 為一個單位,進行多個測試。

void main(){
	group('isFullPower()',(){
	    test('should return true when powerLevel is 100', (){
	      final battery = Battery(100);
	      final result = battery.isFullPower();
	      expect(result, true);
	    });
	    test('should return false when powerLevel is 50', (){
	      final battery = Battery(50);
	      final result = battery.isFullPower();
	      expect(result, false);
	    });
	});
}

setUp()

將 powerLevel 改為不在中建構子傳入,改由 setter 來修改

//battery.dart
class Battery {
  int powerLevel;

  Battery([this.powerLevel]);

  bool isFullPower(){
    return powerLevel == 100;
  }
}

我們便可以將實例化 Battery 移到 setUp() 函式中執行:

void main(){
  Battery battery;

  group('isFullPower()',(){

    setUp((){
      battery = Battery();
    });

    test('should return true when powerLevel is 100', (){
      battery.powerLevel = 100;
      final result = battery.isFullPower();
      expect(result, true);
    });

    test('should return false when powerLevel is 50', (){
      battery.powerLevel = 50;
      final result = battery.isFullPower();
      expect(result, false);
    });
  });
}
  • 如此在進行每一次測試的時候,都會產生一個新的 Battery 實例來使用。

equatable package

假設,Battery 可以由外部輸入一個 Json 物件,裡面包含了電池的序號以及電量。

class Battery{
  int powerLevel;
  String serialNo;
  Battery(this.serialNo,[this.powerLevel]);

  bool isFullPower(){
    return powerLevel == 100;
  }

  factory Battery.fromJson(Map<String, dynamic>jsonMap) {
    return Battery(jsonMap['serialNo'], jsonMap['powerLevel']);
  }
}
  • 這邊我新增了一個 fromJson() 函式,這個函是用來解析傳入的Json 物件,並依據這個物件的內容建立一個 Battery 物件出來。
  • 我希望能夠利用單元測試測試這個函式是否正確運行,測試代碼如下:
void main(){
  Battery battery;

	group('fromJson', (){
	    final json = {'serialNo':'fake_serialno', 'powerLevel':50};
	    setUp((){
	      battery = Battery('fake_serialno', 50);
	    });
	
	    test('should return Battery when json is correct', (){
	     final result = Battery.fromJson(json);
	     expect(result, battery);
	    });
	  });

}
  • 可以看到我在 setUp 函數裡面定義了一個預期的 Battery 物件,序號為:fake_serialno,電量為:50。
  • 在下方的測試,我將一組 Json 物件傳入至 Battery.fromJson 中。
  • 預期這兩個物件應該是一樣的,結果呢...
    • 結果測試出現了紅燈。

這是因為兩個物件在比較的時候, Dart 是用它們的 hashCode 來比較,但是建立兩次物件,縱使物件內容相同,其 hashCode 還是不同,所以測試的結果當然會是紅燈 (失敗)。

這時候我們可以安裝 [Equatable package](https://pub.dev/packages/equatable)

將 Battery 類別繼承 Equatable 類,修改如下:

import 'package:equatable/equatable.dart';

class Battery extends Equatable{
  final int powerLevel;
  final String serialNo;
  Battery(this.serialNo,[this.powerLevel]);

  bool isFullPower(){
    return powerLevel == 100;
  }

  factory Battery.fromJson(Map<String, dynamic>jsonMap) {
    return Battery(jsonMap['serialNo'], jsonMap['powerLevel']);
  }

  @override
  List<Object> get props =>[serialNo, powerLevel];
}
  1. 類別的屬性需改為 final ,因為 Equatable 有不可變屬性的註解(@immutable)。
  2. 實作 props 函數,裡面填入需要比較的屬性。

改完 Battery 之後,果然測試通過了。


小結

單元測試在軟體開發是相當重要的一個環節,有了單元測試我們就可以確保我們的修改是如我們所預期,其他人在研究程式碼的時候,也可以依據單元測試來了解該類別的功能。

將需要被測試的內容加入 test() 函數中,再利用 expect() 來判斷運算的結果是否如預期。

若要比較兩個物件是否具有相同的屬性,可以使用 Equatable package 來協助我們用更直覺的方式判斷兩個物件。不過需要注意的是,要將屬性改為不可變的 (final),並且實作 props 函數。


上一篇
Day 27:讓產生器 (Generator) 來產生一連串的同步或異步資料吧。
下一篇
Dart 29:Dart 也有 Mockito!
系列文
Dart 語言 - 開啟 Flutter 的鑰匙30

尚未有邦友留言

立即登入留言