Try   HackMD

Ceedling 基本教學與心得

C 語言也可以做TDD,C 語言也可以做Unit Test

主要參考此網頁的Code

https://spin.atomicobject.com/2019/02/07/cmock-get-started/
他的Github

Ceedling 概念

主要有個Ruby 的Tool 可以幫助C 語言的使用者達到Unit Test
Unit Test 當中有一個最重要的概念是需要純電腦執行,環境是被假設成理想的環境
編寫Unit Test 的人必須知道理想上會得到什麼結果

C 語言最麻煩的部分就是會常常跟硬體有連結,例如I2C ,UART 之類的,但是Ceedling 這個工具包裡面有含有可以模仿(Mock) 的工具,CMock 可以幫助你解決這個問題。

例如:我們讀取I2C Address 0x3E 的溫度IC,他會回傳0x55。我們會利用這個0x55 數值來計算溫度。

//File Name get_tempature.h
#include <stdint.h>
//This Will Return the Tempature
uint8_t get_tempature(uint8_t I2C_address);
//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 }

一般執行的主程式

//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

例如以下這一段程式碼就是會有假設->執行->確認結果,這三個步驟。

#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

ceedling new TempatureSensor cd .\TempatureSensor\

建議手動步驟

如果輸入Ceedling Help 看到: Unknown alias: common_defines

可以先把project.yml 當中的 Common Define 刪掉

刪除後

建立一個新的模組(Moudle)

ceedling module:create[mainget]

測試編譯環境

ceedling test:all

應該會看到

編寫程式碼(以VS Code 為範例)

新增需要的.c 檔案

ceedling module:create[get_tempature]
一開始的狀態 新增需要的檔案後與刪除多餘Teset 檔案

C 語言程式碼如下

test_get_tempature.c

#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 也會檢查出來

Ceedling 其他 example Project 的程式碼如下

#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);
#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); }
#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(); }
#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

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"