爲Raspberry Pi開發.NET應用程序:第2部分

目錄

介紹

背景

問題描述

解決方案結構

關注點分離

重構

單元測試、模擬和依賴注入

運行代碼

興趣點


一個簡單而現實的Raspberry Pi .NET解決方案,演示了項目佈局,單元測試和最佳實踐。開發是在能夠運行Visual Studio Code的計算機上進行的,該計算機通過SSH遠程連接到Raspberry Pi

介紹

可以在.NET中爲Raspberry Pi編寫應用程序,就像在任何其他環境中一樣。編寫良好的.NET(和C#)代碼的優點是,可以實現更具伸縮性和可維護性的解決方案。在本文中,我將描述可用於創建此類解決方案的項目結構。這將包括單元測試,模擬和依賴注入。該應用程序將是一個簡單的應用程序,但是解決方案結構將實現任何大型開發項目的兩個關鍵目標:

  1. 該代碼應嘗試實現關注點分離
  2. 該代碼應有助於重構

這兩個原則將在本文後面進行演示。我將使用.NET Core IoT,這是Microsoft的官方解決方案,用於與Raspberry Pi等平臺上的低級設備接口。

背景

第1部分:在Raspberry Pi上輕鬆設置.NET Core並使用VS代碼進行遠程調試

在上一篇文章中,我描述瞭如何設置開發環境,該環境將使使用自動化部署和調試的遠程Raspberry Pi開發變得容易。那篇文章包括幾個簡單的程序。本文將以該文章爲基礎,並說明如何創建更現實,更大的解決方案。

問題描述

我爲此示例解決方案選擇了一個簡單的問題。對此的要求很明確,但這是有意的。解決方案結構是重要的部分,並且通過解決一個簡單的問題,這有望使焦點集中在解決方案上。

因此出現了問題:將有兩個按鈕開關和兩個LED。我們將創建一個控制檯應用程序,使每個LED都可以打開或關閉。這些按鈕將識別短按的時間。我們還將有兩種測試模式——一種持續使LED閃爍直到長按按鈕2爲止,另一種顯示消息以表明已按下哪個按鈕,長按按鈕2也會終止該模式。

我已將開關和LED連接到以下GPIO引腳:

顯然,這是一個人爲的例子。一個更現實的解決方案將對輸入和輸出有所幫助,並且可能包括更復雜的設備。在撰寫本文時,IOT項目包括70多個與溫度,加速度計和光傳感器等設備以及許多有線和無線設備的接口。

解決方案結構

頂層結構如下所示:

> .vscode
> Doc
> RpiBlinkButtonApp
> RpiBlinkButtonLib
> RpiBlinkButtonLibTests
> Scripts
.gitignore

有三個代碼文件夾,RpiBlinkBut​​tonAppRpiBlinkBut​​tonLibRpiBlinkBut​​tonLibTests。在更大的解決方案中,可能會有更多的類庫項目,並且每個類庫都應該有自己的Tests項目。可能會有更多Application項目。另外,請注意,我沒有爲該App項目提供單元測試,但是對於真正的解決方案,也應該爲此項目提供單元測試。

.vscode腳本文件夾是基於以前的文章,以及包括腳本和配置,以支持遠程部署和調試。

關注點分離

本文中方法的主要目標之一是關注點分離。這對於較大的解決方案很重要,在這種解決方案中,通常不可能或不希望爲了整個解決方案而理解整個解決方案。爲此,需要有清晰的抽象層。顯而易見的第一個抽象層是實際的物聯網庫本身。值得看一下該存儲庫中的代碼,但理解所有內容以使用它並不是必需的。

我們的第一個抽象是擁有一個類庫項目。在該項目中,我選擇創建兩個控制器類。該LED控制器類封裝了兩個LED——我選擇了有紅色和綠色LED。有兩種方法可以打開或關閉紅色或綠色LED按鈕控制器有兩個按鈕,並且因爲這些是稍微更復雜(例如,要求取消開關),控制器使用另一個類,GpioButton。但是,使用按鈕控制器應該很容易,我決定提供一個ButtonPressed事件,該事件的EventArgs參數描述發生了什麼類型的事件(短按或長按的按鈕12)。

重構

該代碼應該適合重構。與項目開始時一成不變的設計不同,現代代碼應隨着項目的進行不斷地重構。包括VS Code在內的IDE提供了使此過程更容易的工具,但我們還需要確保代碼的複雜性不會失控,因爲一旦發生這種情況,就很難進行重構,並且在某些情況下變得不可能。

重構的另一個關鍵部分是確信重構沒有破壞任何東西。這是進行良好單元測試的關鍵原因之一。

單元測試、模擬和依賴注入

庫中的每個類都有其自己的測試類。理想情況下,我們只想測試該類並把對其他類的調用存根。最好的方法是模擬被調用的類。我已經使用了Moq,可以通過NuGet輕鬆安裝。模擬的關鍵部分是使用依賴注入和控制反轉。他們的主要思想是,我們將實例化的對象傳遞給我們的類,而不是將類創建對象本身。這樣,我們就可以傳遞模擬對象而不是真實對象。這樣的一個例子是在ButtonControllerTests中的CreateMockedObjects()

private MockButtonCollection CreateMockObjects()
{
    var mockObjects = new MockButtonCollection();

    mockObjects.Button1 = new Mock<GpioButton>(null);
    mockObjects.Button2 = new Mock<GpioButton>(null);
    mockObjects.GpioDriver = new Mock<GpioDriver>();

    // Mock the methods to setup a pin
    mockObjects.GpioDriver.Protected().Setup<bool>("IsPinModeSupported", ItExpr.IsAny<int>(),
                                                   ItExpr.IsAny<PinMode>()).Returns(true);

    // Create a ButtonController
    mockObjects.GpioController = new GpioController(PinNumberingScheme.Logical,
                                                      mockObjects.GpioDriver.Object);

    mockObjects.ButtonController = new ButtonController(mockObjects.GpioController,
                                      mockObjects.Button1.Object, mockObjects.Button2.Object);

    // Initialize the button controller
    mockObjects.ButtonController.Initialize();

    return mockObjects;
}

另一個例子是我們傳遞給GpioButton類的DateTimeProvider類。可以如下模擬:

var mockDateTime = new Mock<IDateTimeProvider>();

var dateNow = DateTime.UtcNow;

mockDateTime.SetupSequence(m => m.UtcNow)
  .Returns(dateNow)
  .Returns(dateNow.AddMilliseconds(20))
  .Returns(dateNow.AddMilliseconds(30));

這使我們可以返回特定的值,在這種情況下,第一個調用UtcNow()獲取當前時間,隨後的調用獲取時間+ 20ms,然後是+ 30ms然後,我們可以在GpioButton中控制這樣的代碼:

else if ((_dateTime.UtcNow - PressedStart).TotalMilliseconds >= LONG_PRESS_DURATION)
{
  longReleaseAction();
}

依賴項注入的問題之一是使用不可模擬的第三方類。一個很好的例子是GpioController。爲了使類成爲可模擬的,它需要從接口派生(例如IList),或者需要具有虛擬方法。GpioController兩者都不具備,因此我們無法創建模擬程序GpioController。幸運的是,GpioController構造函數確實將 GpioDriver作爲參數。這使我們可以創建一個模擬GpioDriver。然後,我們可以調用GpioController,將調用我們的模擬程序GpioDriver。一個簡單的例子就是LedControllerTests,在其中我們可以檢查SetLed()方法實際寫入正確的gpio pin

public void SetLed_ShouldCall_DriverWriteMethod(string method, int pin, bool on)
{
  // Given I have created a mock Gpio driver
  var mockDriver = new Mock<GpioDriver>();

  // And I have mocked the methods to write to a pin
  mockDriver.Protected().Setup<bool>("IsPinModeSupported", ItExpr.IsAny<int>(),
                                                  ItExpr.IsAny<PinMode>()).Returns(true);
  mockDriver.Protected().Setup<PinMode>("GetPinMode", ItExpr.IsAny<int>())
                        .Returns(PinMode.Output);

  // And created a LedController
  var gpioController = new GpioController(PinNumberingScheme.Logical, mockDriver.Object);
  var ledController = new LedController(gpioController);
  ledController.Initialize();

  // When I call the controller - e.g. led.SetRed(true)
  typeof(LedController).GetMethod(method).Invoke(ledController, new object[] { on });

  // Then I expect the pin to be written to
  mockDriver.Protected().Verify("Write", Times.Once(), ItExpr.Is<int>(p => p == pin),
                  ItExpr.Is<PinValue>(m => m == (on ? PinValue.High : PinValue.Low)));
}

運行代碼

有兩個步驟可以運行或調試程序。假設已按照這些文章的第1部分設置了Raspberry Pi,則需要兩個步驟。這些步驟是:

  1. 設置Raspberry Pi名稱(通過Ctrl Shift P
  2. 從菜單或活動欄(通常位於左側)運行

興趣點

使用Raspberry Pi C#代碼與處理任何其他C#代碼一樣容易。我們擁有重構、智能感知等所有可用工具,這些工具使我們比在Raspberry Pi上嘗試編寫Raspberry Pi應用程序容易得多。

我在這裏沒有介紹它,但是很明顯,我們可以包含其他任何.NET應用程序中都將包含的功能,例如實體框架,具有廣泛支持的Web應用程序等。

.NET中爲Raspberry Pi進行開發與在PythonJavaScriptC ++中進行開發(在使用此處描述的Raspberry Pi設置在VS Code中都應該可以進行開發)不同,在.NET中,C#代碼在開發機器上,而對於其他語言,源通常位於目標計算機上。我想無論哪種方式都沒關係(儘管您可能有自己的偏好),但是瞭解這一點可以使事情更清楚。

從理論上講,我們可以在開發機器上本地運行代碼(無論是LinuxMacOS還是Windows)。但是,gpio調用將不起作用(如果嘗試使用,則會發現它們拋出不支持的異常)。但是應該可以創建與之通信的模擬器GpioDriver。但是,這超出了本文的範圍!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章