Categories
程式開發

函數式UI簡介:一種基於模型的方法


本文要點

  • 用戶界面是都反應式系統,由用戶界面應用程序接收的事件與應用程序必須在接口系統上執行的動作之間的關係來確定。
  • 函數式UI是用於用戶界面應用程序的一組實現技術,其強調的是應用程序的效果部分和純函數部分之間要有明確的界限。
  • 用戶界面的行為可通過狀態機建模,狀態機在接收事件時會在界面的不同行為模式之間轉換。狀態機模型可以直觀且經濟地可視化,吸引了各種角色的構建者(產品所有者、測試人員、開發人員),並且能讓設計錯誤在早期開發過程中就暴露出來。
  • 用戶界面模型可以自動生成用戶界面的實現和測試,從而獲得更靈活,更可靠的軟件。
  • 基於屬性的測試和蛻變測試利用自動生成的測試序列來查找錯誤,而不必定義用戶界面對測試序列的完整而準確的響應。這樣的測試技術在兩個流行的C編譯器(GCC和LLVM)中發現了100多個新錯誤。

介紹

函數式UI依賴一種顯式函數關係,將用戶界面接收的事件與界面應用程序必須在接口系統上執行的動作鏈接起來:

(1) (action_n, state_n+1) = f(state_n, event_n),其中:

-n是應用程序處理的第n個事件,

-state_n是處理第n個事件時反應系統的狀態,

-f稱為”反應函數”

這種函數等式已經在Elm前端框架的推廣下廣為人知,並在受Elm啟發的一系列語言和框架中得到使用。

本文介紹了另一種函數式UI技術,其依賴於一種用戶界面應用程序行為的模型。該模型使用了狀態機,將應用程序對事件的反應描述為機器狀態之間的一種轉換

首先將等式(1)中的狀態分為控制狀態和擴展狀態,將等式改寫為狀態機模型的形式。然後,我們提出一種直觀而嚴謹的視覺形式,準確而簡潔地描述應用程序的行為。本文將用一個帶有具體JavaScript實現的示例應用程序來說明該方法。

上一篇文章還解釋了函數式UI如何簡化用戶場景的單元測試來增強應用程序的可靠性。基於模型的測試則更進一步,可以允許開發人員完全或部分自動化代碼生成和用戶場景生成作業。面對大量測試時,基於屬性狀態的測試可以測試接口的特定不變項來檢測出錯誤。用戶界面模型使開發人員可以測試規範(模型),而不是測試特定的實現,從而降低了測試的脆弱程度。這裡也會使用一個示例應用程序來具體解釋。

當模型方法只適合部分應用程序行為時,可將其與類似Elm的方法(等式1所述)混合使用。這種靈活性是使用純函數的直接好處,可以提供更好的可組合性。

使用模型時有多種方式可用來確保規範與實現之間保持一致,這也是在註重安全性的行業中普遍使用模型驅動軟件的原因所在。

使用狀態機對用戶界面行為建模

在上一篇文章中,我們給出了一個簡單的小貓動圖搜索程序的示例:

函數式UI簡介:一種基於模型的方法 1

一種反應函數映射為:

事件 動作
More please 請求一個小貓動圖鏈接,顯示加載中的消息
Ok 顯示一張小貓動圖
Err 顯示一條錯誤消息

下面變複雜一點,要求應用程序在接收“More please”按鈕之前等待圖像鏈接被提取。該應用程序現在有兩種模式,一種是在應用程序正在獲取,另一種是未獲取。這意味著我們有兩種截然不同的反應函數,應用在兩種程序模式中。

當應用程序準備獲取時:

事件 動作 新模式
More please 請求一個小貓動圖鏈接,顯示加載中的消息 忙(Busy)
Ok 忽略 就緒(Ready)
Err 忽略 就緒

當應用程序忙於獲取時:

事件 動作 新模式
More please 忽略
Ok 顯示一張小貓動圖 就緒
Err 顯示一條錯誤消息 就緒

可以在上述基本等式中將模式(稱為控制狀態)與其餘狀態(稱為擴展狀態)分離開來表示這種情況。在這裡:

f :: State -> Event ->  (Actions, State)

變成:

f :: (ControlState, ExtendedState) -> Event -> (Actions, (ControlState, ExtendedState))

從中我們可以根據模式(控制狀態)選擇要應用哪個子反應函數:

f :: (ControlState, ExtendedState) -> Event -> (Actions, (ControlState, ExtendedState))
f (cs, es) e = case cs of
  -- application is not busy fetching
  ready -> f_ready es e
  -- application is busy fetching
  busy  -> f_busy es e

事實上,許多用戶界面都表現出以離散模式為特徵的行為,其中反應函數具有更簡單的形式。 Web應用程序中的路由是一個經典例子。對於應用程序的每個路由,都會有一個單獨的反應函數來計算對事件的反應。對於具有兩個路由(home和about)的應用程序,在偽代碼中,我們將有以下類型的內容:

f (cs, es) event = case cs of
      home  -> f_home es event
      about -> f_about es event

另一個經典例子是一款街機遊戲,其中玩家的輸入將根據玩家角色在特定時點的行為而導致不同的動作:

f (cs, es) event = case cs of
      standing -> f_standing es event
      ducking  -> f_ducking es event
      jumping  -> f_jumping es event
      diving   -> f_diving es event

我們將模式稱為”控制狀態”(control state),因為我們根據模式的值將正在運行的應用程序的控制流從反應函數轉移到某個子反應函數。我們將其餘狀態稱為”擴展狀態”(extended state),因為這種形式的反應函數描述了一種稱為擴展狀態轉換器的狀態機。擴展狀態轉換器帶有內存的狀態機,可處理輸入並產生輸出。狀態轉換器和f之間的關係如下:

  • 機器的狀態是上述模式(控制狀態),
  • 機器的內存是擴展狀態,
  • f輸出的動作(actions)是機器的輸出,
  • 由f計算的擴展狀態和控制狀態定義狀態機的轉換

總而言之,應用程序行為的規範由反應函數來描述。該反應函數可以寫為一個狀態轉換器,其在接收輸入(事件),和在計算模式(控制狀態)之間轉換時,更新其內存(擴展狀態)並輸出要執行的命令。這有什麼用呢?

走向視覺形式

狀態轉換器可以通過鏈接機器狀態的圖形直觀而準確地表示出來,並且用戶場景就是該圖中的路徑。之前舉例的Elm應用程序的用戶界面行為可以總結為如下的圖像:

函數式UI簡介:一種基於模型的方法 2

這裡使用的視覺形式非常簡單。這兩個節點是我們修改後的提取小貓動圖的應用程序的兩個控制狀態。節點之間的邊標記的是應用程序處理的事件。標記為e/c的邊有事件e和命令c,而將節點A鏈接到節點B的是以下語義的可視化表示:**假定(given)應用程序處於控件狀態A,則當(when)事件e發生時,那麼(then)**應用程序的新控制狀態為B,而命令c由反應函數返回。

應用程序會接收事件,但既不會導致輸出,也不會通過反應函數改變控制狀態的事件是不會被表示的。例如,假設應用程序處於Busy控制狀態,則在發生More Please事件時,應用程序應忽略該事件,這意味著反應函數會返回一個空輸出。因此,沒有哪條邊具有More Please事件並保持Busy控制狀態。相反,在Ready控制狀態下就存在這樣的邊。

這種視覺表示有一些顯而易見的重要好處:

  • 它是應用程序行為的簡潔而準確的表示:它明確回答了以下問題,那就是”事件X發生時會發生什麼?”
  • 只要遵循圖形的給定路徑,就可以輕鬆地可視化用戶場景(前文加粗的BDD術語given/when/then就是為了說明這一點)
  • 它的行為描述也可能比等效的代碼更具可讀性,至少對於那些可能不熟悉編程奧秘的讀者而言是這樣的。

此外很重要的是,我們先前是從反應函數的偽代碼來實現可視化的。相反的路子往往更有價值:首先繪製可視化圖形,然後編寫或自動生成與之匹配的代碼。為此,必須使用一種可視化語言來描述可視化圖像,其中可視化語言的語義可以復制必要的代碼語義。有許多這樣的可視化語言,它們大多是從David HarelStatecharts上的開創性工作中獲得了啟發,並做出了改進。本文使用Kingly狀態機庫中定義的可視化語言,其中Kingly用於實現所提供的示例。

如果能有一種可視化、像代碼一樣精確的規範語言,可能會極大地幫助程序員,開發出更強大、更可靠的軟件。這是為什麼?圖靈獎的獲得者Frederick Brooks在他的著名文章《沒有銀彈——軟件工程的本質與意外》中,將與問題空間相關的基本複雜性與給定解決方案空間中的偶然複雜性區分開來。 Brooks進一步闡述了他的理念,那就是隨著系統所經歷的狀態數量呈指數級增長,難以理解正在開發的軟件應做哪些工作,以及與他人交流和軟件固有的無法可視化屬性是現代軟件開發麵臨的核心難題:

開發軟件產品時遇到的許多經典問題都源於這種基本的複雜性,並且其非線性會隨著開發規模的增長而增加。由於這種複雜性,團隊成員之間難以溝通,從而導致產品缺陷、成本超支和進度延遲。這種複雜性導致開發人員難以枚舉且難以理解程序所有的可能狀態,由此降低了軟件的可靠性。
(……)
構建軟件的困難之處在於決定該做什麼,而不是具體做事的過程。
(……)
儘管行業在限制和簡化軟件結構方面取得了進步,但它們本質上仍然是不可見的,因此開發人員無法在頭腦中使用一些最強大的概念工具。這種缺失不僅影響了人們腦中的設計過程,而且嚴重阻礙了人與人之間的交流。

可以解決前述痛點的可視化規範語言是降低軟件本質複雜性的良好備選方案。回到前面提到的街機遊戲。下圖:

函數式UI簡介:一種基於模型的方法 3
(來源在這裡

這里以遊戲設計師、項目所有者或遊戲開發人員可以快速理解的方式可視化遊戲需求的特定部分,而無需編寫任何代碼。在討論和探索需求時可以繼續使用這種視覺形式,然後用事件和之前提到的動作來標記圖的邊,從而增強精度。實際上,圖像可以做得足夠精確,乃至可以自動從中生成代碼。

對抗非線性狀態增長的層次結構和歷史記錄

雖然上文的視覺形式解決了Brooks所關註一部分問題,也就是決定該做什麼和人與人之間的溝通障礙,但如果不釐清快速增長的狀態,可視化也是無濟於事的。這可以通過以下方法實現:

  • 僅表示以某種方式更改機器狀態(控制狀態,擴展狀態)或產生輸出的轉換,
  • 使用擴展狀態來捕獲控制流中不包含的狀態,
  • 將機器的控制狀態描述為一種層次結構,
  • 使用這種層次結構將多個轉換解構為一個,
  • 添加一種方法來恢復機器的過去配置(歷史記錄機制),

我們來詳細介紹最後三個項目。

用層次結構解構行為

請注意,反應子函數的簽名類似於頂級反應函數。這意味著將狀態分為控制狀態和擴展狀態的過程可以遞歸應用。設想一個有多個路由的應用程序。先前的示例有兩條路由:

f :: (ControlState, ExtendedState) -> Event -> (Actions, (ControlState, ExtendedState))
f (cs, es) event = case cs of
      home  -> f_home es event
      about -> f_about es event

假設About頁面有一個Team子路由和一個Main索引路由,我們可以依次編寫:

f_about :: (ControlState, ExtendedState') -> Event -> (Actions, (ControlState, ExtendedState'))
f_about (cs, es) event = case cs of
      index -> f_about_index es event
      team  -> f_about_team es event

由於必要時還可以擴展f_about_team和任何反應子函數,因此自然會出現控制狀態樹。該樹可以視覺化表示。來看一下帶有嵌套路由的更複雜的示例:

函數式UI簡介:一種基於模型的方法 4

可以看到控制狀態的層次結構由包含關係反映出來。例如,About detail控制狀態包含在About控制狀態中,而其本身包含了Team控制狀態。

此外同樣重要的是,包含關係可用於解構反應。例如在規範級別,無論應用程序處於什麼狀態,當更改URL時,應用程序都應顯示與新URL對應的路由。在可視化級別,我們有五個控制狀態(實際上是七個,沒有事件觸發的轉換的兩個Routing控制狀態一進入就被捨棄了)。這意味著需要5個邊來表示與路由更改對應的行為。將所有5個狀態都包含在一個單一的包含狀態(App)中後,在App和頂級Routing控制狀態之間就只需要一個邊了。

歷史記錄機制

在反應系統中,經常有必要臨時中斷一個行為,將其替換為另一個行為,然後恢復被中斷的行為。

回想一下之前應用程序路由行為的可視化圖像,當應用程序更改url時,狀態機將轉換為Routing控制狀態並確定是否允許更改路由。如果不是這種情況,就必須回到我們原來的位置。

在退出時記住這個位置並在進入時恢復它(在App控制狀態下轉換為帶圈的H)就能做到這一點。

有了所有之前的約定,可視化圖像就可以清楚地說明在應用程序的給定狀態下哪些動作是可能的、禁止的或必鬚髮生的。現在來看一個具體的JavaScript示例。

示例

考慮一個雙人國際象棋遊戲的用戶界面:

函數式UI簡介:一種基於模型的方法 5

其行為大致如下:

  • 白棋和黑棋交替行動
  • 遊戲從白棋開始
  • 白棋移動時,首先選擇(單擊)要移動的棋子,然後單擊該棋子的目的地
  • 如果目的地是有效的(符合國際象棋遊戲規則),則該棋子將被移動,然後黑棋開始行動
  • 相同的規則適用於黑棋(選擇棋子並指示目的地)。黑棋成功行動後,輪到白棋
  • 直到某一回合結束遊戲

這樣,我們就有三種表現出不同行為的模式:白棋回合(white pieces turn),黑棋回合(black pieces turn)和遊戲結束(game over)。看看在這些模式下映射的事件〜動作(event – action)。在第一種模式下(白棋回合),界面不應理會用戶對黑棋的點擊。當用戶單擊一粒白棋,應高亮顯示該棋。在第二種模式下反之亦然。在遊戲結束模式下,不應理會任何點擊:

控制狀態 事件 動作
白棋回合 點擊黑棋
白棋回合 點擊白棋 高亮棋子
黑棋回合 點擊白棋
黑棋回合 點擊黑棋 高亮棋子
遊戲結束 任意點擊

這些模式可以進一步完善。在“白棋回合”模式下,單擊一粒白棋后,我們將再次改變行為。具體而言,下一個單擊的方塊可能是另一粒白棋,或者是所選白棋要移動到的有效目的地,或者是無效目的地。在第一種情況下,界面應高亮顯示新選擇的白棋,並按照之前一樣操作。在第二種情況下,界面應在其目標位置顯示選定的棋子(即執行移動操作)。在第三種情況下,界面應忽略點擊。

因此我們確定了另外兩個屬於”白棋回合”模式的子模式:一個是在選擇某個塊之前應用程序所處的模式,以及選擇一塊之後的模式。 “白棋回合”詳細的事件〜動作映射如下所示:

控制狀態 事件 動作
白棋回合 點擊黑棋
白棋回合 白棋行動 點擊白棋 高亮棋子,新模式是”選定方塊”(piece selected)
白棋回合 選定方塊 選擇的方塊是取勝行動 移動棋子,新模式是”遊戲結束”
白棋回合 選定方塊 選擇的方塊是非取勝行動 移動棋子,新模式是”黑棋行動”(black piece plays)
白棋回合 選定方塊 選擇的方塊是無效目的地

一旦我們確定了所有模式(控制狀態),就可以將它們鏈接在一個圖中,其中節點是控制狀態,並且鏈接(也稱為轉換)反映事件〜動作的關係:

函數式UI簡介:一種基於模型的方法 6

這裡先不談可視化圖像的細節。註解event [guard] / action用於標記控制狀態之間的轉換,並且在進入分層控制狀態(例如”白棋回合”)後立即進行初始轉換(也就是具有原始控制狀態init的轉換)。

現在添加一個撤消(Undo)功能。添加兩個新的邊(下面的紅色轉換部分)來更新對應的建模,這些新邊對應於應用程序的“黑棋回合”或“白棋回合”轉換狀態中的“撤消”按鈕點擊:

函數式UI簡介:一種基於模型的方法 7

最後再添加一個功能:遊戲計時器,它將計算從遊戲開始到現在經過的秒數。點擊時,計時器還將暫停並閃爍:

函數式UI簡介:一種基於模型的方法 8

建模使用了一個計時器(timer),其每秒產生一個事件。機器會記住處理計時器事件(遊戲開始時的歷史記錄偽狀態)時的位置,並在完成後恢復遊戲的行為:

函數式UI簡介:一種基於模型的方法 9

再次,新功能在可視化圖中產生了新的邊(紅色),並且沒有修改現有行為建模。前面的兩個模型顯示了層次結構和歷史偽狀態如何實現經濟的行為表示。在設計良好的機器中,行為的增量更改應與建模中的增量更改相對應。

用JavaScript實現

可以利用Kingly狀態機庫來用狀態機實現事件~動作關係。

Kingly有一些教程,包括本文中使用的雙人國際象棋遊戲,以及真實世界的Conduit演示應用的實現。此外還有與ReactVue的預製集成。

下面是第一個國際象棋遊戲迭代的機器代碼示例。用於定義Kingly機器映射的轉換記錄正好與模型化中出現的轉換相對應

const transitions = [
  { from: OFF, event: START, to: WHITE_TURN, action: ACTION_IDENTITY },
  // Defining the automatic transition to the initial control state for the compound state WHITE_TURN
  { from: WHITE_TURN, event: INIT_EVENT, to: WHITE_PLAYS, action: displayInitScreen },
  {
    from: WHITE_PLAYS, event: BOARD_CLICKED, guards: [
      { predicate: isWhitePieceClicked, to: WHITE_PIECE_SELECTED, action: highlightWhiteSelectedPiece }
    ]
  },
  {
    from: WHITE_PIECE_SELECTED, event: BOARD_CLICKED, guards: [
      { predicate: isWhitePieceClicked, to: WHITE_PIECE_SELECTED, action: highlightWhiteSelectedPiece },
      { predicate: isLegalNonWinningWhiteMove, to: BLACK_PLAYS, action: moveWhitePiece },
      { predicate: isLegalWinningWhiteMove, to: GAME_OVER, action: endWhiteGame },
    ]
  },
  {
    from: BLACK_PLAYS, event: BOARD_CLICKED, guards: [
      { predicate: isBlackPieceClicked, to: BLACK_PIECE_SELECTED, action: highlightBlackSelectedPiece }
    ]
  },
  {
    from: BLACK_PIECE_SELECTED, event: BOARD_CLICKED, guards: [
      { predicate: isBlackPieceClicked, to: BLACK_PIECE_SELECTED, action: highlightBlackSelectedPiece },
      { predicate: isLegalNonWinningBlackMove, to: WHITE_PLAYS, action: moveBlackPiece },
      { predicate: isLegalWinningBlackMove, to: GAME_OVER, action: endBlackGame },
    ]
  },
];

// Events handled by the machine
const events = [BOARD_CLICKED, START];
// JSON is used to express state hierarchy
const states = {
  [OFF]: "",
  [WHITE_TURN]: {
    [WHITE_PLAYS]: "",
    [WHITE_PIECE_SELECTED]: ""
  },
  [BLACK_TURN]: {
    [BLACK_PLAYS]: "",
    [BLACK_PIECE_SELECTED]: ""
  },
  [GAME_OVER]: "",
};
const initialControlState = OFF;
const initialExtendedState = ...

