首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

MVVM 打造可測試表示層:從 Charmed 框架看 ViewModel 單元測試實戰全流程

     編輯:關於.NET

WPF 引入數據綁定與 MVVM 模式,使表示邏輯從 View 代碼後置剝離,成為可獨立實例化、無生命週期綁定的 POCO 類,從而實現真正的單元測試。本文以開源框架 Charmed 及示例應用 Charmed Reader(跨平台博客閱讀器,支援 Windows 8 / Phone 8)為載體,完整演示 ViewModel 測試策略、依賴注入、Mock 手工實作、導航與輔助磁貼測試。

單元測試三段式規範

[TestMethod]
public void SomeTestMethod() {
    // Arrange - 構建被測對象與依賴
    // Act     - 呼叫目標方法/屬性
    // Assert  - 驗證結果符合預期
}

核心架構:介面隔離與依賴注入

IRssFeedService 抽象 RSS 下載/解析,MainViewModel 僅依賴介面:

public interface IRssFeedService {
    Task<List<FeedData>> GetFeedsAsync();
}
public class MainViewModel {
    private readonly IRssFeedService _rssService;
    public MainViewModel(IRssFeedService rssService, ...) { _rssService = rssService; }
    public List<FeedData> FeedData { get; private set; }
    public async Task LoadFeeds() { FeedData = await _rssService.GetFeedsAsync(); }
}

手工 Mock 實現 (無動態 Mock 框架平台)

public class RssFeedServiceMock : IRssFeedService {
    public Func<List<FeedData>> GetFeedsAsyncDelegate { get; set; }
    public Task<List<FeedData>> GetFeedsAsync() =>
        GetFeedsAsyncDelegate != null ? Task.FromResult(GetFeedsAsyncDelegate()) : Task.FromResult<List<FeedData>>(null);
}

測試數據加載交互

[TestMethod]
public void FeedData() {
    // Arrange
    var vm = GetViewModel();
    var expected = new List<FeedData>();
    this.RssFeedService.GetFeedsAsyncDelegate = () => expected;
    // Act
    var actual = vm.FeedData;
    // Assert
    Assert.AreSame(expected, actual);
}

可測試導航:INavigator 抽象

public interface INavigator {
    bool CanGoBack { get; }
    void GoBack();
    void NavigateToViewModel<TViewModel>(object parameter = null);
}
public class NavigatorMock : INavigator {
    public Action<Type, object> NavigateToViewModelDelegate { get; set; }
    public void NavigateToViewModel<TViewModel>(object parameter = null) =>
        NavigateToViewModelDelegate?.Invoke(typeof(TViewModel), parameter);
}
[TestMethod]
public void ViewFeed() {
    var vm = GetViewModel();
    var expectedItem = new FeedItem();
    Type actualVmType = null; FeedItem actualItem = null;
    this.Navigator.NavigateToViewModelDelegate = (t, p) => { actualVmType = t; actualItem = p as FeedItem; };
    vm.ViewFeed(expectedItem);
    Assert.AreSame(expectedItem, actualItem);
    Assert.AreEqual(typeof(FeedItemViewModel), actualVmType);
}

可測試輔助磁貼:ISecondaryPinner 雙平台統一介面

Windows 8 / Phone 8 磁貼 API 差異大,統一介面 ISecondaryPinner + DTO TileInfo,測試驗證參數組裝與成功/失敗分支:

[TestMethod]
public async Task Pin_PinSucceeded() {
    var vm = GetViewModel(); vm.LoadState(feedItem, null);
    Placement actualPlacement = Placement.Default; TileInfo actualTile = null;
    SecondaryPinner.PinDelegate = (ti) => { actualPlacement = ti.RequestPlacement; actualTile = ti; return true; };
    string actualKey = null; List<FeedItem> actualItems = null;
    Storage.SaveAsyncDelegate = (k, v) => { actualKey = k; actualItems = (List<FeedItem>)v; };
    await vm.Pin(null);
    Assert.AreEqual(Placement.Above, actualPlacement);
    Assert.AreEqual(Constants.PinnedFeedItemsKey, actualKey);
    Assert.IsNotNull(actualItems);
}
[TestMethod]
public async Task Pin_PinNotSucceeded() {
    // ... PinDelegate 回傳 false
    Assert.IsFalse(wasSaveCalled); // 失敗時不應呼叫儲存
}

關鍵經驗總結

  • 事先規劃可測試性:靜態方法、DB 訪問、Web 呼叫全部介面化
  • ViewModel 無 View 依賴:跨平台複用核心邏輯
  • 手工 Mock 以委托為核心:無動態框架時的通用模式
  • 測試即文檔:Arrange/Act/Assert 結構讓新人快速理解預期行為

完整代碼見 GitHub: github.com/brentedwards/Charmed

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved