# Ceedling 基本教學與心得 C 語言也可以做TDD,C 語言也可以做Unit Test # 主要參考此網頁的Code https://spin.atomicobject.com/2019/02/07/cmock-get-started/ [他的Github](https://github.com/joaniedavis/cmock_example) # Ceedling 概念 主要有個Ruby 的Tool 可以幫助C 語言的使用者達到Unit Test Unit Test 當中有一個最重要的概念是需要純電腦執行,環境是被假設成理想的環境 編寫Unit Test 的人必須知道理想上會得到什麼結果 C 語言最麻煩的部分就是會常常跟硬體有連結,例如I2C ,UART 之類的,但是Ceedling 這個工具包裡面有含有可以模仿(Mock) 的工具,CMock 可以幫助你解決這個問題。 例如:我們讀取I2C Address 0x3E 的溫度IC,他會回傳0x55。我們會利用這個0x55 數值來計算溫度。 ```cpp //File Name get_tempature.h #include <stdint.h> //This Will Return the Tempature uint8_t get_tempature(uint8_t I2C_address); ``` ```cpp= //File Name get_tempature.c #include "get_tempature.h" uint8_t get_tempature(uint8_t I2C_address) { return I2C_GetData(I2C_address) * 5 / 10; // 5/10 就是除與2 } ``` 一般執行的主程式 ```cpp= //File Name mainget.c #include "mainget.h" uint8_t mainget(void) { uint8_t nowTempature = 0; uint8_t I2C_address = 0x3E; nowTempature = get_tempature(I2C_address); return nowTempature; } ``` 要開始使用Ceedling 之前,需要開始學會假設: (這是理想的目標,但如果I2C 不是這樣子想,那也是我們誤會IC 了,需要重新假設。) > I2C Address 0x3E 的溫度IC,他會回傳0x55 例如以下這一段程式碼就是會有假設->執行->確認結果,這三個步驟。 ```cpp= #include "mock_get_tempature.h" //mock_ 這個前綴是固定的(以後也可以改設定改) void test_mainget_NeedToImplement(void) { uint8_t testTempature = 0; get_tempature_ExpectAndReturn(0x3E, 42); testTempature = mainget(); TEST_ASSERT_EQUAL(testTempature, 42 ); } ``` # Ceedling 實際執行步驟 ## 建置環境 ### VS Code Extension https://marketplace.visualstudio.com/items?itemName=numaru.vscode-ceedling-test-adapter ## 指令 ### 建立Project ```bash= ceedling new TempatureSensor cd .\TempatureSensor\ ``` ### 建議手動步驟 如果輸入Ceedling Help 看到: Unknown alias: common_defines 可以先把project.yml 當中的 Common Define 刪掉 ![](https://i.imgur.com/cM2Rmqm.png) > 刪除後 ![](https://i.imgur.com/FC2bNqu.png) ### 建立一個新的模組(Moudle) ```bash= ceedling module:create[mainget] ``` ### 測試編譯環境 ```bash= ceedling test:all ``` 應該會看到 ![](https://i.imgur.com/URHdL3P.png) ### 編寫程式碼(以VS Code 為範例) #### 新增需要的.c 檔案 ```bash= ceedling module:create[get_tempature] ``` | 一開始的狀態 | 新增需要的檔案後與刪除多餘Teset 檔案 | | ---------- | -------- | | ![](https://i.imgur.com/8vgFSzw.png)| ![](https://i.imgur.com/Pa2nZhn.png) | #### C 語言程式碼如下 > test_get_tempature.c ```cpp= #ifdef TEST #include "unity.h" #include "mainget.h" #include "mock_get_tempature.h" void setUp(void) { } void tearDown(void) { } void test_mainget_NeedToImplement(void) { uint8_t testTempature = 0; get_tempature_ExpectAndReturn(0x3E, 42); testTempature = mainget(); TEST_ASSERT_EQUAL(testTempature, 42 ); } #endif // TEST ``` ### 如果故意Expect 輸入填錯,Test code 也會檢查出來 ![](https://i.imgur.com/rJzhhRP.png) # Ceedling 其他 example Project 的程式碼如下 ```cpp= #include <stdbool.h> void rectangle_init(uint16_t length, uint16_t width); uint16_t rectangle_get_area(void); uint16_t rectangle_get_perimeter(void); bool rectangle_is_square(void); ``` ```cpp= #include "rectangle.h" typedef struct rectangle { uint16_t r_length; uint16_t r_width; } rectangle_t; static rectangle_t rect; void rectangle_init(uint16_t length, uint16_t width) { rect.r_length = length; rect.r_width = width; } uint16_t rectangle_get_area(void) { return rect.r_width * rect.r_length; } uint16_t rectangle_get_perimeter(void) { return (rect.r_length + rect.r_width) * 2; } bool rectangle_is_square(void) { return (rect.r_length == rect.r_width); } ``` ```cpp= #include "shape_container.h" void shape_container_init(uint16_t r_length, uint16_t r_width) { rectangle_init(r_length, r_width); } bool shape_container_calc_rect(uint16_t r_length, uint16_t r_width) { rectangle_init(r_length, r_width); rectangle_get_area(); rectangle_get_perimeter(); return rectangle_is_square(); } ``` ```cpp= #ifdef TEST #include "unity.h" #include "shape_container.h" #include "mock_rectangle.h" void setUp(void) { } void tearDown(void) { } void test_shape_container_init(void) { // Set up known values uint16_t length = 4; uint16_t width = 3; //State, in order of call, what expectations we have, and the expected values to be returned, if any rectangle_init_Expect(length, width); //Run Actual Function Under Test shape_container_init(length, width); } void test_shape_container_calc_rect_is_square(void) { // Set up known values uint16_t length = 4; uint16_t width = 4; uint16_t x_area = length * width; uint16_t x_perimeter = length + width + length + width; bool x_is_square = true; //State, in order of call, what expectations we have, and the expected values to be returned, if any rectangle_init_Expect(length, width); rectangle_get_area_ExpectAndReturn(x_area); rectangle_get_perimeter_ExpectAndReturn(x_perimeter); rectangle_is_square_ExpectAndReturn(x_is_square); //Run Actual Function Under Test bool is_square = shape_container_calc_rect(length, width); //We can still verify whatever things we normally would after TEST_ASSERT_EQUAL(x_is_square, is_square); } void test_shape_container_calc_rect_is_not_square(void) { // Set up known values uint16_t length = 4; uint16_t width = 3; uint16_t x_area = length * width; uint16_t x_perimeter = length + width + length + width; bool x_is_square = false; //State, in order of call, what expectations we have, and the expected values to be returned, if any rectangle_init_Expect(length, width); rectangle_get_area_ExpectAndReturn(x_area); rectangle_get_perimeter_ExpectAndReturn(x_perimeter); rectangle_is_square_ExpectAndReturn(x_is_square); //Run Actual Function Under Test //This will Test the shape_container_calc_rect Function inside //rectangle_is_square rectangle_init rectangle_get_area rectangle_get_perimeter bool is_square = shape_container_calc_rect(length, width); //We can still verify whatever things we normally would after TEST_ASSERT_EQUAL(x_is_square, is_square); } #endif // TEST ``` # CMock Creating Skeletons Creating Skeletons 可以加速讓編譯完成,之後再來考慮哪些是需要mock 還是stub 的功能 ## Creating Skeletons: https://github.com/ThrowTheSwitch/CMock/blob/master/docs/CMock_Summary.md ## 遇到的問題 需要先手動建立一個資料夾 mocks,因為預設的設定檔案是這樣子設定。 把需要mock 的header 檔案放在資料夾當中 ![image](https://hackmd.io/_uploads/H1IR4dCJ0.png) ## treat_externs 也可以使用使用命令列的方式傳入,用來debug 使用 ruby C:\Ruby32-x64\lib\ruby\gems\3.2.0\gems\ceedling-0.31.1\vendor\cmock\lib\cmock.rb --skeleton --treat_externs=":include" --skeleton_path=".\mocks"