[英]How do I write unit tests for ensuring numerical accuracy in scientific computation in C?
[英]How to write unit tests in plain C?
單元測試只需要可以進行測試的“切割平面”或邊界。 測試不調用其他函數或僅調用其他也被測試的函數的 C 函數是非常簡單的。 這方面的一些示例是執行計算或邏輯操作的函數,並且本質上是函數式的。 功能性的意思是相同的輸入總是導致相同的輸出。 測試這些功能可以帶來巨大的好處,即使它只是通常被認為是單元測試的一小部分。
更復雜的測試,例如使用模擬或存根也是可能的,但它並不像在更動態的語言中那么容易,甚至不像 C++ 那樣只是面向對象的語言。 解決這個問題的一種方法是使用#defines。 一個例子是這篇文章, 單元測試 OpenGL 應用程序,它展示了如何模擬 OpenGL 調用。 這允許您測試是否進行了有效的 OpenGL 調用序列。
另一種選擇是利用弱符號。 例如,所有 MPI API 函數都是弱符號,因此如果您在自己的應用程序中定義相同的符號,您的實現將覆蓋庫中的弱實現。 如果庫中的符號不弱,則在鏈接時會出現重復的符號錯誤。 然后,您可以實現整個 MPI C API 的有效模擬,這使您可以確保調用正確匹配,並且沒有任何可能導致死鎖的額外調用。 也可以使用dlopen()
和dlsym()
加載庫的弱符號,並在必要時傳遞調用。 MPI其實提供了PMPI的符號,很強,所以沒必要用dlopen()
。
您可以實現 C 單元測試的許多好處。它稍微困難一些,並且可能無法從用 Ruby 或 Java 編寫的內容中獲得與您期望的相同級別的覆蓋率,但這絕對值得做。
在最基本的層面上,單元測試只是執行其他代碼位並告訴您它們是否按預期工作的一小段代碼。
您可以簡單地制作一個帶有 main() 函數的新控制台應用程序,該應用程序執行一系列測試函數。 每個測試都會調用您應用程序中的一個函數,並返回 0 表示成功或另一個值表示失敗。
我會給你一些示例代碼,但我對 C 真的很生疏。我相信有一些框架也可以讓這更容易一些。
下面是一個示例,說明如何在單個測試程序中為可能調用庫函數的給定函數實現多個測試。
假設我們要測試以下模塊:
#include <stdlib.h>
int my_div(int x, int y)
{
if (y==0) exit(2);
return x/y;
}
然后我們創建以下測試程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <setjmp.h>
// redefine assert to set a boolean flag
#ifdef assert
#undef assert
#endif
#define assert(x) (rslt = rslt && (x))
// the function to test
int my_div(int x, int y);
// main result return code used by redefined assert
static int rslt;
// variables controling stub functions
static int expected_code;
static int should_exit;
static jmp_buf jump_env;
// test suite main variables
static int done;
static int num_tests;
static int tests_passed;
// utility function
void TestStart(char *name)
{
num_tests++;
rslt = 1;
printf("-- Testing %s ... ",name);
}
// utility function
void TestEnd()
{
if (rslt) tests_passed++;
printf("%s\n", rslt ? "success" : "fail");
}
// stub function
void exit(int code)
{
if (!done)
{
assert(should_exit==1);
assert(expected_code==code);
longjmp(jump_env, 1);
}
else
{
_exit(code);
}
}
// test case
void test_normal()
{
int jmp_rval;
int r;
TestStart("test_normal");
should_exit = 0;
if (!(jmp_rval=setjmp(jump_env)))
{
r = my_div(12,3);
}
assert(jmp_rval==0);
assert(r==4);
TestEnd();
}
// test case
void test_div0()
{
int jmp_rval;
int r;
TestStart("test_div0");
should_exit = 1;
expected_code = 2;
if (!(jmp_rval=setjmp(jump_env)))
{
r = my_div(2,0);
}
assert(jmp_rval==1);
TestEnd();
}
int main()
{
num_tests = 0;
tests_passed = 0;
done = 0;
test_normal();
test_div0();
printf("Total tests passed: %d\n", tests_passed);
done = 1;
return !(tests_passed == num_tests);
}
通過重新定義assert
來更新布爾變量,如果斷言失敗,您可以繼續並運行多個測試,跟蹤成功的次數和失敗的次數。
在每個測試開始時,將rslt
( assert
宏使用的變量)設置為 1,並設置控制存根函數的任何變量。 如果您的存根之一被多次調用,您可以設置控制變量數組,以便存根可以檢查不同調用的不同條件。
由於許多庫函數是弱符號,因此可以在您的測試程序中重新定義它們,以便調用它們。 在調用函數進行測試之前,您可以設置多個狀態變量來控制存根函數的行為並檢查函數參數的條件。
如果您不能像這樣重新定義,請為存根函數指定不同的名稱並在代碼中重新定義要測試的符號。 例如,如果您想存根fopen
但發現它不是弱符號,請將存根定義為my_fopen
並編譯文件以使用-Dfopen=my_fopen
進行測試。
在這種特殊情況下,要測試的函數可能會調用exit
。 這很棘手,因為exit
不能返回到被測試的函數。 這是使用setjmp
和longjmp
有意義的罕見時期之一。 您在進入要測試的函數之前使用setjmp
,然后在存根exit
調用longjmp
以直接返回到您的測試用例。
還要注意,重新定義的exit
有一個特殊的變量,它檢查您是否真的想退出程序並調用_exit
來這樣做。 如果您不這樣做,您的測試程序可能無法完全退出。
此測試套件還計算嘗試和失敗的測試次數,如果所有測試均通過則返回 0,否則返回 1。 這樣, make
可以檢查測試失敗並采取相應措施。
上面的測試代碼將輸出以下內容:
-- Testing test_normal ... success
-- Testing test_div0 ... success
Total tests passed: 2
並且返回碼將為 0。
您可以使用libtap ,它提供了許多可以在測試失敗時提供診斷的功能。 其使用示例:
#include <mystuff.h>
#include <tap.h>
int main () {
plan(3);
ok(foo(), "foo returns 1");
is(bar(), "bar", "bar returns the string bar");
cmp_ok(baz(), ">", foo(), "baz returns a higher number than foo");
done_testing;
}
它類似於其他語言的tap庫。
進行單元測試的最簡單方法是構建一個與其他代碼鏈接的簡單驅動程序代碼,並在每種情況下調用每個函數……並斷言函數結果的值並一點一點地構建。 ..反正我就是這樣做的
int main(int argc, char **argv){ // call some function int x = foo(); assert(x > 1); // and so on.... }
希望這可以幫助。
孤立地測試小段代碼沒有本質上是面向對象的。 在過程語言中,您可以測試函數及其集合。
如果你絕望了,你不得不絕望,我把一個小的 C 預處理器和基於 gmake 的框架組合在一起。 它最初是一個玩具,從未真正長大,但我已經用它來開發和測試幾個中型(10,000 多行)項目。
Dave 的單元測試的干擾最小,但它可以做一些我原以為基於預處理器的框架不可能的測試(你可以要求一段代碼在某些條件下拋出分段錯誤,它會為你測試)。
這也是為什么大量使用預處理器很難安全地做到的一個例子。
使用 C,它必須走得更遠,而不僅僅是在現有代碼之上實現一個框架。
我一直做的一件事是制作一個測試模塊(帶有主模塊),您可以從中運行少量測試來測試您的代碼。 這允許您在代碼和測試周期之間進行非常小的增量。
更大的問題是編寫可測試的代碼。 專注於不依賴共享變量或狀態的小型獨立函數。 嘗試以“功能”方式(無狀態)編寫,這將更容易測試。 如果您的依賴項無法始終存在或運行緩慢(例如數據庫),您可能需要編寫一個完整的“模擬”層,在測試期間可以替換您的數據庫。
單元測試的原則目標仍然適用:確保被測代碼始終重置為給定狀態,不斷測試,等等......
當我用 C 編寫代碼時(回到 Windows 之前),我有一個批處理文件可以調出編輯器,然后當我完成編輯並退出時,它會編譯、鏈接、執行測試,然后用構建結果調出編輯器,測試結果和代碼在不同的窗口。 休息后(一分鍾到幾個小時,取決於正在編譯的內容),我可以查看結果並直接返回編輯。 我相信這個過程可以在這些天得到改進:)
我使用斷言。 雖然它不是一個真正的框架。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.