// Definition of the fsm modelizing the game behavior
const gameFsmDef = {
  initialControlState,
  initialExtendedState,
  states,
  events,
  transitions,
  updateState
};

// Create the executable machine from the machine definition
const gameFsm = createStateMachine(gameFsmDef, {
// Injecting necessary dependencies
  eventEmitter,
  chessEngine
});

由於可執行狀態機已經封裝了其狀態,因此它僅接收事件(在此相當於用戶點擊棋盤上的事件)。運行的示例可以是:

// Clicking on a white square holding a pawn
gameFsm({CLICKED: "g2"})
-> [{
 command:  "render"
 // Props for the React component rendering the chess board
 params:  {
   boardStyle: {borderRadius: "5px", boxShadow: "0 5px 15px rgba(0,0,0,0.5)"},
   draggable:  false
   onSquareClick:  ƒ onSquareClick(square)
   position:  "start"
   squareStyles: {
     g2: {
       // Style for the highlighted piece
       backgroundColor:  "rgba(255, 255, 0, 0.4)"
     }
   },
  width: 320
 }
}]

// Now clicking on a square that is not accessible per chess game rules
// The machine thus computes no commands (`null` return value)
gameFsm({CLICKED: "g5"})
-> null

// Now clicking on g4 (valid move) to have the g2 pawn move there
gameFsm({CLICKED: "g4"})
-> [{
 command:  "render"
 params: ...
}, {
 command:  "MOVE_PIECE"
 params: {from:  "g2", to:  "g4"}
}]

如前所述,對於每個已處理事件,機器都會在接口系統(此處為屏幕和國際象棋引擎)上生成要執行的命令(render和MOVE_PIECE)。
該機器僅實現應用程序的控制流程:它對國際象棋遊戲一無所知,只不過是兩個玩家在輪流行動而已。關注點很好地分離開來:通過 React組件完成棋盤渲染;應用國際象棋規則並維護棋盤的工作是由國際象棋引擎完成的。同一個機器可以不修改就直接應用於跳棋遊戲。

基於模型的測試

在之前的函數式UI文章中,出於測試目的提供了一個純函數h(等效於反應函數):

h([event_0]) = [action_0]
h([event_0, event_1]) = [action_0, action_1]
h([event_0, event_1, ..., event_n]) = [action_0, action_1, ..., action_n]

我們將其稱為先知(oracle)函數。傳遞給h的輸入事件的順序是特定的用戶場景,相應的輸出是應執行的計算出的命令。因此,可以使用常規的斷言檢查技術對用戶場景進行單元測試。此外,通過狀態機建模,可以以高度的靈活性自動化生成大量測試序列。但是測試序列數量太多的話本身就有問題。
這裡我們會回顧要測試的空間、如何使用模型生成測試序列、開發人員面臨的兩個基本測試問題,以及如何利用基於示例的測試、基於屬性的測試和蛻變測試的組合。對該主題的完整討論將需要單獨成文。以下僅介紹基礎知識。

呈指數增長的測試空間,面臨兩個挑戰

對於長度為n(即由輸入[e_1,…,e_n]組成)的用戶場景,輸入測試空間是事件測試空間的笛卡爾積。回到前文的國際象棋遊戲示例的第一個迭代,長度為2的用戶場景就會是[{CLICKED: square1},{CLICKED: square2}]。因為用戶可以點擊棋盤上的任何方塊,所以CLICKED事件的測試空間為8×8 =64。這樣,長度為n的用戶方案的測試空間為64 ^ n。開發人員經常要面對巨大的測試空間,其隨著用戶場景的長度呈指數增長。

對於測試空間中的任何測試,必須設計一種方法來驗證觀察到的測試結果。因此測試人員面臨兩個挑戰,陳宗岳教授將其稱為可靠測試集問題和先知問題

先知問題是指很難或不可能驗證給定測試場景的測試結果的情況(……)。

