本記事は、以下の記事を新人プログラマ応援用に改訂したつもりの版です。
paizaラーニングレベルアップ問題集の1つのデータの入力をやってみました。
問題
提出コード
#include <stdio.h>
int main(int argc, char *argv[]){
char s[102];
scanf("%s", s);
printf("%s\n", s);
return 0;
}
実務では、上記の様なコードに対して、上司や先輩から指摘を受けるでしょう。
- 引数
argc,argvが未使用である - 入力チェックがされていない
- バッファオーバーランしてバッファオーバーフローになる
- その他、変数名
sが分かりにくい、エラーチェックがされていない、等
等々。
本記事では、入力チェック及びそのテストに重点を置いて記述します。
入力チェック
- $S$は1文字以上100文字以下の文字列
- $S$の各文字は英小文字または大文字または数字
を満たすか、チェックする関数を作成します。
入力チェック関数は、エントリポイントmain関数とは分けて記述することにします。
プロトタイプ
#ifndef INPUT_H_
#define INPUT_H_
#include <stdbool.h>
bool input(const char*);
#endif /* INPUT_H_ */
テスト
今回は、先にテストコードを記述します。
末尾の改行を除いた入力値が
- 0文字の文字列
- 101文字以上の文字列
- 英小文字、英大文字、数字以外の文字を含む
場合はNGとなることを確認します。
同値分割
同値分割とは、出力が同じになるような入力をそれぞれグループにまとめ、グループの中から代表を選んで行うテストです。
(例)
| グループ | 代表値 | 期待値 |
|---|---|---|
| 英小文字 | m |
真 |
| 英大文字 | N |
真 |
| 数字 | 5 |
真 |
| その他 | _ |
偽 |
境界値分析
境界値分析とは、出力が同じになるような入力をそれぞれグループにまとめ、グループが隣接する境界やその前後の値を入力として行うテストです。
(例)$S$は1文字以上100文字以下の文字列
| グループ | 代表値 | 期待値 |
|---|---|---|
| 1文字未満 | 0文字 | 偽 |
| 1文字以上 | 1文字 | 真 |
| 100文字以下 | 100文字 | 真 |
| 100文字超え | 101文字 | 偽 |
その他
文字列チェックについては
- NULL
- 0文字(空文字)
- 1文字:ここで「同値分割」テストを実施する
- 2文字
- 3文字
はテストするようにしましょう。尚、配列についても文字列(文字配列)と同様のことが言えます。
以上を踏まえると、以下の様なテストケースが考えられます。
| # | 期待値 | ||
|---|---|---|---|
| 1 | NULL | 偽 | |
| 2 | 0文字 | 空文字列 | 偽 |
| 3 | 1文字 | 英小文字 | 真 |
| 4 | 英大文字 | 真 | |
| 5 | 数字 | 真 | |
| 6 | その他 | 偽 | |
| 7 | 2文字 | 正常文字列 | 真 |
| 8 | 先頭が不正 | 偽 | |
| 9 | 末尾が不正 | 偽 | |
| 10 | 3文字 | 正常文字列 | 真 |
| 11 | 先頭が不正 | 偽 | |
| 12 | 中間が不正 | 偽 | |
| 13 | 末尾が不正 | 偽 | |
| 14 | 100文字 | 真 | |
| 15 | 101文字 | 偽 |
今回は、エラー内容に応じて<errno.h>のerrnoを
- 字数が範囲外の場合は
ERANGE - (1~100文字で)英小文字、英大文字、数字以外の文字を含む場合は
EINVAL
に設定することにします。
また、単体テストツールとしてminunit.hを使用します。
#include <stdio.h>
#include <errno.h>
#include "minunit.h"
#include "input.h"
int tests_run = 0;
static char* message(int expected, int actual) {
static char msg[72];
snprintf(msg, sizeof(msg), "Error: expected: <%d> but was: <%d>", expected, actual);
return msg;
}
static char* test_input(const char *str) {
mu_assert("Error: expected: <true> but was: <false>", input(str));
mu_assert(message(0, errno), errno == 0);
return 0;
}
static char* test_input_out_of_range(const char *str) {
mu_assert("Error: expected: <false> but was: <true>", !input(str));
mu_assert(message(ERANGE, errno), errno == ERANGE);
return 0;
}
static char* test_input_invalid(const char *str) {
mu_assert("Error: expected: <false> but was: <true>", !input(str));
mu_assert(message(EINVAL, errno), errno == EINVAL);
return 0;
}
static char* test_input_1() {return test_input_out_of_range(NULL);}
static char* test_input_2() {return test_input_out_of_range("");}
static char* test_input_3() {return test_input("m");}
static char* test_input_4() {return test_input("N");}
static char* test_input_5() {return test_input("5");}
static char* test_input_6() {return test_input_invalid("_");}
static char* test_input_7() {return test_input("Mn");}
static char* test_input_8() {return test_input_invalid("-0");}
static char* test_input_9() {return test_input_invalid("0.");}
static char* test_input_10() {return test_input("A2z");}
static char* test_input_11() {return test_input_invalid("!00");}
static char* test_input_12() {return test_input_invalid("0_0");}
static char* test_input_13() {return test_input_invalid("00~");}
static char* test_input_14() {return test_input("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZab");}
static char* test_input_15() {return test_input_out_of_range("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabc");}
static char* all_tests() {
mu_run_test(test_input_1);
mu_run_test(test_input_2);
mu_run_test(test_input_3);
mu_run_test(test_input_4);
mu_run_test(test_input_5);
mu_run_test(test_input_6);
mu_run_test(test_input_7);
mu_run_test(test_input_8);
mu_run_test(test_input_9);
mu_run_test(test_input_10);
mu_run_test(test_input_11);
mu_run_test(test_input_12);
mu_run_test(test_input_13);
mu_run_test(test_input_14);
mu_run_test(test_input_15);
return 0;
}
int main() {
char *result = all_tests();
if (result != 0) {
fprintf(stderr, "%s\n", result);
fprintf(stderr, "Tests run: %d\n", tests_run);
} else {
fprintf(stdout, "ALL TESTS PASSED\n");
fprintf(stdout, "Tests run: %d\n", tests_run);
}
return result != 0;
}
実装
- 入力値が
NULLの場合はerrnoにERANGEを設定し、falseを返す - 入力文字列が0文字または101文字以上の場合は
errnoにERANGEを設定し、falseを返す - 入力文字列が英小文字、英大文字、数字以外の文字を含む場合は
errnoにEINVALを設定し、falseを返す - 最後
trueを返す
ように実装します。このとき、上述のテストコードも全てパスします。
問題自体は条件分岐もループも使わずに解けるのに出してしまって申し訳ない💦
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include "input.h"
bool input(const char *str) {
errno = 0;
if (!str) {
errno = ERANGE;
return false;
}
size_t len = strlen(str);
if (len < 1 || len > 100) {
errno = ERANGE;
return false;
}
for (const char *c = str; *c; c++) {
if (!isalnum(*c)) {
errno = EINVAL;
return false;
}
}
return true;
}
現場では、正規表現を使うことが多いと思います。
Windows環境の場合、以下のバッチファイルを作成するとテストしやすいと思います。
echo off
gcc -Wall -Wextra -Werror -std=c99 inputTest.c input.c util.c -o inputTest 1>inputTest.txt 2>&1
if exist inputTest.exe (
.\inputTest
echo %ERRORLEVEL%
del inputTest.exe
del inputTest.txt
) else (
type inputTest.txt
)
pause
エントリポイント
上述のinput関数を使ってmain関数を実装します。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include "input.h"
int main(void) {
char str[128];
if (scanf("%127s", str) != 1) {
return EXIT_FAILURE;
}
if (!input(str)) {
if (errno == ERANGE) {
perror("The input value must be at least 1 character and no more than 100 characters.");
} else if (errno == EINVAL) {
perror("Each character of the input value must be a lowercase letter, an uppercase letter, or a digit.");
} else {
perror(NULL);
}
return EXIT_FAILURE;
}
if (puts(str) < 0) {
perror(NULL);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
このプログラムをコンパイルするコマンドは以下の様になります。
gcc -Wall -Wextra -Werror -std=c99 main.c input.c -o main 1>main.txt 2>&1