在開發程式的時候,無論是用 TDD 開發或是面對遺留代碼 (Legacy code),單元測試都是一個相當重要的工具。單元測試可以協助開發者確認每一個使用情境都是如預期般運行。
在現今的程式語言開發中,每一個語言都會有它們自己的單元測試,當然 Dart 也不例外。
Dart 的單元測試 package 在 https://pub.dev/packages/test ,目前的版本為 1.15.4。
開啟 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 內容如下:
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。
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);
	    });
	});
}
將 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 可以由外部輸入一個 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);
	    });
	  });
}
這是因為兩個物件在比較的時候, 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];
}
final ,因為 Equatable 有不可變屬性的註解(@immutable)。改完 Battery 之後,果然測試通過了。
單元測試在軟體開發是相當重要的一個環節,有了單元測試我們就可以確保我們的修改是如我們所預期,其他人在研究程式碼的時候,也可以依據單元測試來了解該類別的功能。
將需要被測試的內容加入 test() 函數中,再利用 expect() 來判斷運算的結果是否如預期。
若要比較兩個物件是否具有相同的屬性,可以使用 Equatable package 來協助我們用更直覺的方式判斷兩個物件。不過需要注意的是,要將屬性改為不可變的 (final),並且實作 props 函數。