可靠測試集問題意味著,由於通常不可能窮舉執行所有可能的測試場景,因此要有效地選擇一部分測試場景(可靠的測試集),並獲得確定程序正確性的能力是一項挑戰。

讓我們看看如何通過實際示例解決這兩個問題。

申請表格嚮導

該示例包含一個多步驟工作流的實際案例(不過可視界面已改為使用開放源碼設計系統,但行為完全沒變)。用戶正在申請一個志願服務機會,為此必須通過一個5步的流程,並且每個步驟都有專用的屏幕。從一個步驟移到另一個步驟時,將驗證用戶輸入的數據,然後異步保存。用戶流程如下:

函數式UI簡介:一種基於模型的方法 10

像顯示的那樣,用戶流程通常可以用作起點,以迭代方式完善為應用程序的精確模型

用戶流以及錯誤和數據獲取流所表示的核心行為的第一個建模如下:

函數式UI簡介:一種基於模型的方法 11

實現的原型如下所示:

函數式UI簡介:一種基於模型的方法 12

該模型可用於解決可靠測試集問題

儘管可以隨機採樣測試空間,但有了接口行為模型就可以選擇一個有趣的測試序列,所謂有趣是指:

  • 它們代表特定的用戶場景,
  • 或滿足某些覆蓋指標,
  • 或滿足特定屬性。

為了說明第一點,在上面的可視化圖中,應用程序的滿意路徑以粗體綠色表示。遵循錯誤路徑(紅色虛線)時,可以將該場景擴展為包括一些驗證錯誤。

為了使人們對建模和實現產生信心,手動選擇一小組涵蓋規範關鍵部分的用戶場景是非常有價值的。應提前計算反應函數的預期輸出,以便隨後與實際輸出做對比。換句話說就是需要一個測試先知

該模型還可用於自動生成測試序列,以滿足某些結構化覆蓋指標。面向數據的覆蓋需要覆蓋擴展狀態測試空間,而基於轉換的覆蓋需要覆蓋控制狀態之間的轉換。常見的基於轉換的覆蓋指標是(按覆蓋範圍順序):

  • 當測試集到達模型中的每個狀態至少一次時,將實現全狀態覆蓋。這樣的覆蓋一般來說效率不夠高,因為行為錯誤只是偶然發現的。
  • 當測試執行模型中的每個轉換至少一次時,就可以實現全轉換覆蓋。這也自動涵蓋了所有狀態。
  • 全部n-轉換覆蓋,意味著測試套件中包含n個或更多轉換的所有可能轉換序列。
  • 當基礎模型圖的所有可能分支都被測試時(控制結構的詳盡測試),即可實現全路徑覆蓋。這對應先前的覆蓋指標,可以做到足夠高的n。
  • 所有單循環路徑所有無循環路徑都是更嚴格的指標,它們關注的是模型中的循環。

下面的簡單模型說明了各項覆蓋指標:

函數式UI簡介:一種基於模型的方法 13

使用一個專用的圖遍歷庫,可以為表格嚮導應用程序創建一個抽象測試套件,該套件滿足了所有單循環路徑指標,最終進行了大約1500次測試!在這些測試中,手動選擇的是4個,總共進行了約50個轉換(超過26個),滿足“所有轉換”的覆蓋指標。這四個轉換所涵蓋的控制狀態如下(nok對應於init偽控制狀態——在可視化圖中為橙色):

[
 "nok","INIT_S","About","About","Question","Question","Teams",
 "Team_Detail","Team_Detail","Team_Detail","Team_Detail","Teams",
 "Review","Question","Review","About","Review","State_Applied"
],
["nok","INIT_S","Question","Review","State_Applied"],
["nok","INIT_S","Teams","State_Applied"],
["nok","INIT_S","Review","Teams","State_Applied"]

四個測試場景的預期結果(先知)是手動計算的。但是,為成千上萬個自動測試計算先知是昂貴或不可能的。這樣,基於屬性的測試將比基於案例的測試更有效地發現錯誤。

用基於屬性的測試和蛻變測試解決先知問題

嚮導應用程序的一個屬性(在這裡是業務規則)可用來在第一個原型實現中發現錯誤:所有訂閱的團隊都必須對激勵問題有一個非空的答案。一個失敗的序列表明,當有一個訂閱的團隊給出了有效的答案,然後刪除該答案,並且用戶返回到Teams屏幕時,就會發生該錯誤。

由於該團隊仍處於訂閱狀態,因此用戶可以使用空白答案進入Review屏幕,這違反了該屬性。進一步的調查表明,根本原因是Back按鈕註冊了空答案,而沒有檢查它是否確實是有效答案。

蛻變屬性涉及不同測試序列的測試結果。例如,這裡按順序訂閱團隊A和B的用戶(序列t1)應與使用相同數據按團隊B和A的順序訂閱的用戶產生相同的應用程序數據(在應用程序流程的最後一步中生成) 。因此,先知函數h使得last(h(t1)) = last(h(t2))。

在沒有先知的情況下,蛻變屬性非常強大:它們不需要測試實際輸出與預期輸出,而是需要測試兩個(或幾個)輸出之間的關係。 《蛻變測試:挑戰與機遇》中提供了關於蛻變測試的完整而出色的評價。這篇文章提到蛻變測試的一大成果是在兩個流行的C編譯器(GCC和LLVM)中發現了100多個新的錯誤。

屬性(不變項、蛻變性質或其他性質)與問題相關,而不是與解決方案相關。因此,可以在發現錯誤的同時修改實現,同時保持基於屬性的測試完好無損。為了幫助識別屬性,高級軟件架構師Scott Wlaschin建議採用定向分類法

總結

用戶界面是通過其事件/動作接口與感興趣的外部系統確定的反應系統,一個純粹的反應函數可將用戶在用戶界面上的動作映射到接口系統上的動作。

可以使用狀態機來對一大類反應系統(這些反應系統具有指示其行為的數量有限的模式)建模,可以將狀態機表達為適用於特定機器狀態的一系列反應子函數式。

模型驅動的開發使人們能夠以一種吸引各種支持者(產品所有者、測試人員和開發人員)的方式經濟地可視化行為。使用函數式UI,可以對用戶場景進行單元測試,從而避免使用複雜的自動化工具和長時間運行的不穩定測試。重要的是,對於基於模型的函數式UI,可以根據模型中編碼的規範自動生成實現和測試。大量且多樣的測試以及基於屬性的技術的應用導致了更高質量的軟件。

不利的一面是,雖然建模是一個迭代過程,但它是一種自上而下的方法,許多開發人員可能沒什麼發言權。要花更多時間考慮系統的規範和屬性,也就是what而不是how,是問題而不是解決方案,這可能需要思維方式的轉變。其次,源自狀態圖的視覺形式不能有效地表示狀態之間的數據流動。轉換顯示了執行流程,但不能代表數據。在大多數情況下,數據變量在圖表表示中不可見。第三,並非所有接口都具有有限(或可管理)數量的行為模式。在某些情況下,狀態機建模增加的複雜性可能會比其減少的複雜性更多。

在註重安全的領域中,基於模型的函數式UI已被廣泛用於嵌入式系統的接口原型。 Esterel Technologies的創始人EricBantégnie在接受The Atlantic編輯的James Somers採訪時解釋說(Esterel Technologies是一家開發基於模型的設計工具的公司):

沒有人會手工建造汽車。 (……)在許多地方,代碼仍然是手工藝品。當你手工編寫10,000行代碼時沒什麼問題。但是你擁有的系統,拿空客來說會有3000萬行代碼,或者特斯拉有1億行代碼[……]——這就會變得非常非常複雜了。

參考鏈接

作者介紹:

Bruno Couriol擁有法國格蘭德高等商學院的電信理學碩士學位、數學學士學位和歐洲工商管理學院的工商管理碩士學位。他的大部分職業生涯都是作為業務顧問,幫助大型公司解決其關鍵的戰略、組織和技術問題。在過去的幾年中,他專注於業務、技術和企業家精神的融合。

原文鏈接:

Functional UI – a Model-Based Approach